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