Effective Java 2 Serialization
对象序列化(object serialization):将对象编码成字节流(序列化),并从字节流编码中重新构建对象(反序列化)
一旦对象被序列化后,它的编码就可以在机器之间传递,或者被存储到磁盘上,供以后反序列化使用。
第74条:谨慎地实现Serializable接口
要使一个类可被实例化,在声明中加入implements Serializable即可。然后实现序列化需要代价的,如下:
代价
实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了"改变这个类的实现"的灵活性。
如果一个类实现了Serializable接口,它的字节流编码就变成了它的导出的API的一部分。也就是说,如果你使用了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,从而失去了信息隐藏工具的有效性。
如果你接受了默认序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。即client用旧版本序列化,用新版本反序列化,结果导致失败。
序列化会使类的演变受到限制。每个可序列化的类都有一个唯一标识号与它相关联,称作序列版本UID(serial version UID)。如果你没有显示的声明该标识号,系统会自动地根据这个类来调用一个复杂运算过程来产生标识号。所以,如果你增加了一个工具方法,自动产生的UID也会变化,从而导致兼容性遭到破坏,产生InvalidClassException异常。
代价2:
它增加了出现Bug和安全漏洞的可能性。
序列化机制是一种语言之外的对象创建机制。无论你是接受了默认的行为,还是覆盖了默认的行为,反序列化机制都是一个"隐藏的构造器”,具备与其他构造器相同的特点。
因为反序列化机制中没有显示的构造器,所以很容易忘记:反序列化过程必须要保证所有"由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息,而依靠默认的反序列化机制很容易遭到破坏。
代价3:
随着类发行新的版本,相关的测试负担也增加了
很容易理解,新版本和旧版本之间的序列化与反序列化,必须要保证测试成功。
益处
如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化。 实现Serializable接口就非常有必要。
如HttpServlet实现序列化,因此会话状态(session state)可以被缓存。Throwable实现序列化,所以RMI异常可以从服务器传到客户端。
然而一般情况,为了继承而设计的类应该尽可能少地去实现Serializable接口,用户接口也是。(但是上面情况下就是可以的,因为它们参与到需要持久化的框架中了)
如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。特别的是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。因此,对于为继承设计的不可序列化的类,你应该考虑提供一个无参构造器
如下增加了一个protected的无参构造器和一个初始方法。
|
|
注意init域是一个原子引用,确保特定情况时,对象的完整性。
|
|
以下是测试:
|
|
内部类不应该实现Serializable。它们使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值
内部类的默认序列化形式是定义不清楚的,然而静态成员类却可以实现Serializable接口。
总而言之,实现Serializable要小心。如果一个类为了继承而设计的,在允许/禁止 子类实现Serializable接口之间,提供一个可访问的无参构造器是可选折中方案。
第75条:考虑使用自定义的序列化形式
1.首先什么是 默认的序列化形式:
默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。
2.对一个对象来说,序列化形式包括它的逻辑数据和物理表示法
如果一个对象的物理表示法 等同于 它的逻辑内容,可能就适合默认序列化形式,但通常还提供一个readObject保证约束关系和安全性。
如下Name类,从逻辑上看 一个Name包含2个字符串。 同时,在物理表示上,Name类通过2个实例域精确的反映了它的逻辑内容。
|
|
如果一个对象的物理表示法 与 它的逻辑内容 有实质性的区别时,默认序列化就存在问题
如下StringList类,从逻辑上看 它表示一个字符串列表。 但是,在物理表示上,它表示一个双向链表。
此时,采用默认序列化形式,该序列化会镜像出链表中的所有项,以及这些项之间的双向链接。
|
|
所以,此时的默认序列化存在以下问题:
- 它使这个类导出API永远地束缚在该类的内部表示法上 private的Entry类变成公有API的一部分,对将来维护改变产生问题。
- 它会消耗过多的空间 如上的StringList一样,默认序列化形式过于庞大,不利于写到磁盘或者在网络中传输
- 它会消耗过多的时间 序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历
- 它会引起栈溢出 默认的序列化过程要对对象图执行一次递归遍历,很可能引起栈溢出
因此,解决方法包含writeObject和readObject
如下:transient表明这个实例域将从一个类的默认序列化形式中省略掉
虽然以下的所有域都是瞬时的(transient),但是不推荐省略defaultWriteObject()。如果以后版本加入了非瞬时的域,却没有defaultWriteObject(),就造成反序列化失败。
|
|
无论是否使用默认的序列化形式,当调用defaultWriteObject的时候,每一个未被标记为transient的实例域都会被序列化。因此要确定这些域是否是该对象逻辑状态的一部分。
如果使用默认序列化形式,被标记transient的实例,在反序列化的时候,这些域将被初始化为它们的默认值(Java类型的default value)
如果在读取读取整个对象状态的任何其他方法上强制任何同步,则必须在对象的序列化上强制这种同步。
不管选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显示的序列版本UID,如:
|
|
总而言之,当将一个类序列化的时候,要仔细考虑采用什么样的序列化形式
第76条:保护性地编写readObject方法
正如第39条:必要时进行保护性拷贝。
- readObject方法实际上相当于另一个公有的构造器,同其他构造器一样,构造器必须检查其参数的有效性,必要时进行保护性拷贝。
- readObject是一个用字节流作为唯一参数的构造器,正常情况下,对一个正常构造的实例进行序列化可以产生字节流。
当一个对象被反序列化的时候,对于客户端不应该拥有对象的引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝。
对于非final的可序列化的类,readObject方法不可以调用可被覆盖的方法
,因为被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能失败。
总而言之,对于编写readObject的时候,设想成正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例 以下是编写readObject的指导方针:
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
- 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
- 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。
第77条:对于实例控制,枚举类型优先于readResolve
对于一个Singleton的类,如果继承了Serializable,它就不再是一个Singleton。因为任何一个readObject方法,不管显示还是默认的,都会返回一个新建的实例。这个新建的实例不同于该类初始化时创建的实例。
readResolve:反序列化之后,新建对象上的readResolve方法就会被调用。然后该方法返回的对象引用将被返回 取代新建的对象 如下返回一个单例:
|
|
注意:如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient,否则攻击者就可以在readResolve方法被运行之前,反序列化出实例域。当对象引用域的内容被反序列化时,它就允许一个"盗用流"指向最初被反序列化的Singleton的引用。此时,反序列化出的Singleton就不再是之前的Singleton
。
自从Java1.5以后,建议使用enum来实现Singleton。枚举实现了序列化,详见
注意使用枚举反序列化出来的Singleton,和原来的Singleton是完全一致的。它们的hashCode是一样的
总而言之,尽可能使用枚举实施实例控制的约束条件。如果做不到,则提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient。
第78条:考虑用序列化代理代替序列化实例
序列化代理:为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个类称作序列化代理。
它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝 如下:
|
|
主要注意:writeReplace和readResolve这2个方法的作用
当你发现自己必须在一个不能被客户端扩展的类上编写readObject或writeObject时候。或者想要将带有重要约束条件的对象序列化(如Date),就可以采用序列化代理模式