Tips

书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code

注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

86. 非常谨慎地实现SERIALIZABLE接口

允许对类的实例进行序列化可以非常简单,只需将implements Serializable添加到类的声明中即可。因为这很容易做到,所以有一个普遍的误解,认为序列化只需要程序员付出很少的努力。事实要复杂得多。虽然使类可序列化的即时成本可以忽略不计,但长期成本通常是巨大的。

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

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

限制类的序列化演变的一个简单示例涉及到流的唯一标识符(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。

为继承而设计的类(条目 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是一项严肃的承诺,应该非常谨慎。如果类允许继承,则需要更加格外小心。

Effective Java 第三版—— 86. 非常谨慎地实现SERIALIZABLE接口的更多相关文章

  1. Effective Java 第三版——44. 优先使用标准的函数式接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  2. Effective Java 第三版——67. 明智谨慎地进行优化

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  3. Effective Java 第三版——66. 明智谨慎地使用本地方法

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  4. Effective Java 第三版——83. 明智谨慎地使用延迟初始化

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  5. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  6. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  7. Effective Java 第三版——13. 谨慎地重写 clone 方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——48. 谨慎使用流并行

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——19. 如果使用继承则设计,并文档说明,否则不该使用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. js和jquery获取当前元素的内容

    html代码 <div>测试文本</div>js:div.innerHTMLjQuery:div.html()

  2. IO流关键字

  3. 在Qt中调用Mupdf库进行pdf显示

    2018.5.10 更新内存对齐说明 感谢知乎网友@孤独子狮 指出QImage处需要考虑内存对齐的问题.因为本人缺乏跨平台.图形库开发经验,所以在调试成功后就没有深入探究. 主要修改了QImage的构 ...

  4. BZOJ.5286.[AHOI/HNOI2018]转盘(线段树)

    BZOJ LOJ 洛谷 如果从\(1\)开始,把每个时间\(t_i\)减去\(i\),答案取决于\(\max\{t_i-i\}\).记取得最大值的位置是\(p\),答案是\(t_p+1+n-1-p=\ ...

  5. Android:PowerManager类 电源管理

    PowerManager类用于对设备电源状态进行管理:PowerManager.WakeLock类用于保持设备常亮:Android中通过各种Lock锁对电源进行控制,需要注意的是加锁和解锁必须成对出现 ...

  6. 【DWM1000】 code 解密6一TAG 状态机第一步

    我们前面分析过,不论ANCHOR 还是TAG,前面变量的初始化基本都是一样的,只是状态机必须明确区分不同的设备类型.我们从开始看TAG.由于初始化TAG的 testAppState一样初始化为TA_I ...

  7. 树形动态规划(树形DP)入门问题—初探 & 训练

    树形DP入门 poj 2342 Anniversary party   先来个题入门一下~ 题意: 某公司要举办一次晚会,但是为了使得晚会的气氛更加活跃,每个参加晚会的人都不希望在晚会中见到他的直接上 ...

  8. BZOJ4422 : [Cerc2015]Cow Confinement

    从右往左扫描线,用线段树维护扫描线上每一个点能达到的花的数量,并支持最近篱笆的查询. 对于一朵花,找到它上方最近的篱笆,那么它对这中间的每头牛的贡献都是$1$. 当扫到一个篱笆的右边界时,这中间的答案 ...

  9. X-factor Chains [POJ3421] [素数]

    Description    给定一个正整数X, 一个长度为m的X-因子链是由m+1个整数组成的.其中    1 = X0, X1, X2, …, Xm = X 满足Xi < Xi+1 且 Xi ...

  10. 2017.08.15【NOIP提高组】模拟赛B组

    Summary 今天比赛很差很差,掉到谷底.第一题快排打错了,漏了递归,变成一个while循环.最后一题k忘记减一,答案一直是无穷大,所以没交.第三题没时间调DP就打了个递归,第二题状态想歪了.四道题 ...