为什么 HashSet 在 Java 8 和 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 while loop: 142:142
In while loop: 242:242
Size after remove iteration: 2

Java 9+ 输出与上面相同,但:

Size after remove iteration: 0

为什么会发生这种情况?

leqihuan 回答:为什么 HashSet 在 Java 8 和 Java 9+ 中表现不同?

某些在 Java 8 和 Java 9 之间更改了 HashSet,但具体细节实际上并不那么有趣,因为您使用 Set 的方式已经是 {{ 3}}(强调我的):

注意:如果将可变对象用作集合元素,则必须非常小心。 如果在对象是集合中的一个元素的情况下以影响相等比较的方式更改对象的值,则不会指定集合的​​行为

由于 specified to be wrong 取决于 Date 代表的时间,因此您完全可以这样做。

既然你这样做了,集合的行为就不再指定了。

这意味着它可以以任何方式/形状/形式表现不正常,但仍然是符合要求的实现。

您可以尝试找出为什么 Java 9 现在的行为特别不同(我自己也不知道),但它不会改变任何的基本问题如果您以错误的方式使用 set,JVM 在任何时候都可能再次表现出不同的行为。

编辑:出于好奇,我确实调查了到底有什么不同并发现了相关的变化:在 OpenJDK 8 和 9 中,HashSet 是基于一个 HashMap,所以这一切都集中在 HashMap

在 Java 8 中 Date.equals() 包含这一行:

K key = p.key;
removeNode(hash(key),key,null,false,false);

这会重新散列(即获取当前散列)key(即您的 Date)并尝试将其从 Map 中删除。由于该新哈希从未首先添加(回到添加该键时它具有不同的哈希),因此不会找到节点,因此不会删除任何内容。

在 Java 9 中,the remove() method of the relevant Iterator implementation 看起来像这样:

removeNode(p.hash,p.key,false);

这将简单地将先前计算并记住的哈希 p.hash 传递给 removeNode 方法,从而能够找到并删除有问题的节点。

that code 提到了 changeset that introduced that change

那里的评论(特别是 Doug Lea 的评论)似乎同意“修复”面对误用集合的行为不是目标,但不重新计算散列可能会更快。换句话说:该更改是出于性能原因而不是出于正确性原因。

总结并重申这两种行为都是可以接受的实现,因为通过更改集合条目的 equals() 行为,您“已经违约了。

本文链接:https://www.f2er.com/9031.html

大家都在问