本章关注对象序列化API,它提供了一个框架,用来将对象编码成字节流,并从字节流中重新构建对象。“将对象编码成字节流”被称作对象序列化,相反的处理过程被称作反序列化。序列化技术为远程通信提供了标准的线路级对象表示法,也为JavaBeans组件结构提供了标准的持久化数据格式。
?
实现Serializable接口而付出的最大代价是,一旦一个类发布,就大大降低了“改变这个类实现”的灵活性。序列化会使类的演变受到限制,这与流的唯一标识符有关,通常它也被称为序列版本UID。每个可序列化的类都有一个唯一标识符与它相关联。如果你没有显示指定该标志号,系统就会自动根据这个类的名称,所实现接口的名称等计算出一个标志号。如果你改变了类,比如增加了一个工具方法,自动产生的UID会产生变化。因此,如果你没有显示声明UID,兼容性将遭到破坏。
第二个代价是,它增加了出现Bug和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制。反序列化是一个隐藏的构造器,具备和其他构造器相同的特点。所以要确保:反序列化过程必须要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭到非法访问。
第三个代价,随着类发行新的版本,相关的测试负担也增加了。
根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合也应如此。代表活动实体的类比如线程池,一般不实现。
?
第七十五条:考虑使用自定义的序列化形式
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。即使你确认了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法和逻辑数据内容有实质性的区别时,使用默认序列化形式会有下面四个缺点:
1.它使这个类的导出API永远的束缚在该类的内部表示法上。
2.消耗过多的空间。
3.消耗过多的时间。
4.引起栈溢出。
自定义序列化形式通常自定义writeObject和readObject方法,用来序列化和反序列化。同时,使用transient修饰符使实例域从一个类的默认序列化形式中省略掉。
?
第七十六条:保护性的编写readObject方法
不严格地说,readObject是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时,readObject产生的对象可能会违反所属的类的约束条件。
为了解决这个问题,可以为实现默认序列化的类添加一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后对象的有效性。但这样仍可能解决不了问题,攻击者可以使用对象引用来修改实例的内部组件。当一个对象被反序列化的时候,对于客户端不应该拥有的对象应用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝。并且,保护性拷贝是在有效性检查之前进行。
下面的建议有利于编写更加健壮的readObject方法:
1.对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
2.对于任何约束条件,如果检查失败,抛出InvalidObjectException异常。这些检查跟在所有保护性拷贝之后。
3.如果整个对象在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
4.不要调用类中可能被覆盖的方法。
?
第七十七条:对于实例控制,枚举类型优先于readResolve
readResolve特性允许你用readObject创建的实例代替另一个实例。对于一个正在被反序列化的对象,如果他的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后该方法返回的对象引用被返回,取代新建的对象。
尽可能使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型或者是transient的。
?
第七十八条:考虑用序列化代理代替序列化实例
序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:他不需要进行任何一致性检查或者保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明Serializable接口。
?