
标准序列化机制1、基本用法2、复杂对象3、定制序列化4、序列化的基本原理5、版本问题6、序列化特点分析1、基本用法要让一个类支持序列化只需要让这个类实现接口java.io.Serializable。Serializable没有定义任何方法只是一个标记接口。比如对于前面章节提到的Student类为支持序列化可改为publicclassStudentimplementsSerializable{//省略主体代码}声明实现了Serializable接口后保存/读取Student对象就可以使用ObjectOutput-Stream/ObjectInputStream流了。ObjectOutputStream是OutputStream的子类但实现了Object-Output接口。ObjectOutput是DataOutput的子接口增加了一个方法publicvoidwriteObject(Objectobj)throwsIOException这个方法能够将对象obj转化为字节写到流中。ObjectInputStream是InputStream的子类它实现了ObjectInput接口。ObjectInput是DataInput的子接口增加了一个方法publicObjectreadObject()throwsClassNotFoundException,IOException这个方法能够从流中读取字节转化为一个对象。使用这两个流保存学生列表的代码就可以变为publicstaticvoidwriteStudents(ListStudentstudents)throwsIOException{ObjectOutputStreamoutnewObjectOutputStream(newBufferedOutputStream(newFileOutputStream(students.dat)));try{out.writeInt(students.size());for(Students:students){out.writeObject(s);}}finally{out.close();}}而从文件中读入学生列表的代码可以变为publicstaticListStudentreadStudents()throwsIOException,ClassNotFoundException{ObjectInputStreaminnewObjectInputStream(newBufferedInputStream(newFileInputStream(students.dat)));try{intsizein.readInt();ListStudentlistnewArrayList(size);for(inti0;isize;i){list.add((Student)in.readObject());}returnlist;}finally{in.close();}}实际上只要List对象也实现了SerializableArrayList/LinkedList都实现了上面代码还可以进一步简化读写只需要一行代码如下所示publicstaticvoidwriteStudents(ListStudentstudents)throwsIOException{ObjectOutputStreamoutnewObjectOutputStream(newBufferedOutputStream(newFileOutputStream(students.dat)));try{out.writeObject(students);}finally{out.close();}}publicstaticListStudentreadStudents()throwsIOException,ClassNotFoundException{ObjectInputStreaminnewObjectInputStream(newBufferedInputStream(newFileInputStream(students.dat)));try{return(ListStudent)in.readObject();}finally{in.close();}}是不是很神奇只要将类声明实现Serializable接口然后就可以使用ObjectOutput-Stream/ObjectInputStream直接读写对象了。我们之前介绍的各种类如String、Date、Double、ArrayList、LinkedList、HashMap、TreeMap等都实现了Serializable。2、复杂对象上面例子中的Student对象是非常简单的如果对象比较复杂呢比如如果a、b两个对象都引用同一个对象c序列化后c是保存两份还是一份在反序列化后还能让a、b指向同一个对象吗答案是c只会保存一份反序列化后指向相同对象。如果a、b两个对象有循环引用呢即a引用了b而b也引用了a。这种情况Java也没问题可以保持引用关系。这就是Java序列化机制的神奇之处它能自动处理引用同一个对象的情况也能自动处理循环引用的情况具体例子我们就不介绍了。3、定制序列化默认的序列化机制已经很强大了它可以自动将对象中的所有字段自动保存和恢复但这种默认行为有时候不是我们想要的。对于有些字段它的值可能与内存位置有关比如默认的hashCode()方法的返回值当恢复对象后内存位置肯定变了基于原内存位置的值也就没有了意义。还有一些字段可能与当前时间有关比如表示对象创建时的时间保存和恢复这个字段就是不正确的。还有一些情况如果类中的字段表示的是类的实现细节而非逻辑信息那默认序列化也是不适合的。为什么不适合呢因为序列化格式表示一种契约应该描述类的逻辑结构而非与实现细节相绑定绑定实现细节将使得难以修改破坏封装。比如我们在容器类中介绍的LinkedList它的默认序列化就是不适合的。为什么呢因为LinkedList表示一个List它的逻辑信息是列表的长度以及列表中的每个对象但LinkedList类中的字段表示的是链表的实现细节如头尾节点指针对每个节点还有前驱和后继节点指针等。那怎么办呢Java提供了多种定制序列化的机制主要的有两种一种是transient关键字另外一种是实现writeObject和readObject方法。将字段声明为transient默认序列化机制将忽略该字段不会进行保存和恢复。比如类LinkedList中它的字段都声明为了transient如下所示transientintsize0;transientNodeEfirst;transientNodeElast;声明为了transient不是说就不保存该字段了而是告诉Java默认序列化机制不要自动保存该字段了可以实现writeObject/readObject方法来自己保存该字段。类可以实现writeObject方法以自定义该类对象的序列化过程其声明必须为privatevoidwriteObject(java.io.ObjectOutputStreams)throwsjava.io.IOException可以在这个方法中调用ObjectOutputStream的方法向流中写入对象的数据。比如 LinkedList使用如下代码序列化列表的逻辑数据privatevoidwriteObject(java.io.ObjectOutputStreams)throwsjava.io.IOException{s.defaultWriteObject();//写元素个数s.writeInt(size);//循环写每个元素for(NodeExfirst;x!null;xx.next)s.writeObject(x.item);}需要注意的是代码s.defaultWriteObject();这一行是必需的它会调用默认的序列化机制默认机制会保存所有没声明为transient的字段即使类中的所有字段都是transient也应该写这一行因为Java的序列化机制不仅会保存纯粹的数据信息还会保存一些元数据描述等隐藏信息这些隐藏的信息是序列化之所以能够神奇的重要原因。与writeObject对应的是readObject方法通过它自定义反序列化过程其声明必须为privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException在这个方法中调用ObjectInputStream的方法从流中读入数据然后初始化类中的成员变量。比如LinkedList的反序列化代码为privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException{s.defaultReadObject();//读元素个数intsizes.readInt();//循环读入每个元素for(inti0;isize;i)linkLast((E)s.readObject());}注意代码s.defaultReadObject();这一行代码也是必需的。除了自定义writeObject/readObject方法还有一些自定义序列化过程的机制Exter-nalizable接口、readResolve方法和writeReplace方法这些机制用得相对较少我们就不介绍了。4、序列化的基本原理如果类的字段表示的就是类的逻辑信息如上面的Student类那就可以使用默认序列化机制只要声明实现Serializable接口即可。否则的话如LinkedList那就可以使用transient关键字实现writeObject和read-Object自定义序列化过程。Java的序列化机制可以自动处理如引用同一个对象、循环引用等情况。序列化到底是如何发生的呢关键在ObjectOutputStream的writeObject和ObjectInput-Stream的readObject方法内。它们的实现都非常复杂正因为这些复杂的实现才使得序列化看上去很神奇我们简单介绍其基本逻辑。writeObject的基本逻辑是如果对象没有实现Serializable抛出异常NotSerializableException。每个对象都有一个编号如果之前已经写过该对象了则本次只会写该对象的引用这可以解决对象引用和循环引用的问题。如果对象实现了writeObject方法调用它的自定义方法。默认是利用反射机制遍历对象结构图对每个没有标记为transient的字段根据其类型分别进行处理写出到流流中的信息包括字段的类型即完整类名、字段名、字段值等。readObject的基本逻辑是不调用任何构造方法它自己就相当于是一个独立的构造方法根据字节流初始化对象利用的也是反射机制在解析字节流时对于引用到的类型信息会动态加载如果找不到类会抛出ClassNotFoundException。5、版本问题代码是在不断演化的而序列化的对象可能是持久保存在文件上的如果类的定义发生了变化那持久化的对象还能反序列化吗默认情况下Java会给类定义一个版本号这个版本号是根据类中一系列的信息自动生成的。在反序列化时如果类的定义发生了变化版本号就会变化与流中的版本号就会不匹配反序列化就会抛出异常类型为java.io.InvalidClassException。通常情况下我们希望自定义这个版本号而非让Java自动生成一方面是为了更好地控制另一方面是为了性能因为Java自动生成的性能比较低。怎么自定义呢在类中定义如下变量privatestaticfinallongserialVersionUID1L;在Java IDE如Eclipse中如果声明实现了Serializable而没有定义该变量IDE会提示自动生成。这个变量的值可以是任意的代表该类的版本号。在序列化时会将该值写入流在反序列化时会将流中的值与类定义中的值进行比较如果不匹配会抛出InvalidClassException。那如果版本号一样但实际的字段不匹配呢Java会分情况自动进行处理以尽量保持兼容性大概分为三种情况字段删掉了即流中有该字段而类定义中没有该字段会被忽略新增了字段即类定义中有而流中没有该字段会被设为默认值字段类型变了对于同名的字段类型变了会抛出InvalidClassException。6、序列化特点分析序列化的主要用途有两个一个是对象持久化另一个是跨网络的数据交换、远程过程调用。Java标准的序列化机制有很多优点使用简单可自动处理对象引用和循环引用也可以方便地进行定制处理版本问题等但它也有一些重要的局限性。Java序列化格式是一种私有格式是一种Java特有的技术不能被其他语言识别不能实现跨语言的数据交换。Java在序列化字节中保存了很多描述信息使得序列化格式比较大。Java的默认序列化使用反射分析遍历对象结构性能比较低。Java的序列化格式是二进制的不方便查看和修改。由于这些局限性实践中往往会使用一些替代方案。在跨语言的数据交换格式中ⅩML/JSON是被广泛采用的文本格式各种语言都有对它们的支持文件格式清晰易读。有很多查看和编辑工具它们的不足之处是性能和序列化大小在性能和大小敏感的领域往往会采用更为精简高效的二进制方式如ProtoBuf、Thrift、MessagePack等。至此关于Java的标准序列化机制就介绍完了。我们介绍了它的用法和基本原理最后分析了它的特点它是一种神奇的机制通过简单的Serializable接口就能自动处理很多复杂的事情但它也有一些重要的限制最重要的是不能跨语言。