我在第 191 行的 java.util.HashSet.isEmpty() 中遇到了一个 NPE。我猜,这一行只是在内部映射字段上调用了 isEmpty()。
令我惊讶的是,这个字段是 transient 的,所以在反序列化之后,它会是空的。但这不是反序列化集合的目的,以取回值吗?在我看来,反序列化是该字段对于 HashSet 实例可以为 null 的唯一方式。
可能,我在这里遗漏了什么。谁能解释一下?
Linux 上的 Java 1.8.0_151(3.13.0-61-generic)在平台 amd64 上
这是 JDK 中 HashMap.isEmpty
的实现:
/**
* Returns <tt>true</tt> if this set contains no elements.
*
* @return <tt>true</tt> if this set contains no elements
*/
public boolean isEmpty() {
return map.isEmpty(); // line 191
}
编辑/附加信息:
- 不幸的是,我无法给出一个最小的例子来说明这个问题。它发生在对运行数小时的复杂系统进行回归测试期间。所以,我不知道也无法控制这个集合会发生什么。此外,问题是不确定的,这意味着我们偶尔会看到它,但不是每次都会看到它
- 我可以提供一段调用
isEmpty()
的代码。但是 NPE 出现在库中,因此该方法显然是在现有实例上调用的,即不是在null
上调用的。因此,提供此代码段帮助不大 - 我们的代码是多线程的。但是乍一看,这部分只有单线程访问
- 我们的代码在很多地方捕获并吞下异常。这可以隐藏真正的罪魁祸首
最佳答案
由于 map
字段不是 final
,因此无法保证在发布新构造的 HashSet
实例时看到它处于初始化状态在多线程代码中不正确。而“新构造”意味着自构造以来没有发生其他确保所涉及线程之间内存可见性的操作,原则上这可能是任意长的时间。
在对象反序列化期间,readObject
方法负责重新初始化 map
字段并放回元素。这是从持久表单中隐藏这些实现细节所必需的。此外,反序列化的对象可能具有与序列化时不同的哈希码(例如,采用从 java.lang.Object
继承的哈希码)。因此无论如何都必须重新插入它们。大多数集合的一般原则是隐藏实现细节,并在专用的 writeObject
和 readObject
方法中依次序列化和反序列化每个包含的元素。
因此, future 的 HashSet
实现可能是一个真正的哈希集,而不是在幕后使用 HashMap
,而不会影响序列化兼容性。
关于java - Java HashSet.isEmpty() 中的 NPE,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52512767/