本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营连接:http://item.jd.com/12299018.htmlhtml
在前面几节,咱们在将对象保存到文件时,使用的是DataOutputStream,从文件读入对象时,使用的是DataInputStream, 使用它们,须要逐个处理对象中的每一个字段,咱们提到,这种方式比较啰嗦,Java中有一种更为简单的机制,那就是序列化。java
简单来讲,序列化就是将对象转化为字节流,反序列化就是将字节流转化为对象。在Java中,具体如何来使用呢?它是如何实现的?有什么优缺点?本节就来探讨这些问题,咱们先从它的基本用法谈起。编程
基本用法
微信
Serializable网络
要让一个类支持序列化,只须要让这个类实现接口java.io.Serializable,Serializable没有定义任何方法,只是一个标记接口。好比,对于57节提到的Student类,为支持序列化,可改成:工具
public class Student implements Serializable { String name; int age; double score; public Student(String name, int age, double score) { ... } ... }
声明实现了Serializable接口后,保存/读取Student对象就可使用另两个流了ObjectOutputStream/ObjectInputStream。性能
ObjectOutputStream/ObjectInputStreamthis
ObjectOutputStream是OutputStream的子类,但实现了ObjectOutput接口,ObjectOutput是DataOutput的子接口,增长了一个方法:spa
public void writeObject(Object obj) throws IOException
这个方法可以将对象obj转化为字节,写到流中。代理
ObjectInputStream是InputStream的子类,它实现了ObjectInput接口,ObjectInput是DataInput的子接口,增长了一个方法:
public Object readObject() throws ClassNotFoundException, IOException
这个方法可以从流中读取字节,转化为一个对象。
使用这两个流,57节介绍的保存学生列表的代码就能够变为:
public static void writeStudents(List<Student> students) throws IOException { ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream("students.dat"))); try { out.writeInt(students.size()); for (Student s : students) { out.writeObject(s); } } finally { out.close(); } }
而从文件中读入学生列表的代码能够变为:
public static List<Student> readStudents() throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new BufferedInputStream( new FileInputStream("students.dat"))); try { int size = in.readInt(); List<Student> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add((Student) in.readObject()); } return list; } finally { in.close(); } }
实际上,只要List对象也实现了Serializable (ArrayList/LinkedList都实现了),上面代码还能够进一步简化,读写只须要一行代码,以下所示:
public static void writeStudents(List<Student> students) throws IOException { ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream("students.dat"))); try { out.writeObject(students); } finally { out.close(); } } public static List<Student> readStudents() throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new BufferedInputStream( new FileInputStream("students.dat"))); try { return (List<Student>) in.readObject(); } finally { in.close(); } }
是否是很神奇?只要将类声明实现Serializable接口,而后就可使用ObjectOutputStream/ObjectInputStream直接读写对象了。咱们以前介绍的各类类,如String, Date, Double, ArrayList, LinkedList, HashMap, TreeMap等,都实现了Serializable。
复杂对象
上面例子中的Student对象是很是简单的,若是对象比较复杂呢?好比:
咱们分别来看下。
引用同一个对象
咱们看个简单的例子,类A和类B都引用了同一个类Common,它们都实现了Serializable,这三个类的定义以下:
class Common implements Serializable { String c; public Common(String c) { this.c = c; } } class A implements Serializable { String a; Common common; public A(String a, Common common) { this.a = a; this.common = common; } public Common getCommon() { return common; } } class B implements Serializable { String b; Common common; public B(String b, Common common) { this.b = b; this.common = common; } public Common getCommon() { return common; } }
有三个对象, a, b, c,以下所示:
Common c = new Common("common"); A a = new A("a", c); B b = new B("b", c);
a和b引用同一个对象c,若是序列化这两个对象,反序列化后,它们还能指向同一个对象吗?答案是确定的,咱们看个实验。
ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.writeObject(a); out.writeObject(b); out.close(); ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream(bout.toByteArray())); A a2 = (A) in.readObject(); B b2 = (B) in.readObject(); if (a2.getCommon() == b2.getCommon()) { System.out.println("reference the same object"); } else { System.out.println("reference different objects"); }
输出为:
reference the same object
这也是Java序列化机制的神奇之处,它能自动处理这种引用同一个对象的状况。更神奇的是,它还能自动处理循环引用的状况,咱们来看下。
循环引用
咱们看个例子,有Parent和Child两个类,它们相互引用,类定义以下:
class Parent implements Serializable { String name; Child child; public Parent(String name) { this.name = name; } public Child getChild() { return child; } public void setChild(Child child) { this.child = child; } } class Child implements Serializable { String name; Parent parent; public Child(String name) { this.name = name; } public Parent getParent() { return parent; } public void setParent(Parent parent) { this.parent = parent; } }
定义两个对象:
Parent parent = new Parent("老马"); Child child = new Child("小马"); parent.setChild(child); child.setParent(parent);
序列化parent, child两个对象,Java能正确序列化吗?反序列化后,还能保持原来的引用关系吗?答案是确定的,咱们看代码实验:
ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.writeObject(parent); out.writeObject(child); out.close(); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream( bout.toByteArray())); parent = (Parent) in.readObject(); child = (Child) in.readObject(); if (parent.getChild() == child && child.getParent() == parent && parent.getChild().getParent() == parent && child.getParent().getChild() == child) { System.out.println("reference OK"); } else { System.out.println("wrong reference"); }
输出为:
reference OK
神奇吧?
定制序列化
默认的序列化机制已经很强大了,它能够自动将对象中的全部字段自动保存和恢复,但这种默认行为有时候不是咱们想要的。
好比,对于有些字段,它的值可能与内存位置有关,好比默认的hashCode()方法的返回值,当恢复对象后,内存位置确定变了,基于原内存位置的值也就没有了意义。还有一些字段,可能与当前时间有关,好比表示对象建立时的时间,保存和恢复这个字段就是不正确的。
还有一些状况,若是类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不适合的。为何不适合呢?由于序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改,破坏封装。
好比,咱们在容器类中介绍的LinkedList,它的默认序列化就是不适合的,为何呢?由于LinkedList表示一个List,它的逻辑信息是列表的长度,以及列表中的每一个对象,但LinkedList类中的字段表示的是链表的实现细节,如头尾节点指针,对每一个节点,还有前驱和后继节点指针等。
那怎么办呢?Java提供了多种定制序列化的机制,主要的有两种,一种是transient关键字,另一种是实现writeObject和readObject方法。
将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。好比,类LinkedList中,它的字段都声明为了transient,以下所示:
transient int size = 0; transient Node<E> first; transient Node<E> last;
声明为了transient,不是说就不保存该字段了,而是告诉Java默认序列化机制,不要自动保存该字段了,能够实现writeObject/readObject方法来本身保存该字段。
类能够实现writeObject方法,以自定义该类对象的序列化过程,其声明必须为:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException
能够在这个方法中,调用ObjectOutputStream的方法向流中写入对象的数据。好比,LinkedList使用以下代码序列化列表的逻辑数据:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out size s.writeInt(size); // Write out all elements in the proper order. for (Node<E> x = first; x != null; x = x.next) s.writeObject(x.item); }
须要注意的是第一行代码:
s.defaultWriteObject();
这一行是必须的,它会调用默认的序列化机制,默认机制会保存全部没声明为transient的字段,即便类中的全部字段都是transient,也应该写这一行,由于Java的序列化机制不只会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息,这些隐藏的信息是序列化之因此可以神奇的重要缘由。
与writeObject对应的是readObject方法,经过它自定义反序列化过程,其声明必须为:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
在这个方法中,调用ObjectInputStream的方法从流中读入数据,而后初始化类中的成员变量。好比,LinkedList的反序列化代码为:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject(); // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i = 0; i < size; i++) linkLast((E)s.readObject()); }
注意第一行代码:
s.defaultReadObject();
这一行代码也是必须的。
序列化的基本原理
稍微总结一下:
但,序列化究竟是如何发生的呢?关键在ObjectOutputStream的writeObject和ObjectInputStream的readObject方法内。它们的实现都很是复杂,正由于这些复杂的实现才使得序列化看上去很神奇,咱们简单介绍下其基本逻辑。
writeObject的基本逻辑是:
readObject的基本逻辑是:
版本问题
上面的介绍,咱们忽略了一个问题,那就是版本问题。咱们知道,代码是在不断演化的,而序列化的对象多是持久保存在文件上的,若是类的定义发生了变化,那持久化的对象还能反序列化吗?
默认状况下,Java会给类定义一个版本号,这个版本号是根据类中一系列的信息自动生成的。在反序列化时,若是类的定义发生了变化,版本号就会变化,与流中的版本号就会不匹配,反序列化就会抛出异常,类型为java.io.InvalidClassException。
一般状况下,咱们但愿自定义这个版本号,而非让Java自动生成,一方面是为了更好的控制,另外一方面是为了性能,由于Java自动生成的性能比较低,怎么自定义呢?在类中定义以下变量:
private static final long serialVersionUID = 1L;
在Java IDE如Eclipse中,若是声明实现了Serializable而没有定义该变量,IDE会提示自动生成。这个变量的值能够是任意的,表明该类的版本号。在序列化时,会将该值写入流,在反序列化时,会将流中的值与类定义中的值进行比较,若是不匹配,会抛出InvalidClassException。
那若是版本号同样,但实际的字段不匹配呢?Java会分状况自动进行处理,以尽可能保持兼容性,大概分为三种状况:
高级自定义
除了自定义writeObject/readObject方法,Java中还有以下自定义序列化过程的机制:
这些机制实际用到的比较少,咱们简要说明下。
Externalizable是Serializable的子接口,定义了以下方法:
void writeExternal(ObjectOutput out) throws IOException void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
与writeObject/readObject的区别是,若是对象实现了Externalizable接口,则序列化过程会由这两个方法控制,默认序列化机制中的反射等将再也不起做用,再也不有相似defaultWriteObject和defaultReadObject调用,另外一个区别是,反序列化时,会先调用类的无参构造方法建立对象,而后才调用readExternal。默认的序列化机制因为须要分析对象结构,每每比较慢,经过实现Externalizable接口,能够提升性能。
readResolve方法返回一个对象,声明为:
Object readResolve()
若是定义了该方法,在反序列化以后,会额外调用该方法,该方法的返回值才会被当作真正的反序列化的结果。这个方法一般用于反序列化单例对象的场景。
writeReplace也是返回一个对象,声明为:
Object writeReplace()
若是定义了该方法,在序列化时,会先调用该方法,该方法的返回值才会被当作真正的对象进行序列化。
writeReplace和readResolve能够构成一种所谓的序列化代理模式,这个模式描述在<Effective Java> 第二版78条中,Java容器类中的EnumSet使用了该模式,咱们通常用的比较少,就不详细介绍了。
序列化特色分析
序列化的主要用途有两个,一个是对象持久化,另外一个是跨网络的数据交换、远程过程调用。
Java标准的序列化机制有不少优势,使用简单,可自动处理对象引用和循环引用,也能够方便的进行定制,处理版本问题等,但它也有一些重要的局限性:
因为这些局限性,实践中每每会使用一些替代方案。在跨语言的数据交换格式中,XML/JSON是被普遍采用的文本格式,各类语言都有对它们的支持,文件格式清晰易读,有不少查看和编辑工具,它们的不足之处是性能和序列化大小,在性能和大小敏感的领域,每每会采用更为精简高效的二进制方式如ProtoBuf, Thrift, MessagePack等。
小结
本节介绍了Java的标准序列化机制,咱们介绍了它的用法和基本原理,最后分析了它的特色,它是一种神奇的机制,经过简单的Serializable接口就能自动处理不少复杂的事情,但它也有一些重要的限制,最重要的是不能跨语言。
在接来下的几节中,咱们来看一些替代方案,包括XML/JSON和MessagePack。
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。