74,谨慎地实现Serializable接口
- 实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。
如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。
如果你没有声明一个显示的序列版本UID,兼容性将会遭到破坏,在运行时导致invalidClassExeception。
- 实现Serializable接口的第二个代价是,它增加了出现bug和安全漏洞的可能性。
反序列化机制是一个“隐藏的构造器”,具备与其他构造器共同的特点。反序列化的过程必须要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。
- 实现Serializable接口的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。
为了继承而设计的类应该尽可能少的去实现Serializable接口,用户的接口也应该尽可能少的继承Serializable接口。如果一个类或者接口存在的目的主要是为了参与到某个框架中,改框架要求所有的参与者都必须实现Serializable接口,那么实现Serializable接口就非常有意义。
如果超类没有提供可供访问的无参构造器,子类也不可能做到可序列化。因此,对于为了继承而设计的不可序列化的类,你应该考虑提供一个午餐构造器。
75,考虑使用自定义的序列化形式
如果这个类实现了Serializable接口,并且使用了默认的序列化形式,你就永远无法彻底摆脱那个应该丢弃的实现了。它将永远牵制住这个类的序列化形式。
如果没有先认真考虑默认的序列化形式是否适合,则不要贸然接受。一般来将,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认序列化形式。
默认的序列化形式描述了改对象内部所包含的数据,以及每一个可以从这个对象到达其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含改对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
即使你确定了默认的序列化形式是适合的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法与它的逻辑内容有实质性区别时,使用默认序列化形式会有以下四个缺点:
- 它是这个类的导出API永远地束缚在该类的内部类表示法上。
- 它会消耗过多的空间。例如实现细节不需要记录在序列化中。
- 它会消耗过多的时间。序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历过程。
- 它会引起栈溢出。默认的序列化过程要对对象执行一次递归遍历,即使对中等规模的对象图,这样的操作也可能引起栈溢出。
如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做。即使所有的实例域都是transient的,调用defaultWriteObject也会影响该类的序列化形式,从而极大地增强灵活性。这样得到的序列化形式允许再以后的发行版本中增加非transient的实例域,并且还能保持向前或者向后兼容性。
无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用时,每一个未被标记为transient的实例域都会被序列化。在决定一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。
如果在读取整个对象状态的其他任何方法上强制任何同步,则也必须在对象序列化上强制这种同步。
不管你选择了哪种序列化形式,都要为自己编写的每个可序列化类声明一个显示的序列版本UID。
76,保护性地编写readObject方法
当一个对象被反序列化的时候,对于客户端不应该有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这非常重要的。私有可变组件一定进行保护性拷贝。
注意,保护性拷贝是在有效性检查之前进行的。
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象,不可变类的可变组件就属于这一类别。
- 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
- 如果整个对象被反序列化之后必须进行验证,就改使用ObjectInputValidation接口。
- 无论是直接还是间接方式,都不要调用类中任何可被覆盖的方法。
77,对于实例控制,枚举类型优先于readResolve
readResolve特性允许你用readObject创建的实例代替另一个实例。
事实上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例类型则都必须声明为transient的。
如果把readResolve方法放在一个final类上,它就应该是私有的。如果把它放在非final类上,就必须认真考虑它的可访问性。如果是私有的就不适用于任何子类。如果它是包级私有的,就只适用于同一个包中的子类。如果是受保护或共有的,就适用于所有没有覆盖它的子类,如果此时子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样可能导致ClassCastException异常。
78,考虑用序列化代理代替序列化实例
首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称为序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式。
用writeReplace添加到外围类。通过序列化代理,代替外围类的实例。为了防止攻击者伪造,只要在外围类中添加readObject方法抛出异常。最后,嵌套类中提供一个readResolve方法,它返回一个逻辑上相当于外围类的实例。
序列化代理模式有两个局限性。它不能与可以被客户端扩展的类兼容。它不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有他的序列化代理。
最后,序列化代理模式所增强的功能和安全性并不是没有代价的。
总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject时,就应该考虑使用序列化代理模式。