为什么 HashSet 在 Java 8 和 Java 9+ 中表现不同?
Why do HashSets behave differently in Java 8 and Java 9+?
当试图移除包裹在迭代器 Java 8 和 Java 9+ 中的对象时,行为不同。考虑以下示例:
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
class Scratch {
public static void main(String[] args) {
Set<Date> dates = new HashSet<>();
dates.add(new Date(100));
dates.add(new Date(200));
for (Date date : dates) {
System.out.println("Initial "+date.getTime()+":"+date.hashCode());
date.setTime(date.getTime()+42);
System.out.println("Mutated "+date.getTime()+":"+date.hashCode()+"\n");
}
System.out.println("Size before remove iteration: "+dates.size());
Iterator<Date> iterator = dates.iterator();
while (iterator.hasNext()) {
Date date = iterator.next();
System.out.println("In loop: "+date.getTime()+":"+date.hashCode());
iterator.remove();
}
System.out.println("Size after remove iteration: "+dates.size());
}
}
在 HashSet 中改变对象后 Java 8 拒绝使用迭代器删除它们,检查删除循环后的大小。
Java 8 输出:
Initial 100:100
Mutated 142:142
Initial 200:200
Mutated 242:242
Size before remove iteration: 2
In loop: 142:142
In loop: 242:242
Size after remove iteration: 2
Java 9+ 输出与上面相同但是:
Size after remove iteration: 0
为什么会这样?
在 Java 8 和 Java 9 之间 HashSet
发生了一些 的变化,但细节实际上并不那么有趣,因为方式您使用的 Set
已经是 specified to be wrong(强调我的):
Note: Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
由于 Date.equals()
取决于 Date
所代表的时间,因此您完全可以这样做。
既然你这样做了,集合的行为就不再指定了。
这意味着它可以在任何 way/shape/form 中表现不当并且仍然是一个符合规范的实现。
你可以尝试找出为什么 Java 9 现在具体表现不同(我自己也不知道),但这并没有改变根本问题任何 如果您以错误的方式使用集合,JVM 可能在任何时间点再次表现不同。
编辑:出于好奇,我确实调查了到底有什么不同,并发现了一个相关的变化:在 OpenJDK 8 和 9 HashSet
是基于一个HashMap
实现的,所以这一切都集中在HashMap
.
在Java 8 the remove()
method of the relevant Iterator
implementation中包含这一行:
K key = p.key;
removeNode(hash(key), key, null, false, false);
这会重新散列(即获取当前散列)key
(即您的 Date
)并尝试将其从 Map
中删除。由于一开始就没有添加新的哈希值(添加该键时它有一个不同的哈希值),这将找不到节点,因此不会删除任何内容。
在 Java 9 that code 中看起来像这样:
removeNode(p.hash, p.key, null, false, false);
这将简单地将先前计算和记住的散列 p.hash
传递给 removeNode
方法,从而能够找到并删除有问题的节点。
changeset that introduced that change mentions this OpenJDK bug.
那里的评论(尤其是 Doug Lea 的评论)似乎同意“修复”面对滥用集的行为不是目标,但不重新计算哈希可能会更快。换句话说:该更改是出于性能原因而不是出于正确性原因。
总结和重申:这些行为都是可以接受的实现,因为通过改变equals()
你设置条目的行为,你已经违反了合同。
当试图移除包裹在迭代器 Java 8 和 Java 9+ 中的对象时,行为不同。考虑以下示例:
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
class Scratch {
public static void main(String[] args) {
Set<Date> dates = new HashSet<>();
dates.add(new Date(100));
dates.add(new Date(200));
for (Date date : dates) {
System.out.println("Initial "+date.getTime()+":"+date.hashCode());
date.setTime(date.getTime()+42);
System.out.println("Mutated "+date.getTime()+":"+date.hashCode()+"\n");
}
System.out.println("Size before remove iteration: "+dates.size());
Iterator<Date> iterator = dates.iterator();
while (iterator.hasNext()) {
Date date = iterator.next();
System.out.println("In loop: "+date.getTime()+":"+date.hashCode());
iterator.remove();
}
System.out.println("Size after remove iteration: "+dates.size());
}
}
在 HashSet 中改变对象后 Java 8 拒绝使用迭代器删除它们,检查删除循环后的大小。 Java 8 输出:
Initial 100:100
Mutated 142:142
Initial 200:200
Mutated 242:242
Size before remove iteration: 2
In loop: 142:142
In loop: 242:242
Size after remove iteration: 2
Java 9+ 输出与上面相同但是:
Size after remove iteration: 0
为什么会这样?
在 Java 8 和 Java 9 之间 HashSet
发生了一些 的变化,但细节实际上并不那么有趣,因为方式您使用的 Set
已经是 specified to be wrong(强调我的):
Note: Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
由于 Date.equals()
取决于 Date
所代表的时间,因此您完全可以这样做。
既然你这样做了,集合的行为就不再指定了。
这意味着它可以在任何 way/shape/form 中表现不当并且仍然是一个符合规范的实现。
你可以尝试找出为什么 Java 9 现在具体表现不同(我自己也不知道),但这并没有改变根本问题任何 如果您以错误的方式使用集合,JVM 可能在任何时间点再次表现不同。
编辑:出于好奇,我确实调查了到底有什么不同,并发现了一个相关的变化:在 OpenJDK 8 和 9 HashSet
是基于一个HashMap
实现的,所以这一切都集中在HashMap
.
在Java 8 the remove()
method of the relevant Iterator
implementation中包含这一行:
K key = p.key;
removeNode(hash(key), key, null, false, false);
这会重新散列(即获取当前散列)key
(即您的 Date
)并尝试将其从 Map
中删除。由于一开始就没有添加新的哈希值(添加该键时它有一个不同的哈希值),这将找不到节点,因此不会删除任何内容。
在 Java 9 that code 中看起来像这样:
removeNode(p.hash, p.key, null, false, false);
这将简单地将先前计算和记住的散列 p.hash
传递给 removeNode
方法,从而能够找到并删除有问题的节点。
changeset that introduced that change mentions this OpenJDK bug.
那里的评论(尤其是 Doug Lea 的评论)似乎同意“修复”面对滥用集的行为不是目标,但不重新计算哈希可能会更快。换句话说:该更改是出于性能原因而不是出于正确性原因。
总结和重申:这些行为都是可以接受的实现,因为通过改变equals()
你设置条目的行为,你已经违反了合同。