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.putFields和ObjectInputStream.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. java设计模式之-观察者模式(发布-订阅模式)

    1.观察者模式定义  观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象. 这个主题对象在状态上发生变化时,会通知所有观察者对象,让它们能够自动更新自己. 2.观察者模式结构 ...

  2. Alpha(9/10)

    鐵鍋燉腯鱻 项目:小鱼记账 团队成员 项目燃尽图 冲刺情况描述 站立式会议照片 各成员情况 团队成员 学号 姓名 git地址 博客地址 031602240 许郁杨 (组长) https://githu ...

  3. CodeForces round 967 div2 题解(A~E)

    本来准备比完赛就写题解的, 但是一拖拖了一星期, 唉 最后一题没搞懂怎么做,恳请大神指教 欢迎大家在评论区提问. A Mind the Gap 稳定版题面 https://cn.vjudge.net/ ...

  4. [Web]flask-excel实现excel文件下载的前后端实现

    之前同事写了前端表格导出的功能, 前后端逻辑没有梳理, 导致后端代码十分臃肿. 接手之后, 重新选择了前端table插件, 从jqxGrid变更为bootstrapTable. 本来想依赖集成的tab ...

  5. js中使用将json数组 转换为json将一个包含对象的数组用以创建一个包含新重新格式化对象的新数组

    1.使用reduce: let arr = [{ "code": "badge", "priceList": [{ "amount ...

  6. [ZJOI2012]波浪

    Description: L = | P2 – P1 | + | P3 – P2 | + - + | PN – PN-1 | 给你一个N和M,问:随机一个1-N的排列,它的波动强度(L)不小于M的概率 ...

  7. leetCode中老出现的针对一个int中每个数字的处理

    一个int比如322,我想找happy number就得3平方加2平方再加2平方,怎样找到一个一个的数字,就是322%10,得到2,然后/10,然后再%,就可以依次求得每位上的数字 happy num ...

  8. BZOJ4249 : Walls 防壁

    首先可以将攻击位置整理成折线,答案不变. 对于一个长度为$k$的询问,若折线不超过两段,那么显然可以暴力贪心求解. 否则考虑折线中最短的一段$x\rightarrow y$,若其长度$\leq k$: ...

  9. BZOJ3737 : [Pa2013]Euler

    首先枚举$n$的每个约数$d$,检查一下$d+1$是否是质数,这些数都有可能作为答案的质因子出现. 考虑爆搜,每次枚举下一个要在答案中出现的质因子$p$,将$n$除以$p-1$,再枚举$p$的指数,然 ...

  10. 用单进程、多线程并发、多线程分别实现爬一个或多个网站的所有链接,用浏览器打开所有链接并保存截图 python

    #coding=utf-8import requestsimport re,os,time,ConfigParserfrom selenium import webdriverfrom multipr ...