Effective Java 第三版—— 86. 很是谨慎地实现SERIALIZABLE接口

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java

Effective Java, Third Edition

86. 很是谨慎地实现SERIALIZABLE接口

容许对类的实例进行序列化能够很是简单,只需将implements Serializable添加到类的声明中便可。由于这很容易作到,因此有一个广泛的误解,认为序列化只须要程序员付出不多的努力。事实要复杂得多。虽然使类可序列化的即时成本能够忽略不计,但长期成本一般是巨大的。git

实现Serializable的一个主要成本是,一旦类的实现被发布,会下降更改该类实现的灵活性。当类实现Serializable时,其字节流编码(或序列化形式)成为其导出API的一部分。一旦这个类被普遍分发后,一般就须要永远支持序列化形式,就像须要支持导出API的全部其余部分同样。若是不努力设计自定义序列化形式(custom serialized form),而只是接受默认值,则序列化形式将永远绑定到类的原始内部表示上。换句话说,若是接受默认的序列化形式,类的私有和包级私有实例属性将成为其导出API的一部分,而且最小化属性访问的实践(条目 15)也失去其做为信息隐藏工具的有效性。程序员

若是接受默认的序列化形式,往后更改类的内部表示,则会致使序列化形式中的不兼容更改。 尝试使用旧版本的类序列化实例并使用新版本对其进行反序列化(反之亦然)的客户端将遇到程序失败。 能够在保持原始序列化形式(使用ObjectOutputStream.putFieldsObjectInputStream.readFields)的同时更改内部表示,但这可能很困难而且在源代码中留下可见的缺陷。 若是选择将类序列化,应该仔细设计一个愿意长期使用的高质量序列化形式(条目 87,90)。 这样作会增长开发的初始成本,但值得付出努力。 即便是精心设计的序列化形式也会限制一个类的演变; 一个设计不良的序列化形式多是后果严重的。github

限制类的序列化演变的一个简单示例涉及到流的惟一标识符(stream unique identifiers),一般称为序列版本UID(serial version UIDs)。 每一个可序列化的类都有一个与之关联的惟一标识号。 若是未经过声明名为serialVersionUID的静态fianl的long类型的来指定此数字,则系统会在运行时经过加密哈希函数(SHA-1)根据类的结构来自动生成它。 此值受类的名称,它实现的接口及其大多数成员(包括编译器生成的组合成(synthetic members)员)的影响。 若是更改任何这些内容,例如,经过添加一个便捷的方法,生成的序列版本UID就会更改。 若是未能声明序列版本UID,则兼容性将被破坏,从而致使运行时出现InvalidClassException异常。安全

实现Serializable的第二个成本是它增长了错误和安全漏洞的可能性(条目 85)。 一般,使用构造方法建立对象; 序列化是一种语言以外的建立对象的机制。 不管接受默认行为仍是重写默认行为,反序列化都是一个“隐藏的构造方法”,与其余构造方法具备相同的问题。 由于没有与反序列化相关联的显式构造方法,因此很容易忘记必须确保它保证构造方法创建的全部不变性,而且它不容许攻击者访问构造中的对象的内部。 依赖于默认的反序列化机制,能够轻松地将对象置于不变性破坏和非法访问以外(第88项)。服务器

实现Serializable的第三个成本是它增长了与发布新版本类相关的测试负担。 修改可序列化类时,重要的是检查是否能够序列化新版本中的实例能够在旧版本中反序列化,反之亦然。 所以,所需的测试量与可序列化类的数量和可能很大的发布数量的乘积成比。 必须确保“序列化——反序列化”过程成功,并确保它生成原始对象的忠实副本。 若是在首次编写类时仔细设计自定义序列化形式,那么测试的需求就会减小(条目 87,90)。框架

实现Serializable并非一个轻松的决定。若是一个类要参与依赖于Java序列化来进行对象传输或持久性的框架,那么这一点是很是重要的。此外,它还极大地简化了将类做为必须实现Serializable的另外一个类中的组件的使用。然而,与实现Serializable相关的成本不少。每次设计一个类时,都要权衡利弊。历史上,像BigInteger和Instant这样的值类实现了序列化,集合类也实现了Serializable。表示活动实体(如线程池)的类不多实现Serializable。ide

为继承而设计的类(条目 19)应该不多实现Serializable接口,接口也不多去继承它。 违反此规则会给继承类或实现接口的任何人带来沉重的负担。可是 有时候违反规则是合适的。 例如,若是一个类或接口主要存在于要求全部参与者实现Serializable的框架中,对类或接口来讲,实现或继承Serializable是有意义的。函数

专为实现Serializable的继承而设计的类包括Throwable和Component。 Throwable实现Serializable,所以RMI能够从服务器向客户端发送异常。 Component实现Serializable,所以能够发送,保存和恢复GUI,但即便在Swing和AWT的全盛时期,这种机制在实践中不多使用。工具

若是实现了具备可序列化和可扩展的实例属性的类,则须要注意几个风险。若是实例属性的值上有任何不变行,关键是要防止子类重写finalize方法,该类能够经过重写finalize方法并声明它为final来实现这一点。不然,该类将容易受到终结器攻击(finalizer attacks)(条目 8)。最后,若是类的实例属性初始化为其默认值(整数类型为零,布尔值为false,对象引用类型为null),则会违反不变性,必须添加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]。 若是不提供这样的构造方法,则子类被迫使用序列化代理模式(serialization proxy pattern)(条目 90)。

内部类(条目 24)不该实现Serializable。 它们使用编译器生成的合成属性(synthetic fields)来保持对外围实例(enclosing instances)的引用,还保存来自外围做用范围的局部变量的值。这些属性与类定义的对应关系,以及匿名类和本地类的名称都是未指定的。 所以,内部类的默认序列化形式是不明确的。 可是,静态成员类能够实现Serializable。

总而言之,不要认为实现Serializable是简单的事情。除非类只在受保护的环境中使用,在这种环境中,版本永远没必要相互操做,服务器永远不会暴露于不受信任的数据,不然实现Serializable是一项严肃的承诺,应该很是谨慎。若是类容许继承,则须要更加格外当心。

相关文章
相关标签/搜索