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

时间:2016-09-24 22:26:39

标签: 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()陈述了我们所期望的:

  

如果指定的元素尚不存在,则将其添加到此集合中。更正式的是,如果集合中不包含e元素e2,则将指定的元素(e==null ? e2==null : e.equals(e2))添加到此集合中。如果此集已包含元素,则调用将保持集不变并返回false

鉴于我断言这两个对象是equal(),我希望这会通过。我假设我错过了一些明显的东西,除非TreeSet 实际使用Object.equals(),但使用的是&{在绝大多数情况下,它的距离相近。

这是使用JDK 1.8.0.60观察到的 - 我还没有机会测试其他JDK,但是我假设有一些&#34;操作员错误&#34;某处...

2 个答案:

答案 0 :(得分:3)

问题是用于对集合进行排序的比较器与equals的{​​{1}}方法不兼容。您希望WrappedValue的行为与SortedSet相似,但在这种情况下不会这样做。

来自SortedSet

  

请注意,如果排序集要正确实现Set接口,则排序集[...]维护的排序必须equals 一致。 [...]这是因为Set接口是根据Set操作定义的,但是有序集使用其equals(或compareTo执行所有元素比较因此,从排序集的角度来看,这种方法认为相等的两个元素是相等的。排序集的行为定义良好,即使其排序与equals不一致;它只是不遵守compare接口的一般合同。

换句话说,Set仅使用您给出的比较器来确定两个元素是否相等。在这种情况下,比较器是

SortedSet

比较值,然后比较创建时间。但由于Comparator.comparing(WrappedValue::getValue).thenComparing(WrappedValue::getCreationTime) 的构造函数使用WrappedValue初始化(有效)唯一创建时间,因此该比较器不会将两个System.nanoTime()视为相等。因此,就排序集而言

WrappedValue

是两个不同的对象。实际上,如果你修改一点构造函数来添加一个WrappedValue firstInstance = new WrappedValue("Hello"); WrappedValue secondInstance = new WrappedValue("Hello"); 参数,并给两个实例提供相同的时间,你会注意到“预期的”结果(即排序的集合之后只有1的大小)添加两个实例)。

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

  1. 修正long creationTimeequals方法,让他们比较价值和时间。
  2. 只提供一个比较该值的比较器。
  3. 在这种特殊情况下,接受hashCode的行为与SortedSet不同的事实。

答案 1 :(得分:1)

您的平等(仅考虑value)与您的考虑valuecreationTime的比较者不一致。

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