意外的HashSet等于行为

Lau*_*fel 0 java equals hashset

这是Java 8中发生奇怪行为的代码段:

 @Test
    public void test() {
        DummyPojo dummyPojo1 = DummyPojo.of("1", "A");
        DummyPojo dummyPojo2 = DummyPojo.of("2", "B");

        Set<DummyPojo> set1 = new HashSet<>();
        set1.add(dummyPojo1);

        Set<DummyPojo> set2 = new HashSet<>();
        set2.add(dummyPojo2);

        System.out.println("dummyPojo1 == dummyPojo2 should be false = " + (dummyPojo1.equals(dummyPojo2)));
        System.out.println("set1 == set2             should be false = " + (set1.equals(set2)));

        dummyPojo1.setAttribute1(dummyPojo2.getAttribute1());
        dummyPojo1.setAttribute2(dummyPojo2.getAttribute2());

        System.out.println("dummyPojo1 == dummyPojo2 should be true  = " + (dummyPojo1.equals(dummyPojo2)));
        System.out.println("set1 == set2             should be true  = " + (set1.equals(set2)));//WRONG
        System.out.println("set2 == set1             should be true  = " + (set2.equals(set1)));//Breaking of Object#equals symmetry
    }

    @Data
    public static class DummyPojo {
        private String attribute1;
        private String attribute2;

        public static DummyPojo of(String attribute1, String attribute2) {
            DummyPojo dummyPojo = new DummyPojo();
            dummyPojo.attribute1 = attribute1;
            dummyPojo.attribute2 = attribute2;
            return dummyPojo;
        }
    }
Run Code Online (Sandbox Code Playgroud)

结果如下:

dummyPojo1 == dummyPojo2 should be false = false
set1 == set2             should be false = false
dummyPojo1 == dummyPojo2 should be true  = true
set1 == set2             should be true  = false
set2 == set1             should be true  = true
Run Code Online (Sandbox Code Playgroud)

插入修改集合中的元素会具有此行为(请注意,lombok批注@Data确实实现了equals和hashcode方法)。原因是在将元素添加到集合中时,会将其插入支持地图的hashMap的节点表中。为此,它通过哈希码计算索引。

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) //<-- HERE table[15 & hashcode] when first element
            tab[i] = newNode(hash, key, value, null);
        .
        .
        .
}
Run Code Online (Sandbox Code Playgroud)

但是,当检查它是否包含该元素时,它将假定表中的位置相同:

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {//<-- HERE table[15 & hashcode]
        .
        .
        .
}
Run Code Online (Sandbox Code Playgroud)

但是,由于此示例中的元素不是不可变的,因此哈希码已在插入和包含之间更改。因此,即使元素根据其equals方法等于,因此集合包含相同的元素,集合上的equals也会返回意外值。

而且,如此处所示,它打破了Object#equals的javadoc中所述的equals对称性:

它是对称的:对于任何非null的引用值xy当且仅当x.equals(y) return时应返回。true y.equals(x)true

有一个Java错误报告它:https : //bugs.java.com/bugdatabase/view_bug.do?bug_id=6579200

答案是 :

HashSet和HashMap的约定不允许hashCode()的结果在集合中某个对象的使用期限内更改,也不允许与该对象相等的一组对象在使用期限内更改。

但是,我没有在HashMap或HashSet的javadoc中找到它。一个不了解这种行为的人可能对此表示过多的信任,并且很难理解生产问题。我发现更麻烦的是对象契约的破裂,因为框架可能依赖它。

是否计划修改(如果不是实现的话),至少是javadoc以便更清楚地指定它?

And*_*ner 5

它记录在Map

注意:如果将可变对象用作地图键,则必须格外小心。如果在对象是映射中的键的情况下以影响等值比较的方式更改对象的值,则不会指定映射的行为。

Set

注意:如果将可变对象用作集合元素,则必须格外小心。如果对象的值更改为影响相等比较的方式,而该对象是集合中的元素,则不指定集合的​​行为。

可变值问题也影响其他映射/集合实现(例如TreeMap/ TreeSet)。

这实际上不是实现的问题:(通常)没有一种语言机制可以使集合(或者实际上是简单的引用)知道所引用的对象是否已更改。