0%

Java 文件高级技术(四):标准序列化机制

1. 序列化/反序列化的概念

  • 序列化:将对象转化为字节流
  • 反序列化:将字节流转化为对象

2. Java 中标准序列化机制的基本用法

  • 要让一个类支持序列化,只需要让这个类实现接口 java.io.Serializable

    • Serializable 没有定义任何方法,只是一个标记接口
    • 声明实现了 Serializable 接口后,保存/读取对象就可以使用 ObjectOutputStream/ObjectInputStream 流了
    • 之前保存/读取对象使用的是 DataOutputStream/DataInputStream,需要逐个处理对象中的每个字段,比较繁琐
  • ObjectOutputStreamOutputStream 的子类,实现了 ObjectOutput 接口

    • ObjectOutput 接口是 DataOutput 的子接口,增加了一个方法:public void writeObject(Object obj) throws IOException
    • writeObject(Object obj) 方法能够将内存中的一个对象 obj 转化为字节,写到流中(序列化)
  • ObjectInputStreamInputStream 的子类,实现了 ObjectInput 接口

    • ObjectInput 接口是 DataInput 的子接口,增加了一个方法:public Object readObject() throws ClassNotFoundException, IOException
    • readObject() 方法能够从流中读取字节,转化为内存中的一个对象 obj(反序列化)
  • StringDateDoubleArrayListLinkedListHashMapTreeMap 等,都实现了 Serializable 接口

3. 使用流 ObjectOutputStream/ObjectInputStream 保存/读入学生列表(假设学生类 Student 已实现 Serializable 接口)

  • 保存学生列表(序列化)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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.writeObjects(s); //与之前相比,不用单独处理每个字段
    }
    } finally {
    out.close();
    }
    }
  • 读入学生列表(反序列化)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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();
    }
    }

4. 接上题,上面代码的优化

  • 思路:实际上,只要 List 对象也实现了 SerializableArrayList/LinkedList 都实现了),上面代码还可以进一步简化,读写只需要一行代码,省去了循环相关代码

  • 保存学生列表(序列化)

    1
    2
    3
    4
    5
    6
    7
    8
    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();
    }
    }
  • 读入学生列表(反序列化)

    1
    2
    3
    4
    5
    6
    7
    8
    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 {

    }
    }

5. Java 序列化机制对复杂对象的处理

  • Java 序列化机制能自动处理引用同一个对象的情况,也能自动处理循环引用的情况(具体例子可以参看公号“老马说编程”第 62 篇文章)
  • 如果 a、b 两个对象都引用同一个对象 c,序列化后 c 是保存两份还是一份?在反序列化后还能让 a、b 指向同一个对象吗?答案是,c 只会保存一份,反序列化后指向相同对象
  • 如果 a、b 两个对象有循环引用呢?即 a 引用了 b,而 b 也引用了 a。答案是,这种情况 Java 也没问题,可以保持引用关系

6. 定制序列化的原因/场景/理由是

  • 默认的序列化机制已经很强大了,它可以自动将对象中的所有字段自动保存和恢复。但这种默认行为有时候不是我们想要的
  • 对于有些字段,它的值可能与内存位置有关。比如默认的 hashCode() 方法的返回值,当恢复对象后,内存位置肯定变了,基于原内存位置的值也就没有意义了
  • 对于有些字段,可能与当前时间有关,比如表示对象创建时的时间,保存和恢复这个字段就是不正确的

7. 如果类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不合适的。为什么

  • 因为序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改、破坏封装
  • 比如 LinkedList,它的默认序列化就是不合适的。因为 LinkedList 表示一个 List,它的逻辑信息是列表的长度以及列表中的每个对象;但 LinkedList 类中的字段表示的是链表的实现细节,如头尾节点指针,对每个节点,还有前驱和后继节点指针等

8. 怎样自定义序列化

  • transient 关键字

    • 将字段声明为 transient,默认序列化机制将忽略该字段,不会自动保存和恢复该字段。自己实现 writeObject()/readObject() 方法来保存和恢复该字段
    • 可以实现 writeObject() 方法,以自定义该类对象的序列化过程,其声明必须为:private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException
    • writeObject() 对应的是 readObject() 方法,通过它自定义反序列化过程,其声明必须为:private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
  • 除了自定义 writeObject()/readObject() 方法,还有一些自定义序列化过程的机制Externalizable 接口、realResolve() 方法和 writeReplace() 方法,这些机制用得相对较少

9. LinkedList 的序列化和反序列化的源码中代码 s.defaultWriteObject()/s.defaultReadObject() 的作用是

  • 这行代码是必需的,它会调用默认的序列化机制,默认机制会保存所有没声明为 transient 的字段,即使类中的所有字段都是 transient,也应该写这一行
  • 这是因为 Java 的序列化机制不仅会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息
  • 这些隐藏的信息是序列化之所以能够神奇的重要原因

10. 总结一下 Java 的序列化机制

  • 如果类的字段表示的就是类的逻辑信息,那就可以使用默认序列化机制,只要声明实现 Serializable 接口即可
  • 否则的话,比如 LinkedList,那就可以使用 transient 关键字,实现 writeObject()readObject() 自定义序列化过程
  • Java 的序列化机制可以自动处理引用同一个对象循环引用等情况

11. 序列化的基本原理是,或者说序列化到底是如何发生的

  • 关键在 ObjectOutputStreamwriteObject()ObjectInputStreamreadObject() 方法内。这两个方法的实现都非常复杂,正因为这些复杂的实现才使得序列化看上去很神奇

  • writeObject() 方法的基本逻辑

    • 如果对象没有实现 Serializable,抛出异常 NotSerializableException
    • 每个对象都有一个编号,如果之前已经写过该对象了,则本次只会写该对象的引用,这可以解决对象引用和循环引用的问题
    • 默认是利用反射机制,遍历对象结构图,对每个没有标记为 transient 的字段,根据其类型,分别进行处理,写出到流,流中的信息包括字段的类型,即完整类名、字段名、字段值等
  • readObject() 方法的基本逻辑

    • 不调用任何构造方法
    • 它自己就相当于是一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制
    • 在解析字节流时,对于引用到的类型信息,会动态加载,如果找不到类,会抛出异常 ClassNotFoundException

12. 代码是在不断演化的,而序列化的对象可能是持久化保存在文件上的,如果类的定义发生了变化,那持久化的对象还能反序列化吗,或者说 Java 序列化机制怎样解决版本问题

  • 默认情况下,Java 会给类定义一个版本号,这个版本号是根据类中一系列信息自动生成的。反序列化时,如果类的定义发生了变化,版本号就会变化,与流中的版本号就会不匹配,反序列化就会抛出异常,类型是 java.io.InvalidClassException

  • 通常情况下,我们希望自定义这个版本号,而非让 Java 自动生成,一方面是为了更好地控制,另一方面是为了性能,因为 Java 自动生成的性能比较低

  • 自定义这个版本号的方法

    • 在类中定义变量:private static final long serialVersionUID = 1L;,这个变量的值可以是任意的,代表该类的版本号
    • 在序列化时,会将该值写入流;反序列化时,会将流中的值与类定义中的值进行比较。如果不匹配,会抛出 InvalidClassException
  • 如果版本号一样,但实际的字段不匹配呢?Java 会分情况自动进行处理,以尽量保持兼容性,大概分为三种情况:

    • 字段删掉了:即流中有该字段,而类定义中没有,该字段会被忽略
    • 新增了字段:即类定义中有,而流中没有,该字段会被设为默认值
    • 字段类型变了:对于同名的字段,类型变了,会抛出 InvalidClassException

13. Java 标准序列化机制的用途

  • 对象持久化
  • 跨网络的数据交换
  • 远程过程调用

14. Java 标准序列化机制的优点

  • 使用简单(Serializable
  • 可自动处理对象引用循环引用
  • 可以方便地进行定制transientwriteObject()readObject()
  • 可以方便地处理版本问题serialVersionUID

15. Java 序列化机制的局限性

  • Java 序列化格式是一种私有格式,是一种 Java 特有的技术,不能被其他语言识别,不能实现跨语言的数据交换

    • Java 在序列化字节中保存了很多描述信息,使得序列化格式比较大
    • Java 的默认序列化使用反射分析遍历对象结构,性能比较低
  • Java 的序列化格式是二进制的不方便查看和修改

  • 在性能和大小敏感的领域,往往会采用更为高效的二进制方式,如 ProtoBufThriftMessagePack

-------------------- 本文结束感谢您的阅读 --------------------