前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >效率编程 之「序列化」

效率编程 之「序列化」

作者头像
CG国斌
发布2019-05-26 14:48:53
4010
发布2019-05-26 14:48:53
举报
文章被收录于专栏:维C果糖维C果糖

对象序列化提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。“将一个对象编码成一个字节流”,称作将该对象序列化;相反的处理过程称为反序列化。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时使用。序列化技术为远程通信提供了标准的线路级对象表示法,也为 JavaBean 组件结构提供了标准的持久化数据格式。

第 1 条:谨慎地实现Serializable接口

想要使一个类的实例可被序列化,非常简单,只要在它的声明中加入implements Serializable字样即可。正因为太容易了,所以普遍存在这样一种误解,认为程序员毫不费力就可以实现序列化。实际上情形要复杂的多。虽然使一个类可被序列化的直接开销非常低,甚至可以忽略不计,但是为了序列化而付出的长期开销往往是实实在在的。

  • 实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。序列化会使类的演变收到限制,这种限制的一个例子与流的唯一标识号有关,通常它也被称为序列版本 UID(serial version UID)。每个可序列化的类都有一个唯一标识号与它相关联。如果我们没有在一个名为serialVersionUID的私有静态finallong域中显式地指定该标识号,系统就会自动地根据这个类来调用一个复杂的运算过程,从而在运行时产生该标识号。这个自动产生的值会受到类名称、它所实现的接口的名称、以及所有公有的和受保护的名称所影响。如果我们通过任何方式改变了这些信息,比如,增加了一个不是很重要的工具方法,自动产生的序列版本 UID 也会发生变化。因此,如果我们没有声明一个显式的序列版本 UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
  • 实现Serializable接口的第二个代价是,它增加了出现 Bug 和安全漏洞的可能性。通常情况下,对象是利用构造器来创建的;序列化机制是一种语言之外的对象创建机制。无论我们是接受了默认的行为,还是覆盖了默认的行为,反序列化机制都是一个“隐藏的构造器”,具备与其他构造器相同的特点。因为反序列化机制中没有显式的构造器,所以我们很容易忘记要确保:反序列化过程中也要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问。
  • 实现Serializable接口的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。当一个可序列化的类被修订的时候,很重要的一点,要检查是否可以“在新版本中序列化一个实例,然后在旧版本中反序列化”,反之亦然。因此,测试所需的工作量和“可序列化的类的数量和发行版本号”的乘积成正比,这个乘积可能会非常大。这些测试不可能自动构造,因为除了二进制兼容以外,我们还必须测试语义兼容性。换句话说,我们必须既要确保“序列化–反序列化”过程成功,也要确保结果产生的对象真正是原始对象的复制品。可序列化类的变化越大,它就越需要测试。如果在最初编写一个类的时候,就精心设计了自定义的序列化形式,测试的要求就可以有所降低,但是也不能完全没有测试。

实现Serializable接口并不是一个很轻松就可以做出的决定。为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。在为了继承而设计的类中,真正实现了Serializable接口的有Throwable类、ComponentHttpServlet客户端。因为Throwable类实现了Serializable接口,所以 RMI 的异常可以从服务器传到客户端;Component实现了Serializable接口,因此 GUI 可以被发送、保存和恢复;HttpServlet实现了Serializable接口,因此会话状态可以被缓存。

对于一个为了继承而设计的类,在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折衷方案是,提供一个可供访问的无参构造器。这种设计允许(但不要求)子类实现Serializable接口。此外,内部类不应该实现Serializable接口,因为内部类的默认序列化形式是定义不清楚的。

第 2 条:考虑使用自定义的序列化形式

如果没有先认真考虑默认的序列化形式是否合适,就不要贸然接受默认的序列化形式。如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。但是,即使我们确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下 4 个缺点:

  • 它是这个类的导出 API 永远地束缚在该类的内部表示法上;
  • 它会消耗过多的空间;
  • 它会消耗过多的时间;
  • 它会引起栈溢出。

在 Java 中,transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉,但writeObject方法的首要任务仍是调用defaultWriteObjectreadObject方法的首要任务则是调用defaultReadObject。如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObjectdefaultReadObject也是允许的,但是不推荐这样做。

无论我们是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化,在决定将一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。无论我们是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。因此,如果有一个线程安全的对象,它通过同步每个方法来实现了它的线程安全,并且我们选择使用默认的序列化形式,就要使用下列的writeObject方法:

代码语言:javascript
复制
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
    s.defaultWriteObject();
}

无论我们是否使用默认的序列化形式,都要为自己编写的每个可序列化的类声明一个现实的序列版本 UID。这样可以避免序列版本 UID 成为潜在的不兼容根源,而且这样做也会带来小小的性能好处。如果没有通过显式的序列版本 UID,就需要在运行时通过一个高开销的计算过程产生一个序列版本 UID。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年06月23日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第 1 条:谨慎地实现Serializable接口
  • 第 2 条:考虑使用自定义的序列化形式
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档