86. 非常谨慎地实现 Serializable

      实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低更改该类实现的灵活性。 当类实现 Serializable 时,其字节流编码(或序列化形式)成为其导出 API 的一部分。一旦广泛分发了一个类,通常就需要永远支持序列化的形式,就像需要支持导出 API 的所有其他部分一样。如果你不努力设计自定义序列化形式,而只是接受默认形式,则序列化形式将永远绑定在类的原始内部实现上。换句话说,如果你接受默认的序列化形式,类中私有的包以及私有实例字段将成为其导出 API 的一部分,此时最小化字段作用域(详见第 15 条)作为信息隐藏的工具,将失去其有效性。

      如果你接受默认的序列化形式,然后更改了类的内部实现,则会导致与序列化形式不兼容。试图使用类的旧版本序列化实例,再使用新版本反序列化实例的客户端(反之亦然)程序将会失败。当然,可以在维护原始序列化形式的同时更改内部实现(使用 ObjectOutputStream.putFieldsObjectInputStream.readFields),但这可能会很困难,并在源代码中留下明显的缺陷。如果你选择使类可序列化,你应该仔细设计一个高质量的序列化形式,以便长期使用(详见第 87 和 90 条)。这样做会增加开发的初始成本,但是这样做是值得的。即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。

      可序列化会使类的演变受到限制,施加这种约束的一个简单示例涉及流的唯一标识符,通常称其为串行版本 UID。每个可序列化的类都有一个与之关联的唯一标识符。如果你没有通过声明一个名为 serialVersionUID 的静态 final long 字段来指定这个标识符,那么系统将在运行时对类应用加密散列函数(SHA-1)自动生成它。这个值受到类的名称、实现的接口及其大多数成员(包括编译器生成的合成成员)的影响。如果你更改了其中任何一项,例如,通过添加一个临时的方法,生成的序列版本 UID 就会更改。如果你未能声明序列版本 UID,兼容性将被破坏,从而在运行时导致 InvalidClassException

      实现 Serializable 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。 当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它,反之亦然。因此,所需的测试量与可序列化类的数量及版本的数量成正比,工作量可能很大。你必须确保「序列化-反序列化」过程成功,并确保它生成原始对象的无差错副本。如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少(详见第 87 和 90 条)。

      实现 Serializable 接口并不是一个轻松的决定。 如果一个类要参与一个框架,该框架依赖于 Java 序列化来进行对象传输或持久化,这对于类来说实现 接口就是非常重要的。此外,如果类 A 要成为另一个类 B 的一个组件,类 B 必须实现 Serializable 接口,若类 A 可序列化,它就会更易于被使用。然而,与实现 Serializable 相关的代价很多。每次设计一个类时,都要权衡利弊。历史上,像 BigIntegerInstant 这样的值类实现了 Serializable 接口,集合类也实现了 Serializable 接口。表示活动实体(如线程池)的类很少情况适合实现 Serializable 接口。

      为继承而设计的类(详见第 19 条)很少情况适合实现 Serializable 接口,接口也很少情况适合扩展它。 违反此规则会给扩展类或实现接口的任何人带来很大的负担。有时,违反规则是恰当的。例如,如果一个类或接口的存在主要是为了参与一个要求所有参与者都实现 Serializable 接口的框架,那么类或接口实现或扩展 Serializable 可能是有意义的。

      如果你实现了一个带有实例字段的类,它同时是可序列化和可扩展的,那么需要注意几个风险。如果实例字段值上有任何不变量,关键是要防止子类覆盖 finalize 方法,可以通过覆盖 finalize 并声明它为 final 来做到。最后,如果类的实例字段初始化为默认值(整数类型为 0,布尔值为 false,对象引用类型为 null),那么必须添加 readObjectNoData 方法:

      这个方法是在 Java 4 中添加的,涉及将可序列化超类添加到现有可序列化类 [Serialization, 3.5] 的特殊情况。

      关于不实现 Serializable 的决定,有一个警告。如果为继承而设计的类不可序列化,则可能需要额外的工作来编写可序列化的子类。子类的常规反序列化,要求超类具有可访问的无参数构造函数 [Serialization, 1.10]。如果不提供这样的构造函数,子类将被迫使用序列化代理模式(详见第 90 条)。