Map/Set 对象图的奇怪 equals() 结果

Weird equals() result with Map/Set object graph

调查一个特殊情况,其中某些对象不等于它们应有的值,并得出这个简化我的问题的简单测试用例。

当 运行 在 Eclipse 中使用 JUnit 和 jdk8u152 最后一个 assertEquals 失败时,谁能解释为什么?

这与 Set/HashSet 相关,因为如果我将 as,bs 更改为 ArrayList,最终的 assertEquals 就会通过。

@Test
public void test()
{
    String list = "list";
    String object = "object";
    String value = "value";

    Map<String, Object> a = new HashMap<>();
    Map<String, Object> b = new HashMap<>();

    assertEquals(a, b);

    Set<Object> as = new HashSet<>();
    Set<Object> bs = new HashSet<>();

    a.put(list, as);
    b.put(list, bs);

    assertEquals(a, b);

    Map<String, Object> ao = new HashMap<>();
    as.add(ao);
    Map<String, Object> bo = new HashMap<>();
    bs.add(bo);

    assertEquals(a, b);

    ao.put(object, value);
    bo.put(object, value);

    assertEquals(a, b);
}

您正在改变集合的元素。这会导致未指定的行为。

来自JavaDoc

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.

这是因为 HashMap 的 hascode 实现基本上是键和值的异或。如果 key 或 value 为空,则 hascode 将为零。因此,所有空哈希映射的哈希码都将为零。

/*hashcode of HashMap*/
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

/*hashcode of object*/
    public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0;
    }

添加键值对后,哈希码值会发生变化。

您正在将 aobo HashMap 添加到 HashSet asbs

稍后您通过在每个 aobo 中放置一个新条目来改变它们。

这意味着在as中用于放置aohashCode不再是ao的当前hashCode,并且hashCode 用于在 bs 中放置 bo 不再是 bo 的当前 hashCode

结果AbstractSetequals无法在另一个Set中定位一个Set的元素,所以得出as不等于 bs。结果 a 不等于 b.

这里是 AbstractSetequals 的实现。你可以看到它使用 containsAll,它又调用 contains(),它依赖于搜索元素的 hashCode。由于 hashCode 在元素添加到 Set 后发生了变化,contains() 找不到该元素。

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Set))
        return false;
    Collection<?> c = (Collection<?>) o;
    if (c.size() != size())
        return false;
    try {
        return containsAll(c);
    } catch (ClassCastException unused)   {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
}

如果您以影响 equalshashCode 结果的方式改变 HashSet 的元素,则必须先从 HashSet 中删除该元素到更新,更新后再添加。

添加以下 removeadd 调用将导致 a 最后等于 b

....
assertEquals(a, b);

bs.remove (bo); // added
as.remove (ao); // added

ao.put(object, value);
bo.put(object, value);

as.add (ao); // added
bs.add (bo); // added

assertEquals(a, b);