【问题标题】:Why do I seem able to add two objects that are equal() to each other to a TreeSet为什么我似乎能够将彼此相等()的两个对象添加到 TreeSet
【发布时间】:2016-09-24 22:26:39
【问题描述】:

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() 说明了我们的预期:

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

鉴于我断言这两个对象是equal(),我希望这会通过。我正在假设我遗漏了一些非常明显的东西,除非TreeSet 实际上使用Object.equals(),但在绝大多数情况下使用的东西几乎相同.

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

【问题讨论】:

  • 我的实际用例是一个按可选 ZonedDateTime 字段排序的缓存,但使用System.nanoTime() 回退到插入顺序。
  • 如果 equals 和您的比较器查看不同的字段,为什么它们会以兼容的方式运行?

标签: java equals treeset sortedset


【解决方案1】:

问题是给集合排序的比较器与WrappedValueequals方法不兼容。您期望 SortedSet 的行为类似于 Set,但在这种情况下它不会这样做。

来自SortedSet

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

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

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

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

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

是两个不同的对象。实际上,如果您稍微修改构造函数以添加 long creationTime 参数,并为两个实例提供相同的时间,您会注意到“预期”结果(即,在添加两个实例)。

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

  1. 修复equalshashCode方法,让它们比较值和时间。
  2. 只给出比较值的比较器。
  3. 接受SortedSet 在这种特殊情况下的行为与Set 不同的事实。

【讨论】:

  • 谢谢 - 我认为这里要吸取的教训是我需要阅读 所有 文档,而不仅仅是仔细检查被调用的方法。我的用例的最佳解决方案可能是修复比较器。
【解决方案2】:

您的 equals(仅考虑 value)与考虑 valuecreationTime 的 Comparator 不一致。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多