- Effective Java 读书笔记十序列化
- 谨慎地实现 Serializable 接口
- 考虑使用自定义的序列化形式
- 保护性地编写 readObject 方法
- 对于实例控制枚举类型优先于 readResolve
- 考虑用序列化代理代替序列化实例
- 更多资料
Effective Java 读书笔记(十):序列化
谨慎地实现 Serializable 接口
实现 Serializable 接口是一个很严肃的承诺。
- 实现 Serializable 接口付出的最大代价是,一旦这个类被发布,就大大降低了“改变这个类的实现的灵活性“。
- 其字节流编码(私有和包级私有域)变成了导出的 API 的一部分,不符合“最低限度地访问域”的实践准则。
- 序列版本 UID 可以手动指定,但若没有指定,则会根据类名称、接口名称、所有公有和受保护的域进行计算。
- 增加了出现 bug 和安全漏洞的可能性。
- 反序列化机制是一个“隐藏的构造器”,使用默认的反序列化机制,很容易破坏“由真正的构造器建立起来的约束关系”。
- 随着类的新版本的发布,相关的测试负担也增加了,因为你必须确保二进制兼容性和语义兼容性。
- 为了继承而设计的类,应尽可能少地去实现 Serializable 接口。但是,最好提供一个无餐构造方法,允许子类去实现 Serializable 接口。
- 内部类不应该实现 Serializable 接口,因为其默认序列化形式是定义不清楚的。但是,静态成员类可以实现 Serializable 接口。
考虑使用自定义的序列化形式
- 如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化会有 4 个缺点:
- 它使这个类的导出 API 永远地束缚在该类的内部表示法上了。
- 消耗过多空间(导出了可以由其他字段推导出的字段)。
- 消耗过多时间(做了无用功)。
- 引起栈溢出(过深的递归遍历)。
- 即使你确定了使用默认序列化是合适的,通常还必须提供一个 readObject 方法以保证约束关系和安全性。
- 常见的逻辑内容和物理表示不同的有List、数组、Set,它们的逻辑内容都是“多个元素”,比如它们的 Json 序列化结果都是一样的。
通过定制 readObject 和 writeObject 可以定制序列化和反序列化。在方法中,先调用 defaultReadObject 或 defaultWriteObject 会有更好的兼容性(这两方法负责序列化和反序列化所有非 transient 实例域)。
讲一个域做成非 transient 时,一定要确保它的值时对象逻辑状态的一部分。
不管采用哪种序列化形式,都要为自己编写的每个可序列化类,声明一个显式的序列版本 UID(serial version UID)。这样可以避免 UID 称为潜在的不兼容根源,同时也减少了计算 UID 的性能开销。
保护性地编写 readObject 方法
readObject 相当于另一个公有构造器,如果构造方法里有做参数有效性检查,那么 readObject 里也要有。编写健壮 readObject 方法的指导方针如下:
- 对于对象引用域必须为私有的类,要保护性地拷贝这个域中的每个对象。不可变类的可变组件就属于这一类。
- 没有保护性拷贝,容易被客户端拿到内部私有对象的引用。
- 对于任何约束条件,如果检查失败,抛出一个 InvalidObjectException 异常。这些检查动作应该放在所有保护性拷贝之后。
- 不做检查,很容易通过伪造二进制数据流产生不合法的对象。
- 如果整个对象图在反序列化后必须进行验证,就应该使用 ObjectInputValidation 接口。
- 无论是直接方式,还是间接方式,都不要调用类中任何可被覆盖的方法。
对于实例控制,枚举类型优先于 readResolve
- 尽可能使用枚举来实施实例控制。
- 否则,若需要序列化 + 实例控制,就需要提供一个 readResolve 方法,并且确保该类所有实例域都是基本类型或是 transient 类型的。
- 对于一个 final 类,其 readResolve 方法应该是私有的。
- 对于非 final 类,其 readResolve 方法应该是 public 或 protected 或 package,子类覆盖时也必须覆盖 readResolve 方法,否则反序列化时容易有 ClassCastException。
考虑用序列化代理代替序列化实例
与 readResolve 相对的是 writeReplace,writeReplace 可以在序列化时,替换要序列化的对象。
序列化代理,就是使用一个代理类来对实例进行序列号,替代类本身的序列化功能。
1. 优点是安全,反序列化实例也是采用构造函数或静态工厂方法等常用方法构建,不必单独确保反序列化的实例一定要遵守类的约束条件;
2. 缺点是性能稍差、不能与可被客户端扩展到类兼容。
具体代码示例,可以看序列化代理模式。
更多资料
- 序列化代理模式