ITEM 86: IMPLEMENT SERIALIZABLE WITH GREAT CAUTION
允许序列化类的实例就像在其声明中添加单词 implements Serializable 一样简单。因为这很容易做到,所以有一种常见的误解,认为序列化只需要程序员付出很少的努力。事实要复杂得多。虽然使类可序列化的直接成本可以忽略不计,但长期的成本通常是巨大的。
实现 Serializable 的一个主要代价是,它降低了在类实现发布后更改它的灵活性。当一个类实现 Serializable 时,它的字节流编码(或序列化形式)将成为它导出的 API 的一部分。一旦广泛分发了一个类,通常就需要永远支持序列化的形式,就像需要支持导出的 API 的所有其他部分一样。如果您不努力设计定制的序列化表单,而只是接受默认值,那么序列化的表单将永远与类的原始内部表示绑定在一起。换句话说,如果您接受默认的序列化形式,则类的私有和包私有实例字段将成为其导出 API 的一部分,并且最小化对字段的访问(item 15)的实践将失去作为信息隐藏工具的效力。
如果您接受默认的序列化形式,然后更改类的内部表示,则会导致序列化形式中不兼容的更改。尝试使用旧版本的类序列化实例并使用新版本反序列化实例(反之亦然)的客户机将遇到程序失败。可以在保持原始序列化形式的同时更改内部表示(使用ObjectOutputStream)。但是这样做很困难,并且会在源代码中留下明显的缺陷。如果您使一个类可序列化,您应该仔细设计一个您愿意长期使用的高质量的序列化表单(item 87、item 90)。这样做会增加开发的初始成本,但这是值得的。即使是设计良好的序列化形式也会限制类的演化;设计不良的序列化表单可能会造成严重后果。
串行性对演化施加的约束的一个简单示例涉及流惟一标识符,通常称为串行版本uid。每个可序列化的类都有一个与之关联的唯一标识号。如果您没有通过声明一个名为 serialVersionUID 的静态 final long 字段来指定这个数字,系统将在运行时通过对类的结构应用一个加密哈希函数(SHA-1)来自动生成它。这个值受类的名称、它实现的接口和它的大多数成员(包括编译器生成的合成成员)的影响。如果您更改这些内容中的任何一个,例如,通过添加一个方便的方法,生成的串行版本 UID 就会更改。如果您没有声明一个串行版本的 UID,兼容性将被破坏,导致在运行时出现InvalidClassException 异常。
实现 Serializable 的第二个代价是,它增加了出现错误和安全漏洞的可能性(item 85)。通常,对象是用构造函数创建的;序列化是一种语言之外的创建对象的机制。无论您是接受默认行为还是覆盖它,反序列化都是一个“隐藏的构造函数”,具有与其他构造函数相同的所有问题。因为没有与反序列化相关联的显式构造函数,所以很容易忘记必须确保它确保构造函数建立的所有不变量,并且不允许攻击者访问正在构造的对象的内部。依赖于默认的反序列化机制可以很容易地让对象对不变破坏和非法访问开放(item 88)。
实现 Serializable 的第三个代价是,它增加了与发布类的新版本相关的测试负担。当修改可序列化的类时,一定要检查是否可以在新版本中序列化实例,在旧版本中反序列化实例,反之亦然。因此,所需的测试量与可序列化类的数量和发布的数量成比例,发布的数量可能很大。您必须确保序列化-反序列化过程成功,并确保它产生原始对象的忠实副本。如果在第一次编写类时仔细设计定制的序列化形式,那么测试的需求就会减少(item 87、item 90)。
实现 Serializable 并不是一个可以轻易做出的决定。如果一个类要参与一个依赖 Java序列化来进行对象传输或持久化的框架,那么它是非常重要的。而且,它极大地简化了将一个类作为另一个必须实现 Serializable 的类中的组件的使用。然而,实现Serializable 有很多成本。每次设计一个类时,都要权衡利弊。从历史上看,像、Biginteger 和 Instant 这样的值类实现了 Serializable, Collection类也是这样。表示活动实体的类,比如线程池,应该很少实现 Serializable。
为继承而设计的类(item 19)应该很少实现 Serializable,接口也应该很少扩展它。违反这个规则会给任何扩展类或实现接口的人带来沉重的负担。有时候违反规则是合适的。例如,如果一个类或接口的存在主要是为了参与一个要求所有参与者实现Serializable 的框架,那么这个类或接口实现或扩展 Serializable 就很有意义了。
为实现 Serializable 而设计的继承类包括 Throwable 和 Component.Throwable,使得 RMI 可以将异常从服务器发送到客户端。组件实现了 Serializable,这样 GUI 就可以发送、保存和恢复,但是即使在 Swing 和 AWT 的鼎盛时期,这种功能在实践中也很少使用。
如果您实现的类具有既可序列化又可扩展的实例字段,那么需要注意几个风险。
如果实例字段值上有任何不变量,关键是要防止子类覆盖 finalize 方法,该类可以通过覆盖 finalize 并将其声明为 final 来实现这一点。否则,该类将容易终结器(8)项攻击。最后,如果类存在不变量必须是violated, 且字段初始化为默认值(0, false, nul), 您必须添加这个 readObjectNoData 方法:
// readObjectNoData for stateful extendable serializable classes
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("Stream data required");
}
这个方法是在 Java 4 中添加的,用于处理将可序列化超类添加到现有的可序列化类中的极端情况[Serialization, 3.5]。
关于不实现 Serializable 的决定,需要注意一点。如果一个为继承而设计的类不是可序列化的,那么它可能需要额外的努力来编写可序列化的子类。此类类的常规反序列化要求超类具有可访问的无参数构造函数[Serialization, 1.10]。如果不提供这样的构造函数,子类将被迫使用序列化代理模式(item 90)。
内部类(item 24)不应该实现Serializable。它们使用编译器生成的合成字段来存储对封闭实例的引用,并存储来自封闭范围的本地变量的值。这些字段如何与类定义对应是未指定的,匿名类和本地类的名称也是未指定的。因此,内部类的默认序列化形式是未定义的。但是,静态成员类可以实现 Serializable。
总而言之,实现 Serializable 的方便性似乎不太好。除非类只在一个受保护的环境中使用,在这种环境中,版本永远不需要互操作,服务器永远不会暴露给不可信的数据,否则实现 Serializable 是一个需要非常小心的严肃承诺。如果类允许继承,则需要格外小心。
网友评论