关于序列化

JDK内置同步器的实现类经常会看到java.io.Serializable接口,这个接口即是Java序列化操作,这样看来序列化也是同步器的一种机制。本节将深入剖析Java中的序列化机制,并看看AQS同步器的序列化,掌握序列化机制才能完整理解JDK内置的同步工具的实现。

在程序中为了能直接以Java对象的形式进行保存,然后再重新得到该Java对象,我们需要序列化能力。序列化其实可以看成是一种机制,即按照一定的格式将Java对象的状态转成介质可接受的形式,以方便存储或传输。Java中进行序列化操作需要实现Serializable或Externalizable接口。

java 序列化 时区 java序列化机制_反序列化

 

序列化过程

Java序列化的大致过程为:序列化时将Java对象相关的类信息、属性及属性值等等以一定的格式转为字节流,反序列化时则根据字节流表示的信息来构建出Java对象。过程中可能涉及到其它对象的引用,所以涉及到引用的对象的相关信息也要参与序列化。如下图所示,Java对象经过序列化后转为字节流,保存字节流的常见方式有文件、内存、网络、数据库。反序列化时则通过这些介质读取字节流,然后还原为Java对象。

java 序列化 时区 java序列化机制_反序列化_02

 

序列化与反序列化

序列化的作用

  • 提供一种简单又可扩展的对象保存恢复机制。
  • 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
  • 可以将对象持久化到介质中,就像实现对象直接存储。
  • 允许对象自定义外部存储的格式。

序列化例子

常见的使用方式是直接将对象写入流中,比如下面的例子中,创建了FileOutputStream对象,对应输出到tmp.o文件中。然后创建ObjectOutputStream对象嵌套前面的输出流,当我们调用writeObject方法时即能进行序列化操作。

java 序列化 时区 java序列化机制_反序列化_03

 

writeObject方法需要特别说明一下,当我们对某个对象进行写入时,其实不仅仅序列化该对象,它还会去遍历寻找相关引用的其它对象,由自己和其它引用对象组成的一个完整的对象图关系都会被序列化。除了一些特殊指定的类,普通类必须实现Serializable或Externalizable接口才能被序列化。

反序列化例子

反序列化是序列化的反向操作,即通过字节流来还原Java对象。看下面的例子,首先创建FileInputStream对象,其对应的文件为tmp.o。然后创建ObjectInputStream对象嵌套前面的输入流,接着则可以调用readObject方法读取对象。readObject方法除了会恢复对象自己之外,它还会遍历整个完整的对象图关系,创建整个对象图包含的所有对象。

java 序列化 时区 java序列化机制_Java_04

 

serialVersionUID的作用

在序列化操作时,经常会看到实现了Serializable接口的类会存在一个serialVersionUID属性,并且它是一个固定数值的静态变量。比如下面的一行代码,这个属性有什么作用?其实它主要用于验证版本的一致性。每个类都拥有这么一个ID,在序列化的时候会一起被写入流中。在反序列化的时候就被拿出来跟当前类的serialVersionUID值进行比较,两者相同则说明版本一致,可以序列化成功,而如果不同则序列化失败。

java 序列化 时区 java序列化机制_序列化_05

 

一般情况下我们可以自己定义serialVersionUID的值或者由IDE帮我们自动生成,如果我们不显示定义serialVersionUID的话,也不代表不存在serialVersionUID,而是由JDK帮我们生成。生成规则是利用类名、类修饰符、接口名、字段、静态初始化信息、构造函数信息、方法名、方法修饰符、方法签名等组成的信息,经过SHA算法生成摘要作为最终的serialVersionUID值。

父类的序列化

如果一个子类实现了Serializable接口而父类没有实现该接口,则在序列化子类时,子类的属性状态会被写入而父类的属性状态将不被写入。所以如果想要父类属性状态也一起参与序列化,就要让它也实现Serializable接口。

另外,如果父类未实现Serializable接口则反序列化生成的对象会再次调用父类的构造函数,以此完成对父类的初始化。所以父类属性初始值一般都是类型的默认值。比如下面的代码,Father类的属性不会参与序列化,反序列化时Father对象的属性的值为默认值0。

java 序列化 时区 java序列化机制_java 序列化 时区_06

 

哪些字段会序列化

在序列化时对象的哪些字段会参与到序列化中呢?其实有两种方式决定哪些字段会被序列化。一是默认方式,Java对象中的非静态和非transient的字段都会被定义为需要序列化的字段。另外一种方式是通过ObjectStreamField数组来声明需要序列化的对象。

可以看到对象中普通的属性都是默认会被序列化的,而如果某些包含了敏感信息的属性我们不希望它参与序列化,那么最简单的方式就是可以将该字段声明为transient。

如何使用ObjectStreamField呢?举个例子,如下代码中A类中有name和password两个字段,我们通过ObjectStreamField数组声明只需序列化name字段。我们不必纠结为什么这样声明,这仅仅是一个约定而已。

java 序列化 时区 java序列化机制_java 序列化 时区_07

 

Externalizable接口

Externalizable接口主要就是提供给用户自己控制序列化内容,虽然前面我们也看到了transient和ObjectStreamField都能定义参与序列化的字段,但实际上Externalizable接口提供了更加灵活的方式。可以看到它其实继承了Serializable接口,然后提供了writeExternal和readExternal两个方法,我们就是在这两个方法内控制序列化和反序列化。

java 序列化 时区 java序列化机制_序列化_08

 

比如下面的例子,我们可以在writeExternal方法中额外写入Date对象,然后再写入value值。对应地,反序列化时则是在readExternal方法中读取Date对象和value。这样就完成了自定义序列化操作。

java 序列化 时区 java序列化机制_序列化_09

 

写入时替换对象

正常情况下序列化某个对象时写入的正是当前的对象,但如果说我们要替换当前的对象而写入其他对象的话则可以通过writeReplace方法来实现。比如下面,person类通过writeReplace方法最终可以写入Object数组对象。所以我们在反序列化时就不再是转换成Person类型,而是要转换为Object数组对象。

java 序列化 时区 java序列化机制_Java_10

 

读取时替换对象

上面介绍了在写入时可以替换对象,而在读取时也同样支持替换对象的,它是通过readResolve方法实现的。比如下面的代码,在readResolve方法返回2222,则反序列化读取时不再是Person对象,而是2222。

java 序列化 时区 java序列化机制_java 序列化 时区_11

 

AQS的序列化

JDK内置并发AQS同步器实现了Serializable接口,其中head和tail变量声明为transient。也就是说如果对AQS同步器对象进行序列化的话,队列是不参与序列化的,只有同步状态会参与序列化。也就是说序列化会让AQS丢失队列信息,只能保留同步状态信息。

java 序列化 时区 java序列化机制_Java_12

作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。