Java之IO(七)ObjectInputStream和ObjectOutputStream
转载请注明源出处:http://www.cnblogs.com/lighten/p/7003536.html
1.前言
本章介绍Java字节流中重要的成员,对象流ObjectInputStream和ObjectOutputStream。之前的DataInputStream和DataOutputStream只是对一些基本数据类型进行了解析和储存,但是在Java中更常见的是一个对象实例,一个对象实例是十分复杂的,如何保存一个对象呢?这个就是对象流的作用了。对象流最终是要有归属地的,通常也就是文件流。
2.ObjectInputStream
ObjectInputStream虽然需要接受一个输入源,但是并不是继承自FilterInputStream,其继承自抽象父类InputStream,实现了ObjectInput接口,这个接口实现了之前所接触过的DataInput接口,也就是基本数据类型。还实现了ObjectStreamConstants接口,这个接口中只有一些常量,是对象转换的时候的一系列情况约定字节。
其只有一个构造函数:
这个类很复杂,先看构造函数具体做了些什么事情。
第一个verifySubclass()方法是一个安全检验,其作用是校验此类或子类的实例可以在不违反安全约束的情况下构建,子类不能覆写安全敏感的非final方法,否则将检查enableSubclassImplementation的序列化许可。
后面三个类都是ObjectInputStream的内部类,其作用之后介绍。
enableOverride决定后面调用readObjectOverride()还是readObject()方法,true时调用前者。
readStreamHeader()方法用于校验所读流的头,校验的内容是魔方数和版本号。
2.1 BlockDataInputStream
输入流有两种模式:在默认模式下,输入写入的数据与DataOutputStream格式相同; 在“块数据”模式下,输入数据由块数据标记括起来(参见对象序列化规范详情)。 缓冲取决于块数据模式:默认情况下模式,没有预先缓存数据; 当在块数据模式下,所有数据被读入一次(和缓冲)。
BlockDataInputStream中构造了一个PeekInputStream,这个流每次只读一个字节,保存在peekb字段,获取的时候如果这个字段>0就直接返回了,如果小于0就从包装的流中取出一个字段,peek()方法读取的字节,用read()方法还可以读取,但只限一个,如果连续调用两次peek(),再调用read()方法,第一个字节就会丢失,这就是这个流的特点。
字段如下:
从这里可以看出BlockData块数据的缓存大小,默认是非block模式。而我们主要关注上面设置的true,block模式。
BlockDataInputStream里面方法众多,还是通过InputStream的相关方法实现来看吧。
read()方法根据end和pos标识判断是否读完缓存,读取完毕就会重新填充缓存,在setBlockDataMode的时候,如果和默认的模式(false)不一样就会将pos、end、unread都重置为0。然后就返回当前位置的数据,pos+1。读取完了就是返回-1。
refill()方法都过一个unread字段和pos==end的循环判断,来读取。一个块数据,首先会读取blockheader数据,返回block数据的长度,赋值给unread。具体解析block头数据格式过程不做讲解。unread是当前数据块在流中剩余未读取的数据,只要读取到了数据,或无数据可读即end!=0就会跳出循环,下次unread依旧可能不为0,就是当前数据块还没有读完,读完时就是0,又回到了解析block头数据的过程。
currentBlockRemaining()返回的是当前数据块未读的数据,包括缓存中未读的和流中未读取的剩余数据块数据。
BlockDataInputStream小结:看到这里,BlockDataInputStream的其它方法也没有看的必要了,看起来应该也简单。这个类的作用主要就是按块解析数据流,一个数据块包括块头和块身,块头中有一些标识和块身的长度。每次读取都是一个数据块一个个读的,不保证读完了一个数据块。每个块身的大小是1024个字节。看Java规范,这么设计主要是为了对象序列化的前后兼容,以块的形式存储和读取可以兼容1.2版本之前的1.1的次要版本和1.2之后的扩展兼容。
2.2HandleTable
这个类是一个非同步的表,用来追踪对象关联的句柄。在反序列化期间,一个对象首先通过调用assign方法分配一个句柄。这个方法使得分配的句柄处于打开状态,依赖于其它句柄的异常状态,则可以通过markDependency方法注册,或者一个异常可以通过调用markException直接关联。当句柄被标记为异常,HandleTable承担将异常传播到依赖于被标记对象的其它对象的职责。以上是JDK上的部分注释了,可能翻译不到位,再看源码:
根据上面的解释再来看源码,这里三个数组就很清楚了,一个是存对象handler的状态,一个存对象或异常,一个存所有依赖于这个对象的handler。HandlerList这个类很简单,就是一个记录对象依赖句柄序号的数组,add(int handler)方法添加。HandlerTable中的方法如下:
clear():将数组重置成null,byte是0;size()方法就是返回而已;grow()方法是扩容数组,将原数据拷贝。主要关注之前提到的三个方法:
一个对象先被标记成未知状态,将其存入entries中。
根据依赖的状态来选择,从注释的顺序来看,一个对象先被assign再调其它方法,这个时候的状态时unknow。目标是正常的情况下,依赖也就没必要改变,目标有异常,根据传播原则,要将目标的异常传播到本对象。如果目标也是未知状态,就会为目标建立一个list,以后再判断,传导。
这个markException方法也就清楚了,就是将目标的异常传导到其依赖,以及依赖的依赖。finish()则是判断终结后还有未知状态的改成OK状态,所以之前判断异常状态就break,因为处理完了,而ok状态就会抛出异常,因为这个状态应该是最后才会确定的。
HandlerTable小结:对象的构成可能是十分复杂的,在反序列化的步骤中,如果出错了,应该要传导到所有依赖上,比如A对象有个B,B中的C反序列化失败了,B肯定也是异常的,A自然也是异常的了。HandlerTable就是用于计算这个异常传导的。
2.3ValidationList
一旦对象图完成反序列化,就要执行回调的优先列表。
将obj注册到ValidationList,带上优先级,插入链表的相关位置。Callback是一个可以构成链式结构的结构,有下一个的指针。
最后就会按照顺序执行回调了
2.4 readObject()
使用对象流当然是为了将字节读成一个对象了,而不关心一些基本的读取方法,当然这些方法也很简单,都是用到了之前将的BlockDataInputStream的相关方法,看了之前的理解起来也不难。
这个不是子类,enableOverride为false。所以我们需要关注的是后面的流程。初始时,passHandle为-1,即outerHandle就是-1了。后面逻辑之前handlerTable理解了也比较容易,就是查询如果有异常就抛出,没有执行回调,返回obj。finally的时候又将passhandle重置会原值了。下面看readObject0(false)做了什么。
一开始这段是兼容旧的模式,没有看的必要。
读到重置符,重置句柄。
这个就是根据标记,使用不同的方式进行解析了。里面比较复杂,不再进行详细介绍。
2.5 小结读取过程
不考虑旧版本(1.1次要版本)的读取,即非block模式。读取一个对象流,最先读到的四个字节是恒定的,为魔方数[-84, -19]和版本号[0,5],short类型。这个读取就已经是开始读取一个块1024个字节。第5个字节就是上面的readObject0()的switch选项。
3.ObjectOutputStream
略,反向步骤(看的很累-。-!)。反向过程主要就是writeObject方法了。
4.后记
本文对ObjectInputStream进行了详细介绍,但还是有些不明之处,个人对于readObject方法中的handler.markDependency有所疑问,因为最初passHandle应该是-1,这句应该没什么作用才对。eclipse断点不能看到此处的变量值,难验证。ObjectInputStream大致也就这些内容了,只是分拆分析了一下。writeObject没有进行介绍,肯定是和readObject相对应的内容了。下面是一个小例子。
public class Student implements Serializable{ /** * */ private static final long serialVersionUID = 4484961141733473265L; private int id; private String name; private int age; public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", age=" + age + "]"; } }
public class Teacher implements Serializable{ /** * */ private static final long serialVersionUID = -312001656457242689L; private int age; private String name; private Student student; public Teacher(int age, String name, Student student) { this.age = age; this.name = name; this.student = student; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override public String toString() { return "Teacher [age=" + age + ", name=" + name + ", student=" + student + "]"; } }
@Test public void test() throws IOException { Student student = new Student(10, "张三", 12); Teacher teacher = new Teacher(32, "李四", student); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(teacher); byte[] obj = baos.toByteArray(); System.out.println(obj.length); System.out.println(Arrays.toString(obj)); ByteArrayInputStream bais = new ByteArrayInputStream(obj); ObjectInputStream ois = new ObjectInputStream(bais); try { Teacher t = (Teacher) ois.readObject(); System.out.println(t); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
这里面需要序列化的对象必须实现Serializable接口,或者Externalizable接口。这个也给一个例子:
public class Classroom implements Externalizable { private Teacher teacher; private List<Student> students; @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(teacher); out.writeObject(students); } @SuppressWarnings("unchecked") @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.teacher = (Teacher) in.readObject(); this.students = (List<Student>) in.readObject(); } public Teacher getTeacher() { return teacher; } public void setTeacher(Teacher teacher) { this.teacher = teacher; } public List<Student> getStudents() { return students; } public void setStudents(List<Student> students) { this.students = students; } @Override public String toString() { return "Classroom [teacher=" + teacher + ", students=" + students + "]"; } }
@Test public void test2() throws IOException { Student student = new Student(10, "张三", 12); Teacher teacher = new Teacher(32, "李四", student); Classroom classroom = new Classroom(); List<Student> students = new ArrayList<Student>(); students.add(student); classroom.setStudents(students); classroom.setTeacher(teacher); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(classroom); byte[] obj = baos.toByteArray(); System.out.println(obj.length); System.out.println(Arrays.toString(obj)); ByteArrayInputStream bais = new ByteArrayInputStream(obj); ObjectInputStream ois = new ObjectInputStream(bais); try { Classroom c = (Classroom) ois.readObject(); System.out.println(c); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
本文讲的有些不清楚,或许有所差错,如果发现了请指出。下面两篇也可以参考一下。
ObjectStreamField与ObjectStreamClass:http://blog.csdn.net/silentbalanceyh/article/details/8250096
ObjectIuputStream与ObjectOutputStream: http://blog.csdn.net/silentbalanceyh/article/details/8294269
5.补充
5.1 对象流写文件只能写一个对象的问题
先不写入文件,写入一个数组来看看现象:
@Test public void test3() throws IOException { Student student = new Student(10, "张三", 12); Teacher teacher = new Teacher(32, "李四", student); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(student); oos.writeObject(teacher); byte[] obj = baos.toByteArray(); System.out.println(obj.length); System.out.println(Arrays.toString(obj)); ByteArrayInputStream bais = new ByteArrayInputStream(obj); ObjectInputStream ois = new ObjectInputStream(bais); try { Student s = (Student) ois.readObject(); System.out.println(s); Teacher t = (Teacher) ois.readObject(); System.out.println(t); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
这里写入了两个对象,实际上实验了两次,第一次只写了一个Student对象,第二次写入了Student和Teacher对象,对比字节码,前面Student的完全一样。区别如下:
115在再一次调用readObject的时候是一个标识符,意味着读取一个对象Object。上述写两个对象通过ByteArray流是没有问题的,执行如下:
那么再实验一下将两个对象写入文件,再读取。
@Test public void test4() throws IOException { String path = ObjectStreamTest.class.getClassLoader().getResource("").getPath() + "objectstream.txt"; // System.out.println(path); File file = new File(path); Student student = new Student(10, "张三", 12); Teacher teacher = new Teacher(32, "李四", student); FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(student); oos.writeObject(teacher); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream(new File(path)); @SuppressWarnings("resource") ObjectInputStream ois = new ObjectInputStream(fis); try { Student s = (Student) ois.readObject(); System.out.println(s); Teacher t = (Teacher) ois.readObject(); System.out.println(t); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
实验结果如下:
并没有问题,实验环境是JDK8,可能在这个版本解决了这个问题,也许是由于其它问题导致的。总而言之,没有测试出问题。所有测试程序都忘记关闭输入输出流了,各位读者自己补一下,懒的修改了。