java - 为什么我似乎能够将两个彼此 equal() 的对象添加到 TreeSet

标签 java equals treeset sortedset

将对象添加到 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);
}

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

@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)错误”...

最佳答案

问题是用于对集合进行排序的比较器与 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");

是两个不同的对象。事实上,如果您稍微修改构造函数以添加一个 longcreationTime 参数,并为两个实例提供相同的时间,您会注意到“预期”结果(即排序集将只有一个添加两个实例后大小为 1)。

所以这里有3个解决方案:

  1. 修复 equalshashCode 方法,让它们比较值和时间。
  2. 仅提供比较器来比较值。
  3. 接受这样一个事实:在这种特殊情况下,SortedSet 的行为与 Set 不同。

关于java - 为什么我似乎能够将两个彼此 equal() 的对象添加到 TreeSet,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39681505/

相关文章:

具有相等项的集合中的 Java 比较器

Java 在 O(N) 中迭代 TreeMap/TreeSet 并删除当前元素以外的其他元素

java - equals 和 contains 方法有什么区别

c# - == 是 Equal() 方法的快捷方式吗?

java - 如何检查两个简单的二维数组是否具有相同的一维数组? (顺序和重复并不重要)

java - Android 导入重叠

java - 如何在 Java 中将框架设置为无边框全屏、窗口全屏和全屏?

java - Java中如何让线程池在不同的核心上运行作业?

java - 在Java中将一个对象分配给另一个对象

java - .Contains() 方法不调用 Overridden equals 方法