将对象添加到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;某处...
答案 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个解决方案:
long creationTime
和equals
方法,让他们比较价值和时间。hashCode
的行为与SortedSet
不同的事实。答案 1 :(得分:1)
您的平等(仅考虑value
)与您的考虑value
和creationTime
的比较者不一致。
我假设您有两个具有相同值的对象,因此它们等于true但创建时间不同,因此它们是compareTo!= 0.