15.8.5 自定义序列化
15.8.5 自定义序列化
递归序列化
- 当对某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,
- 如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;
- 如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化
- 如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;
这种情况被称为递归序列化。
transient关键字
不希望序列化的实例变量的情况
在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息等,这时不希望系统将该实例变量值进行序列化;
或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSerializableException
异常。
通过在实例变量前面使用transient
关键字修饰,可以指定Java
序列化时无须理会该实例变量。transient
关键字只能用于修饰实例变量,不可修饰Java
程序中的其他成分。
程序 transient关键字修饰实例变量
如下Person
类与前面的Person
类几乎完全一样,只是它的age
使用了transient
关键字修饰。
1 | public class Person implements java.io.Serializable { |
程序 序列化后反序列化 再输出transient修饰的实例变量
下面程序先序列化一个Person
对象,然后再反序列化该Person
对象,得到反序列化的Person
对象后程序输出该对象的age
实例变量值。
1 | import java.io.*; |
上面程序中的
代码1创建了一个Person
对象,并为它的name
、age
两个实例变量指定了值;
代码2将该Person
对象序列化后输出;
代码3从序列化文件中读取该Person
对象;
代码4输出该Person
对象的age
实例变量值。
由于本程序中的Person
类的age
实例变量使用transient
关键字修饰,所以p.getAge()将得到0,而不是500。
transient关键字的问题
使用transient
关键字修饰实例变量虽然简单、方便,但被transient
修饰的实例变量将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java
对象时无法取得该实例变量值。
自定义序列化机制
Java
还提供了一种自定义序列化机制,**通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量(与使用transient
关键字的效果相同)**。
在类中添加特殊的方法
在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化。
1 | private void writeObject(java.io.ObjectOutputStream out) throws IOException |
writeObject方法
writeObject
方法负责写入特定类的实例状态,以便相应的readObject
方法可以恢复它。通过重写该writeObject方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要怎样序列化。在默认情况下,该方法会调用out.defaultObject
来保存Java
对象的各实例变量,从而可以实现序列化Java
对象状态的目的
readObject方法
readObject
方法负责从流中读取并恢复对象实例变量,通过重写该readObject方法,程序员可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,以及如何进行反序列化。在默认情况下,该方法会调用in default Readobject
来恢复Java
对象的非瞬态实例变量。在通常情况下,readObject
方法与writeObject
方法对应,如果writeObject
方法中对Java
对象的实例变量进行了一些处理,则应该在readObject
方法中对其实例变量进行相应的反处理,以便正确恢复该对象。
readObjectNoData方法
当序列化流不完整时,readObjectNoData
方法可以用来正确地初始化反序列化的对象。例如,接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统都会调用readObjectNoData
方法来初始化反序列化的对象。
程序 自定义序列化
下面的Person
类提供了writeObject
和readObject
两个方法,其中writeObject
方法在保存Person
对象时将其name
实例变量包装成StringBuffer
,并将其字符序列反转后写入;
在readObject
方法中处理name
的策略与此对应——先将读取的数据强制类型转换成StringBuffer
,再将其反转后赋给nane
实例变量
1 | import java.io.*; |
上面程序中提供了writeObject
和readObject
用以实现自定义序列化,对于这个Person
类而言,序列化、反序列化Person
实例并没有任何区别——区别在于序列化后的对象流,即使有Cracker
截获到Person
对象流,他看到的name
也是加密后的name
值,这样就提高了序列化的安全性。writeObject
方法存储实例变量的顺序应该和readObject
方法中恢复实例变量的顺序一致,否则将不能正常恢复该Java
对象。
对Person
对象进行序列化和反序列化的程序与前面程序没有任何区别,故此处不再赘述。
更彻底的自定义序列化方式
writeReplace方法
还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象。如果需要实现序列化某个对象时替换该对象,则应为序列化类提供如下特殊方法。
1 | ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException |
此writeReplace
方法将由序列化机制调用,只要该方法存在。因为该方法可以拥有私有(private
)受保护的(protected
)和包私有(package-private
)等访问权限,所以其子类有可能获得该方法。
程序 使用writeReplace方法
例如下面的Person
类提供了writeReplace
方法,这样可以在写入Person
对象时将该对象替换成ArrayList
1 | import java.util.*; |
Java
的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace
方法,如果该方法返回另一个Java
对象,则系统转为序列化另一个对象。如下程序表面上是序列化Person
对象,但实际上序列化的是ArrayList
1 | import java.io.*; |
上面程序中代码一使用writeObject
写入了一个Person
对象,但代码二使用readObject
方法返回的实际上是一个ArayList
对象,这是因为Person
类的writeReplace
方法返回了个ArrayList
对象,所以序列化机制在序列化Person
对象时,实际上是转为序列化ArrayList
对象。
先调用writeReplace方法 再调用writeObject方法
根据上面的介绍,可以知道系统在序列化某个对象之前,会先调用该对象的writeReplace
和writeObject
两个方法,系统总是先调用被序列化对象的writeReplace
方法,如果该方法返回另一个对象,系统将再次调用另一个对象的writeReplace
方法…直到该方法不再返回另一个对象为止,程序最后将调用该对象的writeObject
方法来保存该对象的状态。
readResolve方法
与writeReplace()
方法相对的是,序列化机制里还有一个特殊的方法,它可以实现保护性复制整个对象。这个方法就是:
1 | ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; |
readResolve
方法会紧接着readObject
之后被调用,readResolve
方法的返回值将会代替原来反序列化的对象,而原来readObject
反序列化的对象将会被立即丢弃。
readResolve()
方法在序列化单例类、枚举类时尤其有用。
当然,如果使用Java5
提供的enum
来定义枚举类,则完全不用担心,程序没有任何问题。
早期枚举类问题
但如果应用中有早期遗留下来的枚举类,例如下面的Orientation
类就是一个枚举类。
1 | import java.io.*; |
在Java5
以前,这种代码是很常见的。Orientation
类的构造器私有,程序只有两个Orientation
对象,分别通过Orientation
的HORIZONTAL
和VERTICAL
两个常量来引用。但如果让该类实现Serializable
接口,则会引发一个问题,如果将一个Orientation. HORIZONTAL
值序列化后再读出,如下代码片段所示
1 | import java.io.*; |
**如果立即拿ori
和Orientation.HORIZONTAL
值进行比较,将会发现返回false
**。也就是说,ori
是个新的Orientation
对象,而不等于Orientation
类中的任何枚举值——虽然Orientation
的构造器是private
的,但反序列化依然可以创建Orientation
对象。
前面已经指出,反序列化机制在恢复Java
对象时无须调用构造器来初始化Java
对象从这个意义上来看,序列化机制可以用来“克隆”对象。
在这种情况下,可以通过为Orientation
类提供一个readResolve()
方法来解决该问题,readResolve()
方法的返回值将会代替原来反序列化的对象,也就是让反序列化得到的Orientation
对象被直接丢弃。
下面是为Orientation
类提供的readResolve()
方法(程序清单同上)。
1 | // 为枚举类增加readResolve()方法 |
通过重写readResolve()
方法可以保证反序列化得到的依然是Orientation
的HORIZONTAL
或VERTICAL
两个枚举值之一。
单例类 枚举类都应该提供readResolve方法
所有的单例类、枚举类在实现序列化时都应该提供readResolve()
方法,这样才可以保证反序列化的对象依然正常。
readResolve方法需要注意的问题
与writeReplace()
方法类似的是,readResolve()
方法也可以使用任意的访问控制符,因此父类的readResolve()
方法可能被其子类继承。这样利用readResolve()
方法时就会存在一个明显的缺点,就是当父类已经实现了readResolve()
方法后,子类将变得无从下手。如果父类包含一个protected
或public
的readResolve
方法,而且子类也没有重写该方法,将会使得子类反序列化时得到一个父类的对象——这显然不是程序要的结果,而且也不容易发现这种错误。
总是让子类重写readResolve()
方法无疑是一个负担,因此对于要被作为父类继承的类而言,实现readResolve(()
方法可能有一些潜在的危险。
通常的建议是:
- 对于
final
类,重写readResolve()
方法不会有任何问题; - 否则,重写
readResolve()
方法时应尽量使用private
修饰该方法。