上一篇《Java中的IO流(五)》把流中的打印流PrintStream,PrintWriter,序列流SequenceInputStream以及结合之前所记录的知识点完成了文件的切割与文件的合并功能,接下来我们接着记录有关于Java中流的其它知识点。
这一系列的知识点虽然取名叫“Java中的IO流”,便实质上有些知识点并非IO包中的内容,只是跟IO流相关联或相类似,所以归纳进来了。
一,能操作对象的流:ObjectOutputStream和ObjectInputStream
ObjectOutputStream将Java对象的基本数据类型和图形写入OutputStream。可以使用ObjectInputStream读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或另一个进程中重构对象。
此流只能支持实现了Serializable接口的对象写入流中。每个实现了Serializable对象的类都被编码,编码内容包括类名和类签名,对象的字段值和数组值,以及从初始对象中物其他所有对象的闭包。
实现了Serializable接口的Pserson类代码如下
1 public class Person implements Serializable { 2 private String name; 3
4 public String getName() { 5 return name; 6 } 7
8 public void setName(String name) { 9 this.name = name; 10 } 11
12 public int getAge() { 13 return age; 14 } 15
16 public void setAge(int age) { 17 this.age = age; 18 } 19
20 private int age; 21 }
Serializable接口里没有任何的方法,它只是一个标识接口,标识所实现了此接口的类具有被序列化以及反序列化的功能。
将一个Person对象序列化到obj.object文件中代码如下
1 private static void function_demo1() throws IOException { 2 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.object")); 3 Person person = new Person(); 4 person.setName("张三"); 5 person.setAge(32); 6 oos.writeObject(person); 7 oos.close(); 8 }
将此被序列化的对象反序列化读出来的代码如下
1 private static void function_demo2() throws IOException, ClassNotFoundException { 2 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.object")); 3 Person p = (Person) ois.readObject(); 4 System.out.println(p.getName()); 5 System.out.println(p.getAge()); 6 ois.close(); 7 }
Exception in thread "main" java.io.InvalidClassException: com.zw.otherslikestream.Person; local class incompatible: stream classdesc serialVersionUID = 3588443867695946454, local class serialVersionUID = -3836108205857858437
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at com.zw.otherslikestream.OtherLikeStreamExercise.function_demo2(OtherLikeStreamExercise.java:19)
at com.zw.otherslikestream.OtherLikeStreamExercise.main(OtherLikeStreamExercise.java:14)
从上面的错误信息我们可以看出,它的意思是“ stream classdesc serialVersionUID = 3588443867695946454”和“local class serialVersionUID = -3836108205857858437”不一样,所以才会出错。
其原理是这样的:当我们让一个类实现了Serializable接口时,编译时编译器会用一个名叫serialVersionUID的版本号与该类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException
。可序列化类可以通过声明名为 "serialVersionUID"
的字段(该字段必须是静态 (static)、最终 (final) 的 long
型字段)显式声明其自己的 serialVersionUID。
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面(类中字段的访问修饰符啊,数据类型啊等等)计算该类的默认 serialVersionUID 值,不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException
。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private
修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
了解了以上的原理后我们手动为Person类添加一个serialVersionUID:private static final long serialVersionUID = 1L;然后再序列化此类后修改此类信息再反序列化此类便不会报错。
三,transient关键字
我们再次修改Person类
1 public class Person implements Serializable { 2 private static final long serialVersionUID = 1L; 3 private String name; 4 private static int age; 5
6 public String getName() { 7 return name; 8 } 9
10 public void setName(String name) { 11 this.name = name; 12 } 13
14 public int getAge() { 15 return age; 16 } 17
18 public void setAge(int age) { 19 this.age = age; 20 } 21
22 }
即将字段age改为静态的。再次运行序列化,然后再反序列化此类,发现age变为0了;原理是这样的,静态的字段是被放在JVM的静态区域的,属于共享数据,序列化Person类的时候不可能拿到静态区域里的东西即Serializable接口不可以序列化静态字段或方法;
若类中的某一个字段不能作为共享数据即不需要被static所修饰,又不想被序列化,则此字段可以被transient关键安来修饰,如下
1 public class Person implements Serializable { 2 private static final long serialVersionUID = 1L; 3 private transient String name; 4 private static int age; 5
6 public String getName() { 7 return name; 8 } 9
10 public void setName(String name) { 11 this.name = name; 12 } 13
14 public int getAge() { 15 return age; 16 } 17
18 public void setAge(int age) { 19 this.age = age; 20 } 21
22 }
此时Person类的对象被序列化,再被反序列化时,此字段的值为null
1 private static void function_demo3() throws IOException { 2 RandomAccessFile raf = new RandomAccessFile("raf.txt", "rw");//以读取写入方法创建文件(若文件不存在) 3 raf.write("sb".getBytes());//写入两个字节 4 raf.writeInt(97);//写入一个Int数据,此方法按四个字节写入文件,先写高位 5 raf.write("小龙".getBytes());//由于是utf8编码形式,所以两个汉字占用了六个字节 6 raf.writeInt(109);//写入四个字节 7 raf.close();8 }
RandomAccessFile类的读取操作
1 private static void function_demo4() throws IOException { 2 RandomAccessFile raf = new RandomAccessFile("raf.txt", "r"); 3 byte[] bt = new byte[2];//因为存储时是两个字节,所以这里声明字节数组长度为2
4 raf.read(bt);//从RandomAccessFile所维护的byte数组中读取前两个字节的内容
5 System.out.println(new String(bt));//转为字符输出
6 System.out.println(raf.readInt());//用readInt方法读取写入的int数据97
7 byte[] be = new byte[6];//因为存储时两个汉字共6个字节,所以声明长度为6
8 raf.read(be);//读取6个字节到be
9 System.out.println(new String(be));//转成字符输出
10 System.out.println(raf.readInt());//读取109数据
11 raf.close(); 12 }
RandomAccessFile类的随机读取与写入操作只是针对于此类所维护的那个byte数组的指针的位置,以下是示例代码
1 private static void function_demo5() throws IOException { 2 RandomAccessFile raf = new RandomAccessFile("raf.txt", "rw"); 3 raf.seek(10);// 将RandomAccessFile维护的数组指针移动到下标为10的位置
4 raf.write("李四".getBytes());// 写入六个字节
5 System.out.println(raf.getFilePointer());// 获取当前指针的位置16
6 raf.seek(10);// 将指针移动到10的位置,可直接读取到刚才写放的内容
7 byte[] bt = new byte[3]; 8 int len; 9 while ((len = raf.read(bt)) != -1) { 10 System.out.print(new String(bt) + " "); 11 } 12 raf.close(); 13 }
1 public class Input implements Runnable { 2 private PipedInputStream input; 3
4 public Input(PipedInputStream input) { 5 this.input = input; 6 } 7
8 @Override 9 public void run() { 10 try { 11 int len; 12 byte[] bt = new byte[1024]; 13 while ((len = input.read(bt)) != -1) { 14 System.out.println(new String(bt)); 15 } 16 input.close(); 17 } catch (IOException e) { 18 e.printStackTrace(); 19 } 20 } 21
22 }
1 public class Output implements Runnable { 2 private PipedOutputStream output; 3
4 public Output(PipedOutputStream output) { 5 this.output = output; 6 } 7
8 @Override 9 public void run() { 10 try { 11 output.write("测试管道流".getBytes()); 12 output.close(); 13 } catch (IOException e) { 14 e.printStackTrace(); 15 } 16 } 17
18 }
1 private static void function_demo6() throws IOException { 2 PipedInputStream input = new PipedInputStream(); 3 PipedOutputStream output = new PipedOutputStream(); 4 input.connect(output); 5 new Thread(new Input(input)).start(); 6 new Thread(new Output(output)).start(); 7 }
六,操作基本数据类型的流对象DataInputStream和DataOutputStream
IO流中提供了两个操作基本数据类型的流对象,可对基本数据类型进行流的读取与写入操作,这两个流提供了对各种基本数据类型的读与写的操作,其中有两个特殊的读写方法readUTF()和writeUTF(),这两个方法分别使用UTF-8修改版编码将一个字符串写入或读出流中,演示如下
1 private static void function_demo7() throws IOException { 2 DataOutputStream outputStream = new DataOutputStream(new FileOutputStream("output.txt")); 3 outputStream.writeUTF("张三"); 4 outputStream.close(); 5 }
1 private static void function_demo8() throws IOException { 2 DataInputStream inputStream = new DataInputStream(new FileInputStream("output.txt")); 3 System.out.println(inputStream.readUTF()); 4 inputStream.close(); 5 }
七,操作数组的流ByteArrayInputStream和ByteArrayOutputStream,操作字符的流CharArrayReader和CharArrayWriter,操作字符串的流StringReader和StringWriter
由于这几个流的操作方式基本相同,所以下面演示一下操作数组的流,其它两个流就不再演示了
1 private static void function_demo9() throws IOException { 2 ByteArrayInputStream in = new ByteArrayInputStream("李四".getBytes()); 3 ByteArrayOutputStream ba = new ByteArrayOutputStream(); 4 int len; 5 while ((len = in.read()) != -1) { 6 ba.write(len); 7 } 8 System.out.println(ba.toString()); 9 }