为什么我似乎能够将两个彼此相等的对象添加到 TreeSet

Why do I seem able to add two objects that are equal() to each other to a TreeSet

将对象添加到 java.util.TreeSet 时,您希望两个相等的对象在添加后只存在一次,并且以下测试按预期通过:

@Test
void canAddValueToTreeSetTwice_andSetWillContainOneValue() {
    SortedSet<String> sortedSet = new TreeSet<>(Comparator.naturalOrder());

    // String created in a silly way to hopefully create two equal Strings that aren't interned
    String firstInstance = new String(new char[] {'H', 'e', 'l', 'l', 'o'});
    String secondInstance = new String(new char[] {'H', 'e', 'l', 'l', 'o'});

    assertThat(firstInstance).isEqualTo(secondInstance);
    assertThat(sortedSet.add(firstInstance)).isTrue();
    assertThat(sortedSet.add(secondInstance)).isFalse();
    assertThat(sortedSet.size()).isEqualTo(1);
}

将这些字符串包装在包装器 class 中,其中 equals()hashCode() 仅基于包装的 class,但测试失败:

@Test
void canAddWrappedValueToTreeSetTwice_andSetWillContainTwoValues() {
    SortedSet<WrappedValue> sortedSet = new TreeSet<>(Comparator.comparing(WrappedValue::getValue).thenComparing(WrappedValue::getCreationTime));

    WrappedValue firstInstance = new WrappedValue("Hello");
    WrappedValue secondInstance = new WrappedValue("Hello");

    assertThat(firstInstance).isEqualTo(secondInstance); // Passes
    assertThat(sortedSet.add(firstInstance)).isTrue();   // Passes
    assertThat(sortedSet.add(secondInstance)).isFalse(); // Actual: True
    assertThat(sortedSet.size()).isEqualTo(1);           // Actual: 2
}

private class WrappedValue {
    private final String value;
    private final long creationTime;

    private WrappedValue(String value) {
        this.value = value;
        this.creationTime = System.nanoTime();
    }

    private String getValue() {
        return value;
    }

    private long getCreationTime() {
        return creationTime;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof WrappedValue)) return false;
        WrappedValue that = (WrappedValue) o;
        return Objects.equals(this.value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

The JavaDoc for TreeSet.add() 陈述了我们的预期:

Adds the specified element to this set if it is not already present. More formally, adds the specified element e to this set if the set contains no element e2 such that (e==null ? e2==null : e.equals(e2)). If this set already contains the element, the call leaves the set unchanged and returns false.

鉴于我断言这两个对象是 equal(),我希望这会通过。我正在假设我遗漏了一些非常明显的东西,除非 TreeSet 没有 实际上使用 Object.equals(),而是使用了足够接近的东西在绝大多数情况下都是一样的。

这是使用 JDK 1.8.0.60 观察到的 - 我还没有机会测试其他 JDK,但我假设某处有一些 "Operator Error".. .

您的 equals(仅考虑 value)与您的 Comparator 不一致,后者考虑了 valuecreationTime

我假设您有两个具有相同值的对象,因此它们等于 true 但创建时间不同,因此它们是 compareTo != 0。

问题是给定的用于对集合进行排序的比较器与 WrappedValueequals 方法不兼容。您期望 SortedSet 表现得像 Set,但在这种情况下它不会这样做。

来自 SortedSet:

Note that the ordering maintained by a sorted set [...] must be consistent with equals if the sorted set is to correctly implement the Set interface. [...] This is so because the Set interface is defined in terms of the equals operation, but a sorted set performs all element comparisons using its compareTo (or compare) method, so two elements that are deemed equal by this method are, from the standpoint of the sorted set, equal. The behavior of a sorted set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface.

换句话说,SortedSet 仅使用您提供的比较器来确定两个元素是否相等。本例中的比较器是

Comparator.comparing(WrappedValue::getValue).thenComparing(WrappedValue::getCreationTime)

先比较值再比较创建时间。但是由于 WrappedValue 的构造函数使用 System.nanoTime() 初始化了一个(有效的)唯一创建时间,因此此比较器不会认为两个 WrappedValue 相等。因此,就排序集而言

WrappedValue firstInstance = new WrappedValue("Hello");
WrappedValue secondInstance = new WrappedValue("Hello");

是两个不同的对象。实际上,如果您稍微修改构造函数以添加 long creationTime 参数,并为两个实例提供相同的时间,您会注意到 "expected" 结果(即排序集的大小仅为1 添加两个实例后)。

所以这里有3种解法:

  1. 修复 equalshashCode 方法,让它们比较值和时间。
  2. 只给一个comparator比较值
  3. 接受 SortedSet 在这种特殊情况下表现得不像 Set 的事实。