序列化
- 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的格式的过程。 即将数据转换为字节流
- 反序列化 (Deserialization)是通过从存储或者网络读取对象的状态,重新创建该对象。即将字节流转换回对象
- 序列化广泛应用在远程调用(RPC)或者数据存取。
Serializable接口
Serializable接口,这是一个空接口;
- 如果一个类实现了Serializable接口,那么就代表这个类是自动支持序列化和反序列化的,毋须我们编程实现。
- 如果一个类没有实现Serializable接口,那么默认是不能被序列化的,除非使用其他办法。
- 只能序列化类的状态,不能序列化类的方法。
如果一个类实现了Serializable接口,那么它的子类默认也可以被序列化和反序列化,不论子类是否实现了Serializable接口。
如果一个类实现了Serializable接口,其父类没有实现Serializable接口,那么父类必须有无参的构造器,并且父类中的状态默认不能被序列化。如果想要序列化和反序列化父类,需要在子类里干预序列化的过程,例如使用readObject和writeOjbect方法来序列化和反序列化父类的状态。
实例
- 如何把一个对象序列化到磁盘上,并从磁盘上将该对象恢复,使用的是ObjectOutputStream和ObjectInputStream。
- 使用transient关键字、readObject和writeOjbect方法、writeReplace和readresolve方法干预序列化。
实现序列化
下面的例子演示了如何使用ObjectOutputStream把一个Person对象写到磁盘上,之后再通过ObjectInputStream把对象恢复。
执行下面的代码可以发现:
- 创建Person对象的时候,Person构造器输出了
I am constructor,too.
;但反序列化构建Person对象的时候,并没有使用构造器。 - 反序列化回来得到的Person对象p2和之前的p1内地地址不一样,是深拷贝。
1 | public static void main(String[] args) throws IOException,ClassNotFoundException { |
Person类,仅有name和age两个属性。
1 | import java.io.Serializable; |
干预序列化
3.1 transient
并不是所有java对象里的内容,都是我们想要序列化写出去的,例如一些隐私数据,比如password等。对于上面的Person对象,假如age是一个隐私字段,我们不想序列化写到磁盘上,那么我们可以用transient关键字来标记它。
1 | private transient int age; |
3.2 readObject和writeOjbect方法
我们可以通过readObject和writeOjbect方法来干预序列化,例如把待序列化的对象的某个属性字段加密和解密,或者把transient关键字标记的属性序列化写出去。
1 | /** |
3.3 readResolve和writeReplace方法
前文已经说过,反序列化得到的对象,和序列化之前的对象,不是同一个对象,它们的内存地址不相同。那么,假设一个单例类实现了Serializable接口,反序列化时内存里就会出现多个实例,这违背了当初设计单例的初衷。
Java的序列化机制提供了一个钩子方法,即私有的readresolve方法,允许我们来控制反序列化时得到的对象。下面的代码就说明了readresolve方法的用法,可以看出反序列化得到的对象,还是唯一的单例对象。
与readresolve方法对应,还有一个writeReplace方法,可以让我们在writeOjbect方法之前修改序列化的对象。
1 | public class Singleton implements Serializable { |
验证代码如下,同时也可以看出,序列化时,先执行了writeReplace方法,后执行了writeObject方法;在反序列化的时候,先执行了readObject方法,最后执行了readResolve方法。
1 | public static void main(String[] args) throws IOException,ClassNotFoundException { |
输出结果如下:
1 | readResolve |
通常情况下,如无必要,只需要writeObject只和readObject配合使用就够了。此外,关于Serializable,还可能有readObjectNoData()和serialVersionUID需要关注。假如Person对象被序列化到磁盘上了,第二天Person类被修改了,多了旧对象里没有的内容,那么可以实现readObjectNoData,弥补这种因临时扩展而无法兼容反序列化的缺陷。serialVersionUID用于标记对象的版本。
拓展部分
上面已经知道可以通过writeObject和readObject、readResolve和writeReplace、还有readObjectNoData方法来干预序列化和反序列化。那么这些方法是什么时候被调用的呢?
他们是在ObjectOutputStream或ObjectInputStream中,配合SerialCallbackContext、ObjectStreamClass类,通过反射回调实现的。ObjectStreamClass类中有invokeWriteObject、invokeReadObject、invokeReadObjectNoData、invokeWriteReplace、invokeReadResolve等方法;有兴趣的同学,可以去看看源码。
以前文的Person类中private void writeObject(ObjectOutputStream out) throws IOException
的调用为例,我们去ObjectOutputStream类的writeObject()方法开始,依次看下列方法就知道了。
1 | 1.ObjectOutputStream.writeObject(Object obj) |