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. gradle3.0新命令

    摘抄原文https://mp.weixin.qq.com/s/6UZhaI9cILJiPGYHkXd73g No1: Implementation compile 指令被标注为过时方法,而新增了两个依 ...

  2. 一、利用Python编写飞机大战游戏-面向对象设计思想

    相信大家看到过网上很多关于飞机大战的项目,但是对其中的模块方法,以及使用和游戏工作原理都不了解,看的也是一脸懵逼,根本看不下去.下面我做个详细讲解,在做此游戏需要用到pygame模块,所以这一章先进行 ...

  3. Java数组实现循环队列的两种方法

    用java实现循环队列的方法: 1.添加一个属性size用来记录眼下的元素个数. 目的是当head=rear的时候.通过size=0还是size=数组长度.来区分队列为空,或者队列已满. 2.数组中仅 ...

  4. docker 启动MySQL

    Docker启动mysql的坑2   正确启动mysql: docker run -p 3306:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456 - ...

  5. 001.Redis简介及安装

    一 Redis简介 1.1 Redis 简介 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库. Redis 与其他 key-value 缓存产品有以下三个特点: ...

  6. 你有所不知的<script>元素

    向html页面中插入javascript的主要方法,就是使用<script>元素. <script>定义了下列6个属性: async:可选.表示应该立即下载脚本,但不应妨碍页面 ...

  7. [蓝点ZigBee] Zstack 之点亮LED灯 ZigBee/CC2530 视频资料

    LED点灯实验,主要是依据Zstack 现有程序修改LED相关驱动,适配到我们自己的开发板上,我们开发板共有4个LED灯,其中一个是电源指示灯,剩余3个都是GPIO控制的LED灯,有LED 灯连接方式 ...

  8. 快速学习MarkDown语法及MarkDown拓展语法

    使用Markdown编辑器写博客 前半部分为效果后半部分为markdown格式,推荐开起两个窗口对比阅读 Markdown和扩展Markdown简洁的语法 代码块高亮 图片链接和图片上传 LaTex数 ...

  9. 04 树莓派截图软件scrot的安装和使用

    2017-08-22 13:52:52 sudo apt-get install scrot 捕捉活动窗口(按下回车后,3秒之内点击要捕捉的窗口): scrot -d 3 -u 捕捉选定的区域(按下回 ...

  10. html标题-段落-字符实体-换行

    Html标题标签: <h1>.<h2>.<h3>.<h4>.<h5>.<h6>标签可以在网页上定义6种级别的标题,这6种级别的标 ...