我对HashSet和TreeSet操作有问题。 这是一个简单的JUnit 4测试,解释了我的问题:
import java.util.HashSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Assert;
import org.junit.Test;
public class TreeSetTest<T> {
@Test
public void test() {
final HashSet<Object> set1 = new HashSet<>();
final TreeSet<Object> set2 = new TreeSet<>((a, b) -> a.toString().compareTo(b.toString()));
Assert.assertEquals(0, set1.size()); // OK
Assert.assertEquals(0, set2.size()); // OK
set1.add(new AtomicReference<>("A"));
set1.add(new AtomicReference<>("B"));
set1.add(new AtomicReference<>("C"));
Assert.assertEquals(3, set1.size()); // OK
Assert.assertEquals(0, set2.size()); // OK
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // OK
Assert.assertEquals(0, set2.size()); // OK
set2.add(new AtomicReference<>("A"));
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
Assert.assertEquals(1, set2.size());
set2.add(new AtomicReference<>("B"));
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
Assert.assertEquals(2, set2.size());
set2.add(new AtomicReference<>("C"));
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // Error Everything has been removed and size is now 0
Assert.assertEquals(3, set2.size());
}
}
从set2
删除set1
的所有元素时,我希望使用set1
的相等比较器,只要set2
具有大小小于set1
的一个,但是如果set2
的大小大于或等于set1
的大小,则从set2
进行比较。
这对我来说很不好,因为它使程序无法预测。
我认为可以将其视为Java实现中的错误,但我担心的是: 如何在不重写的情况下保证预期的行为?
在@FedericoPeraltaSchaffner评论后编辑1:
AtomicReference仅用于提供一个简单示例。实际上,我使用的是库中的final类,因此我无法轻松对其进行改进。
但是,即使考虑有效实现hashCode
和equals
的有效类,我的问题仍然存在。现在考虑一下:
package fr.ncenerar.so;
import java.util.HashSet;
import java.util.TreeSet;
import org.junit.Assert;
import org.junit.Test;
public class TreeSetTest<T> {
public static class MyObj {
private final int value1;
private final int value2;
public MyObj(final int v1, final int v2) {
super();
this.value1 = v1;
this.value2 = v2;
}
public int getValue1() {
return this.value1;
}
public int getValue2() {
return this.value2;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.value1;
result = prime * result + this.value2;
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
final MyObj other = (MyObj) obj;
if (this.value1 != other.value1) {
return false;
}
if (this.value2 != other.value2) {
return false;
}
return true;
}
}
@Test
public void test() {
final HashSet<MyObj> set1 = new HashSet<>();
final TreeSet<MyObj> set2 = new TreeSet<>((a, b) -> a.getValue1() - b.getValue1());
Assert.assertEquals(0, set1.size()); // OK
Assert.assertEquals(0, set2.size()); // OK
set1.add(new MyObj(0, 0));
set1.add(new MyObj(1, 1));
set1.add(new MyObj(2, 2));
Assert.assertEquals(3, set1.size()); // OK
Assert.assertEquals(0, set2.size()); // OK
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // OK
Assert.assertEquals(0, set2.size()); // OK
set2.add(new MyObj(0, 1));
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
Assert.assertEquals(1, set2.size());
set2.add(new MyObj(1, 2));
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
Assert.assertEquals(2, set2.size());
set2.add(new MyObj(2, 3));
set1.removeAll(set2);
Assert.assertEquals(3, set1.size()); // Error Everything has been removed
Assert.assertEquals(3, set2.size());
}
}
问题仍然存在,MyObj
实现正确。问题出在我使用来自两个不同方面的对象这一事实。在一个集合中,我想基于每个对象的相等性保留一个实例(如对象的equals
方法),在另一个集合中,我想要第一个集合的子集,其中每个{{1 }},我只想保留第一个插入的元素。
使用value1
似乎有效。
编辑2:
糟糕,我错过了TreeSet
文档的那一部分:
请注意,集合(无论是否 提供显式比较器)是否必须等于equals 是正确实现Set接口。 (请参阅可比或 比较器,用于精确定义等式。)这是 之所以如此,是因为Set接口是用等式定义的 操作,但TreeSet实例执行所有元素比较 使用其compareTo(或compare)方法,因此两个元素 从集合的角度来看,被此方法视为相等的对象相等。 集合的行为是明确定义的,即使其顺序是 与平等不一致它只是不遵守 设置界面。
如果我理解正确,我可以使用TreeSet
来达到目的,但不能期望它的行为像我想要的那样。
谢谢大家的帮助。
答案 0 :(得分:2)
AtomicReference a = new AtomicReference<>("A");
AtomicReference a2 = new AtomicReference<>("A");
System.out.println(a==a2);
您的答案就在其中。
如果您使用自定义类而不是Object
,并且您重写Override equals方法将按预期工作。
要使其正常工作
class AtomicString{
private AtomicReference<String> s;
public AtomicString(String s) {
this.s = new AtomicReference<>(s);
}
@Override public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
AtomicString that = (AtomicString) o;
return this.s.get().equals(that.getS().get());
}
public AtomicReference<String> getS() {
return s;
}
@Override public int hashCode() {
return Objects.hash(s.get());
}
答案 1 :(得分:1)
问题实际上是不一致的“平等”逻辑。 TreeSet
和HashSet
都继承AbstractSet#removeAll
,它在较小的集合上进行迭代,因此使用该集合的对象比较。事实证明,可以使用TreeSet
覆盖“平等”逻辑。
通过选择两个Set
实现之一可以避免此问题。如果选择TreeSet
,则还必须使用相同的比较器。
在这种情况下,您无法真正使用HashSet
,因为AtomicReference
没有适合您的equals
/ hashCode
实现。因此,您唯一可行的选择是使用TreeSet
:
Comparator<Object> comparator = (a, b) -> a.toString().compareTo(b.toString());
final Set<Object> set1 = new TreeSet<>(comparator);
final Set<Object> set2 = new TreeSet<>(comparator);
这会破坏您当前的测试,但是因为现在已经按照应有的方式删除了元素(根据比较器的逻辑)。
答案 2 :(得分:0)
正确搜索并阅读有关TreeSet
的文档后,事实证明:
请注意,集合(无论是否 提供显式比较器)是否必须等于equals 是正确实现Set接口。 (请参阅比较器 比较器,用于精确定义与equals的一致性。) 因此,因为Set接口是根据等值定义的 操作,但TreeSet实例执行所有元素比较 使用其compareTo(或compare)方法,因此两个元素 从集合的角度来看,被此方法视为相等的对象相等。 集合的行为是明确定义的,即使其顺序是 与平等不一致它只是不遵守 设置界面。
这意味着示例中使用的TreeSet
不能用作Set
。因此,最简单的解决方案是通过替换以下各项来为HashSet
操作创建removeAll
:
set1.removeAll(set2);
作者
set1.removeAll(new HashSet<>(set2));
从性能的角度来看,也许不是最好的解决方案,但是可行的。
谢谢大家!