1.操作对象的流
作用:用于对象的持久化,序列化存储。
只能序列化堆中数据,也就是不包括方法和静态成员。
如果不想序列化某个堆中数据,用transient修饰。
能直接写入读取各种基本数据类型和Object类型数据。
ObjectInputStream:
Object readObject();如果文件中有多个对象,可以多次读取,直到为null为止,指针会自动移动。
ObjectOutputStream:
writeObject(Object);可以向一个文件中写入多个对象。
对象必须实现Serializable接口,该接口没有方法需要覆盖,这种类型接口一般称为标记接口,就是用于标记类能用于序列化。
类似于食品上的QS标记。
标记:
static final long serialVersionUID = 42L;
本质就是为了对象序列化方便,这是当我们从文件中读取到对象时与类相关联的标示,如果没有自定义该值,那么系统根据成员等算出一个值,因此当你修改了类时,就会得到不同的UID,也就不能将对象与类匹配上了,除非自定义个该值,使其固定为某一个值。
/*
对象的持久化序列化存储
1.写入使用ObjectOutputStream--writeObject(Object)
2.读取使用ObjectInputStream--Object readObject();
可以向一个文件中写入多个对象,读取时readObject方法自动移动指针。
存储到文件中的是对象在堆中的数据,也就是不包括方法和静态等方法区数据。
存取多个对象:
1.try catch捕获异常EOFException
2.将多个对象存到数组或者集合中,再序列化到文件中
*/
import java.io.*;
import java.util.*;
class ObjectStreamDemo
{
public static void main(String[] args) throws IOException,ClassNotFoundException
{
writeObj();
readObj();
}
private static void readObj()throws IOException,ClassNotFoundException
{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("person.txt"));
//使用ObjectOutputStream写入的对象数据只能通过ObjectInputStream读取。
//读取多个对象方式1:try catch捕获EOFException异常简单处理
/*
try
{
for(Object obj=ois.readObject();obj!=null;obj=ois.readObject())
{
((Person)obj).show();
}
}
catch (EOFException e)
{
System.out.println("已到文件末尾");//给出已到文件末尾的提示,或者什么都不写
}
*/
//方式1虽然也能解决问题,但是不建议那样使用,因为对异常的简单处理是不合适的
Object obj=ois.readObject();
ArrayList<Person> arr=(ArrayList<Person>)obj;
for(Person p : arr)
{
p.show();
}
}
private static void writeObj()throws IOException
{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("person.txt"));
/*
oos.writeObject(new Person("helong",22));
//被序列化的对象必须是被标记的,也就是实现了Serializable接口。
//该接口没有任何方法,只是作为标记存在。
oos.writeObject(new Person("ldy",21));
oos.writeObject(new Person("laowang",78));
oos.close();
*/
//方式2,将对象存入集合,将集合序列化
ArrayList<Person> arr=new ArrayList<Person>();
arr.add(new Person("helong",22));
arr.add(new Person("ldy",21));
arr.add(new Person("laowang",56));
oos.writeObject(arr);
oos.close();
}
}
运行图:
2.管道流
PipedInputStream,PipedOutputStream,PipedWriter,PipedReader
可以直接连接而不需要中转站(例如其他输入输出流交互时通常使用中间变量数组等),与多线程结合。
//测试管道流的使用
import java.io.*;
class Read implements Runnable
{
private PipedInputStream in;
Read(PipedInputStream in)
{
this.in=in;
}
public void run()
{
try
{
byte[] by=new byte[1024];
System.out.println("正在等待读取数据......");
int len = in.read(by);
System.out.println("数据读取完毕,打印:");
System.out.println(new String(by,0,len));
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
in.close();
}
catch (IOException e)
{
}
}
}
}
class Write implements Runnable
{
private PipedOutputStream out;
Write(PipedOutputStream out)
{
this.out=out;
}
public void run()
{
try
{
System.out.println("开始写入数据.....");
out.write("数据:测试,,测试,,测试,,测试,,测试".getBytes());
System.out.println("数据写入完毕");
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
out.close();
}
catch (IOException e)
{
}
}
}
}
class PipedStreamDemo
{
public static void main(String[] args) throws IOException
{
PipedInputStream in=new PipedInputStream();
PipedOutputStream out=new PipedOutputStream();
in.connect(out);
new Thread(new Read(in)).start();
new Thread(new Write(out)).start();
}
}
运行图:
3.特殊类RandomAccessFile
随机访问文件,这是这个类的直译,根据Java见名知意的特点,我们知道这也就是这个类的功能特点。
特点:任意位置向文件中写入,或者从任意文件位置读取数据,通过设置指针。
该类不是IO体系的子类,它直接继承自Object,但是它是IO包的成员,因为其具备读写功能,内部封装了一个数组,通过指针对数组中元素进行操作。
它能完成读写原因就是内部封装了字节输入输出流。
内部将数据放到一个大型byte[]中。
getFilePointer:得到指针位置。
seek(int):设置指针位置。
skipBytes(int):向后跳过N个字节,不能往前。
通过构造函数看出,它只能操作文件。通过mode参数"r","rw"来确定是只读还是读写。
"r":如果文件不存在,则不会创建,直接报错。
"rw":如果文件不存在,则创建,如果存在,不会覆盖。
(为什么每次运行程序看着都好像是被覆盖了呢?这是因为每次指针都会回到文件开头,因此如果每次程序中的数据不更改的话是看不出变化的,我们可以第一次运行不做变化,第二次是先seek(100),再写入就能看到没有覆盖的效果了)
若模式为只读,不会创建文件,如果文件不存在,报异常。
若模式为读写,操作文件不存在,会创建,如果存在,不会覆盖。
作用:实现多线程下载(每个线程负责一整块,互不干扰,写入的时候写到指定的地方(随机))。
//RandomAccessFile测试
//小练习:结合多线程,在一个文件中分段写入数据
//例如:位置1位置2位置3位置1位置2位置3......
//用处:多线程下载,下载好之后,按照格式分段取出来就可以了
import java.io.*;
import java.util.*;
//线程1下载的内容放到文件的位置1处
/*
0 20 40
60 80 100
120 140 160
20字节为步长。
*/
class Load1 implements Runnable
{
private RandomAccessFile raf=null;
Load1(RandomAccessFile raf)
{
this.raf=raf;
}
public void run()
{
try
{
for(int i=0;i<100;i++)
{
raf.seek(60*i);
byte[] by=new byte[16];
by=Arrays.copyOf("(数据1)".getBytes(),16);
raf.write(by);
raf.writeInt(i);
}
raf.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
//线程2下载的内容放到文件的位置2处
class Load2 implements Runnable
{
private RandomAccessFile raf=null;
Load2(RandomAccessFile raf)
{
this.raf=raf;
}
public void run()
{
try
{
for(int i=0;i<100;i++)
{
raf.seek(60*i+20);
byte[] by=new byte[16];
by=Arrays.copyOf("(数据2)".getBytes(),16);
raf.write(by);
raf.writeInt(i);
}
raf.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
//线程3下载的内容放到文件的位置3处
class Load3 implements Runnable
{
private RandomAccessFile raf=null;
Load3(RandomAccessFile raf)
{
this.raf=raf;
}
public void run()
{
try
{
for(int i=0;i<100;i++)
{
raf.seek(60*i+40);
byte[] by=new byte[16];
by=Arrays.copyOf("(数据3)".getBytes(),16);
raf.write(by);
raf.writeInt(i);
}
raf.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
class RandomAccessDemo
{
public static void main(String[] args) throws IOException
{
//write();
//read();
//load();
test_load();
}
private static void read()throws IOException
{
//该类只能操作文件,可以设置模式
RandomAccessFile raf=new RandomAccessFile("random.txt","r");
for(int i=0;i<raf.length();i+=20)
{
raf.seek(i);//以20个字节为步长读取数据
byte[] by=new byte[16];
raf.read(by);
int age=raf.readInt();
System.out.println("姓名:"+new String(by)+"年龄:"+age);
}
}
private static void write()throws IOException
{
RandomAccessFile raf=new RandomAccessFile("random.txt","rw");
BufferedReader br=new BufferedReader(new FileReader("randomCount.txt"));
//BufferedWriter bw=new BufferedWriter(new FileWriter("randomCount.txt"));
//该语句不能放在这里,应该放下面,因为如果放在这里在打开文件randomCount.txt时会采用覆盖的方式,那么br就取不到其中的值了。
int count=Integer.parseInt(br.readLine());
BufferedWriter bw=new BufferedWriter(new FileWriter("randomCount.txt"));
bw.write(count+1+"");
raf.seek(20*count);//指针移动单位为字节,根据此处规定:姓名用16个字节,年龄为int用4个字节,供20个字节一个人。
byte[] by=new byte[16];//规定名字用16个字节表示
by=Arrays.copyOf("何龙123".getBytes(),16);
raf.write(by);
raf.writeInt(232);//write方法虽然接收int,但是只是写入int的低8位,因此容易数据丢失
//观察文件看到,姓名后的年龄位占用两个位置,也就是四个字节,正式int大小
raf.close();
br.close();
bw.close();
}
//多线程下载练习
private static void load()throws FileNotFoundException
{
RandomAccessFile raf1=new RandomAccessFile("load.txt","rw");
RandomAccessFile raf2=new RandomAccessFile("load.txt","rw");
RandomAccessFile raf3=new RandomAccessFile("load.txt","rw");
new Thread(new Load1(raf1)).start();
new Thread(new Load2(raf2)).start();
new Thread(new Load3(raf3)).start();
}
//从多线程下载文件中分批取出数据
private static void test_load()throws IOException
{
RandomAccessFile raf=new RandomAccessFile("load.txt","r");
for(int k=0;k<=40;k+=20)
{
for(int i=0;(60*i+k)<raf.length();i++)
{
raf.seek(60*i+k);
byte[] by=new byte[16];
raf.read(by);
int age=raf.readInt();
System.out.println(new String(by)+"...."+age);
}
}
}
}
运行图:
4.操作基本数据类型的流对象
DataOutputStream,DataInputStream
凡是操作基本数据类型就是用这两个类,由于是读取,非常方便。
/*
操作基本数据类型的流对象
writeInt,writeBoolean....
writeUTF:以UTF-8形式写入数据,也就只能使用readUTF读取
*/
import java.io.*;
class DataStreamDemo
{
public static void main(String[] args) throws IOException
{
write();
read();
writeUTF_8();
readUTF_8();
writeUTF();
}
private static void writeUTF()throws IOException
{
//使用UTF,GBK等指定编码表写入数据需要用到转换流
OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("utf.txt"),"utf-8");
osw.write("何龙");
osw.write("小小红");
osw.write("小明");
osw.close();
}
private static void readUTF_8()throws IOException
{
DataInputStream dis=new DataInputStream(new FileInputStream("utf-8.txt"));
sop(dis.readUTF());
sop(dis.readUTF());
sop(dis.readUTF());
dis.close();
}
private static void writeUTF_8()throws IOException
{
DataOutputStream dos=new DataOutputStream(new FileOutputStream("utf-8.txt"));
dos.writeUTF("何龙");
dos.writeUTF("小小红");
dos.writeUTF("小明");
dos.close();
}
private static void read()throws IOException
{
DataInputStream dis=new DataInputStream(new FileInputStream("data.txt"));
//怎么写的,就怎么读
sop(dis.read());
sop(dis.read());
sop(dis.read());
sop(dis.readInt());
sop(dis.readBoolean());
sop(dis.readDouble());
dis.close();
}
private static void write()throws IOException
{
//可以想象,该对象内部是封装了与数据有关的方法,也就是说
//还需要一个流就OK了,因此可知构造函数就是传入一个流对象
DataOutputStream dos=new DataOutputStream(new FileOutputStream("data.txt"));
dos.write(255);//写入的是255的最低八位
dos.write(256);
dos.write(-127);//该方法无法正确的写入读取负数
dos.writeInt(123);
dos.writeBoolean(true);
dos.writeDouble(234.234);
dos.close();
}
private static void sop(Object obj)
{
System.out.println(obj);
}
}
运行图:
5.操作字节数组的流对象
ByteArrayOutputStream,ByteArrayInputStream
特点:
ByteArrayOutputStream :构造函数不需要参数(也可以指定缓冲区大小)。
目的为该对象内部封装了的一个数组中。
ByteArrayInputStream :构造函数需要传入一个byte数组作为源。
不操作任何系统资源:1.不需要关闭(关了也能使用)2.不会报IO异常(除了writeTo方法)
本质:将数据从源数组导入目的数组中。
意义:1.很多操作封装好了,使用方便。2.使用流操作的思想来操作数组。
特别方法:
writeTo(OutputStream) :将数组中数据一次性写入一个输出流。
//操作字节数组的流对象
//特殊方法:writeTo
//特点:源和目的均为内存数组
//无需关闭,不会报IO异常,除了writeTo方法
//好处:1.对数组的操作被封装好了,使用方便2.使用流的思想操作数组
import java.io.*;
class ByteArrayStreamDemo
{
public static void main(String[] args)
{
ByteArrayInputStream bis=new ByteArrayInputStream("何龙最帅!!!".getBytes());//字节数组为参数
ByteArrayOutputStream bos=new ByteArrayOutputStream();//无需构造参数
for(int ch=bis.read();ch!=-1;ch=bis.read())
{
bos.write(ch);//将数据写入buf
}
System.out.println(bos.size());
System.out.println(bos.toString());
try
{
bos.writeTo(new FileOutputStream("writeTo.txt"));
}
catch (IOException e)
{
}
}
}
运行图:
6.操作字符数组的流对象
CharArrayReader(需要一个字符数组作为源)
CharArrayWriter(封装了一个字符数组作为目的)
7.操作字符串的流对象
StringReader
(需要一个字符串为源)
StringWriter
(封装了一个字符串为目的)
方法,操作等与操作字节数组的流对象一致。
8.字符编码
码表:
1.ASCII
2.ISO8859-1
3.GB2312
4.GBK(两个字节表示一个中文)
5.unicode
6.UTF-8(最多三个字节表示一个中文)
IO流中跟字符编码有关的就是转换流了:
OutputStreamWriter,InputStreamReader在它们的构造函数中,可以指定编码表。
如果不指定,默认为GBK编码,如果使用默认,那么就等价于FileWriter,FileReader类
编码:字符串变成字节数组。str.getBytes(charsetName);
解码:字节数组变成字符串。new String(byte[],charsetName);
如果编码时指定了错误的编码表,那么就无法正确解码,例如:有中文的文件,指定了iso编码。
如果编码时指定正确编码表,解码时指定了错误的编码表呢?
情况:1.使用GBK编码"何龙",得到[-70,-50,-63,-6]
2.使用iso解码[-70,-50,-63,-6],得到????
解决:1.使用iso编码????,得到[-70,-50,-63,-6]
2.使用GBK解码[-70,-50,-63,-6],得到"何龙"
例外:如果使用GBK编码,使用U8解码,无法通过该方法得到正确字符串。
原因:U8也能识别中文,它不会简单的保存原文的字节到文件中,而是到未知码区去比对,找到最接近的就存储进来,这样一来存储的字节本来都是错的。
联通问题:
记事本默认使用GBK编码"联通"得到的四个字节的开头二进制位均符合U8编码的字节头规则,因此被记事本误以为是
用U8编码,因此它使用U8解码,因此得到乱码。
解决问题:
本质:添入别的字符,使得某些字节不符合U8的字节头规则,因此就不会被误以为是U8编码。
U8如何决定之后的几个字节代表一个字符(UTF-8中代表一个字符可以是一个,两个或者三个字节)?
字节头规则:
一个字节存储字符:0--- ----
两个字节存储字符:(1)110- ---- (2)10-- ----
三个字节存储字符:(1)1110 ---- (2)10-- ---- (3)10-- ----
//字符编码
import java.io.*;
class EncodeDemo
{
public static void main(String[] args) throws IOException
{
//GBK写入
//OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("gbk.txt"),"GBK");
//UTF-8写入
OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("utf8.txt"),"UTF-8");
//上句等价于FileWriter fw=new FileWriter("gbk.txt");
//GBK读取
//InputStreamReader isr=new InputStreamReader(new FileInputStream("gbk.txt"),"GBK");
//UTF-8读取
InputStreamReader isr=new InputStreamReader(new FileInputStream("utf8.txt"),"UTF-8");
//上句等价于FileReader fr=new FileReader("gbk.txt");
osw.write("何龙");
osw.close();
char[] chs=new char[1024];
int len=isr.read(chs);
System.out.println(new String(chs,0,len));
isr.close();
}
}
运行图:
//编码解码情况测试
import java.util.*;
class EncodeDemo2
{
public static void main(String[] args) throws Exception
{
String str="何龙";
//解码
//byte[] b1=str.getBytes();
//byte[] b1=str.getBytes("utf-8");
//byte[] b1=str.getBytes("iso8859-1");//如果编码就指定了错误码表(此处iso无法识别中文),那么是无法正确解码的。
//编码正确,解码错误的情况
byte[] b1=str.getBytes();
System.out.println(Arrays.toString(b1));//GBK[-70,-50,-63,-6],U8[-28,-67,-107,-23,-66,-103]
//编码
//String result1=new String(b1);
//String result1=new String(b1,"utf-8");
String result1=new String(b1,"iso8859-1");
System.out.println("str="+result1);
//解决方法
//虽然解码错了,但是字节本身是正确的,因此我们将得到的错误字符反向编码
//得到正确的字节,再用这些字节利用正确编码表解码就可以得到正确的字符
byte[] by=result1.getBytes("iso8859-1");//得到正确字节数组
System.out.println(Arrays.toString(by));
String right=new String(by,"GBK");
System.out.println("right="+right);
//这种方法不能解决的问题
//例如使用GBK编码,使用U8解码,无法利用编码解码来还原为正确字符
//原因:U8识别中文,它不会原样保留GBK编码得到的字节,而是去查找这些字节,
//没在正常码表中找到,就到未知码表中查找相似的,就会返回一些奇怪的字节数据
//对应的字符们,因此此时的字节已经不是通过GBK编码得到的了,也就无法还原到
//正确的字符了。
}
}
运行图:
/*
联通问题:
当我们在记事本中写入"联通"时,
保存,关闭,再打开发现就变成了乱码
这是为何呢?
我们知道所谓乱码就是编码和解码不是同一
张码表。
*/
class LianTong
{
public static void main(String[] args) throws Exception
{
//分析联通原因
String str="联通";
byte[] by=str.getBytes("GBK");
for(byte b : by)
{
System.out.println(Integer.toBinaryString(b&255));
/*
<strong><span style="color:#ff0000;">我们发现联通这两个字的四个字节的开头二进制位符合U8表的字节头规则
因此当记事本发现时,它就认为这是由U8编码的,因此它使用U8解码,因此
就得到了乱码
解决这个很简单,如果我们在文件中添加别的中文,那么就不存在这个问题了
本质:让先出现的字节的头不符合U8的字节头规则,那么记事本就不会使用U8
解码</span></strong>
*/
}
}
}
运行图:
/*
需求:
将5个学生信息用键盘输入:name,语文成绩,数学成绩,英语成绩
将这些数据按总分从大到小排列存入文件stud.txt中
思路:
1.接收键盘输入一行数据,分割得到信息,存入对象中
2.由于需要处理多个数据,因此需要集合,需要排序因此使用TreeSet
3.将数据写入文件中
源:
a.内存键盘 b.纯文本 c.设备键盘
FileInputStream
目的
a.硬盘文件 b.纯文本 c.设备硬盘文件
FileOutputStream
对学生进行排序时,主要条件是成绩,次要是姓名。
*/
import java.io.*;
import java.util.*;
class Student implements Comparable<Student>
{
private String name;
private double chinese;
private double math;
private double english;
Student(String name,double chinese,double math,double english)
{
this.name=name;this.chinese=chinese;this.math=math;this.english=english;
}
public int compareTo(Student s)
{
if(this.getSum()>s.getSum())return 1;
else if(this.getSum()<s.getSum())return -1;
return this.getName().compareTo(s.getName());
}
public double getSum()
{
return chinese+math+english;
}
public double getCh()
{return chinese;}
public double getMa()
{return math;}
public double getEn()
{return english;}
public String getName()
{
return name;
}
public String toString()
{
return "Name:"+name+"\tChinese:"+chinese+"\tMath:"+math+"\tEnglish:"+english;
}
//定义类该具有的方法
public int hashCode()
{
System.out.println("hashcode");
return name.hashCode()+(int)(math*13);
}
public boolean equals(Object obj)
{
System.out.println("equals");
if(!(obj instanceof Student))throw new ClassCastException("类型不匹配");
Student temp=(Student)obj;
return this.name.equals(temp.getName())&&this.getSum()==temp.getSum();
}
}
class IOTest
{
public static void main(String[] args) throws IOException
{
writeStudentInfo();
}
private static void writeStudentInfo() throws IOException
{
//读取键盘的固定写法
BufferedReader fr=new BufferedReader(new InputStreamReader(System.in));
BufferedWriter fw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream("stud.txt")));
//需求是按照成绩从大到小排序,也就是与自然顺序相反,可以使用Collections.reverseOrder方法
Comparator<Student> com=Collections.reverseOrder();
TreeSet<Student> set=new TreeSet<Student>(com);
//输入以over结尾
for(String data = fr.readLine();data!=null&&!"over".equals(data);data=fr.readLine())
{
String[] info=data.split(",");
set.add(new Student(info[0],new Double(info[1]),new Double(info[2]),new Double(info[3])));
}
for(Student temp : set)
{
fw.write(temp.toString());
fw.newLine();
fw.flush();//带缓冲区的一定要flush
}
fr.close();
fw.close();
}
}
运行图: