当前位置: 首页 > news >正文

HashSet序列化问题

转自:http://www.examda.com/JAVA/Instructs/060617/090719996.html

这个程序创建了一个对象并且 检查 它是否遵从某个类的不变规则(invariant)。然后该程序序列化这个对象,之后将其反序列化,然后再次检查反序列化得到的副本是否也遵从这个规则。它会遵从这个规则吗?如果不是的话,又是为什么呢?import java.util.*;
import java.io.*;

public class SerialKiller {
  public static void main(String[] args) {
    Sub sub = new Sub(666); 
    sub.checkInvariant();

    Sub copy = (Sub) deepCopy(sub);
    copy.checkInvariant();
  }

  // Copies its argument via serialization (See Puzzle 80)
  static public Object deepCopy(Object obj) {
    try {
      ByteArrayOutputStream bos = 
        new ByteArrayOutputStream();
      new ObjectOutputStream(bos).writeObject(obj);
      ByteArrayInputStream bin =
        new ByteArrayInputStream(bos.toByteArray());
      return new ObjectInputStream(bin).readObject(); 
    } catch(Exception e) {
      throw new IllegalArgumentException(e); 
    }
  }
}

class Super implements Serializable {
  final Setset = new HashSet();


final class Sub extends Super {

  private int id;
  public Sub(int id) {
    this.id = id;
    set.add(this); // Establish invariant
  }

  public void checkInvariant() {
    if (!set.contains(this))
      throw new AssertionError("invariant violated");
  }

  public int hashCode() {
    return id;
  }

  public boolean equals(Object o) {
    return (o instanceof Sub) && (id == ((Sub)o).id);
  }
}

程序中除了使用了序列化之外,看起来很简单,super类有一个单独的set类型的域,Sub类添加了另外一个int类型的域。Super和Sub都不需要定制的序列化形式。那么什么东西会出错呢?
其实有很多。对于5.0版本,运行该程序会得到如下的“堆轨迹”(stack trace):
Exception in thread “main” AssertionError
  at Sub.checkInvariant(SerialKiller.java:41)
  at SerialKiller.main(SerialKiller.java:10)序列化和反序列化一个Sub实例会产生一个被破坏的副本。为什么呢?阅读程序并不会帮助你找出原因,因为真正引起问题的代码在其他地方。错误是由HashSet的readObject方法引起的。在某些情况下,这个方法会间接地调用某个未初始化对象的被覆写的方法。为了组装(populate)正在被反序列化的散列集合,HashSet.readObject调用了HashMap.put方法,而它会去调用每个键(key)的hashCode方法。由于整个对象图(object graph)正在被反序列化,并没有什么可以保证每个键在它的hashCode方法被调用的时候已经被完全初始化了。实际上,这很少会成为一个问题,但是有时候它会造成绝对的混乱。这个缺陷会在正在被反序列化的对象图的某些循环中出现。

为了更具体一些,让我们看看程序中在反序列化Sub实例的时候发生了什么。首先,序列化系统会反序列化Sub实例中Super的域。唯一的这样的域就是set,它包含了一个对HashSet的引用。在内部,每个HashSet实例包含一个对HashMap的引用,HashMap的键是该散列集合的元素。HashSet类有一个readObject方法,它创建一个空的HashMap,并且使用HashMap的put方法,针对集合中的每个元素在HashMap中插入一个键-值对。put方法会调用键的hashCode方法以确定它所在的单元格(bucket)。在我们的程序中,散列映射表中唯一的键就是Sub的实例,而它的set域正在被反序列化。这个实例的子类域(subclass field),即id,尚未被初始化,所以它的值为0,即所有int域的缺省初始值。不幸的是,Sub的hashCode方法将返回这个值,而不是最后保存在这个域中的值666。因为hashCode返回了错误的值,相应的键-值对条目将会放入错误的单元格中。当id域被初始化为666时,一切都太迟了。当Sub实例在HashMap中的时候,改变这个域的值就会破坏这个域,进而破坏HashSet,破坏Sub实例。程序检测到了这个情况,就报告出了相应的错误。


这个问题和谜题51中的那个本质上几乎是完全相同的。唯一真正不同的是在这个谜题中,readObject伪构造器错误地替代了构造器。HashMap和Hashtable的readObject方法受到的影响是类似的。
对于平台的实现者来说,也许可以通过牺牲一点性能来订正HashSet、HashMap和HashTable中的这个问题。当针对HashSet时,订正的策略可以是重写readObject方法使其在反序列化期间,将集合的元素保存到一个数组中,而不是将它们放入散列集合中。这样,当被反序列化的散列集合的公共方法首次被调用的时候,数组中的元素将在方法执行之前被插入到集合中。
这种方法的代价是它需要在与散列集合的每个公共方法相对应的条目上 检查 是否要组装散列集合。由于HashSet、HashMap以及HashTable都是性能临界(performance-critical)的,所以这个方法看起来是不可取的。更不幸的是,所有的用户都要付出这种代价,甚至当他们不对这些集合(collection)进行序列化时也是如此。这就违背了这样一个原则:你绝不应该为你不使用的功能而付出代价。
另外一个可能的方法是让HashSet.readObject方法调用ObjectInputStream.registerValidation方法,用以将散列集合的组装延迟到validateObject方法回调时再进行。这个方法看起来更吸引人,因为它仅仅增加了反序列化的开销,但是它会破坏任何在“包含流”(containing stream)的反序列化过程中试图使用HashSet实例的代码。
上述的2个方法是否可行还有待研究。但是现在,我们必须接受这些类的这种行为。幸运的是,有一个工作区(workaround):如果一个HashSet、Hashtable或HashMap被序列化,那么请确认它们的内容没有直接或间接地引用到它们自身。这里的内容(content),指的是元素、键和值。
这里也有一个教训送给那些使用可序列化类型的开发者们:在readObject或readResolve方法中,请避免直接或间接地在正在进行反序列化的对象上调用任何方法。如果你必须在某个类型C的readObject或readResolve方法中违背这条建议,请确定没有C的实例会出现在正在被反序列化的对象图的某个循环内。不幸的是,这不是一个本地的属性:一般说来,你需要考虑到整个系统来验证这一点。

相关文章:

  • QT学习之路--菜单、工具条、状态栏
  • 序列化-理解readResolve()
  • Java thread的Interrupt, isInterrupt, interrupted
  • Java字符串
  • Java集合的Stack、Queue、Map的遍历
  • Java正则表达式应用总结
  • javascript的基础知识整理
  • 运行Java应用必须通过main()方法吗?
  • struts2标签库详解
  • Servlet技术总结
  • 深入研究servlet的线程安全问题
  • win10下 Edge和IE浏览器都不能上网,而其他浏览器可以。怎么办?
  • Servlet过滤器机制分析及应用
  • MySQL: Table 'mysql.plugin' doesn't exist的解决
  • Servlet常用过滤器
  • 【391天】每日项目总结系列128(2018.03.03)
  • 【RocksDB】TransactionDB源码分析
  • 【跃迁之路】【733天】程序员高效学习方法论探索系列(实验阶段490-2019.2.23)...
  • 30秒的PHP代码片段(1)数组 - Array
  • android 一些 utils
  • classpath对获取配置文件的影响
  • crontab执行失败的多种原因
  • JavaScript对象详解
  • Java面向对象及其三大特征
  • js写一个简单的选项卡
  • Linux各目录及每个目录的详细介绍
  • mysql常用命令汇总
  • SpiderData 2019年2月13日 DApp数据排行榜
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • ucore操作系统实验笔记 - 重新理解中断
  • 编写高质量JavaScript代码之并发
  • 测试如何在敏捷团队中工作?
  • 持续集成与持续部署宝典Part 2:创建持续集成流水线
  • 今年的LC3大会没了?
  • 免费小说阅读小程序
  • 排序(1):冒泡排序
  • 如何编写一个可升级的智能合约
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • 小白应该如何快速入门阿里云服务器,新手使用ECS的方法 ...
  • #define
  • (11)MATLAB PCA+SVM 人脸识别
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (js)循环条件满足时终止循环
  • (层次遍历)104. 二叉树的最大深度
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (力扣题库)跳跃游戏II(c++)
  • (六)Hibernate的二级缓存
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (最简单,详细,直接上手)uniapp/vue中英文多语言切换
  • ./indexer: error while loading shared libraries: libmysqlclient.so.18: cannot open shared object fil
  • .bat批处理(六):替换字符串中匹配的子串
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .Net的C#语言取月份数值对应的MonthName值
  • .NET企业级应用架构设计系列之技术选型
  • @RequestParam @RequestBody @PathVariable 等参数绑定注解详解