使用Map 是正确的方法,但您可以将其封装在Set 实现中,具有“带有自定义equals 的Set”的预期行为。
public class CustomSet<E> extends AbstractSet<E> {
private final Function<E, Object> theKeyFunction;
private final HashMap<Object, E> backend = new HashMap<>();
public CustomSet(Function<E,Object> keyFunction) {
theKeyFunction = Objects.requireNonNull(keyFunction);
}
@Override
public int size() {
return backend.size();
}
@Override
public boolean add(E e) {
Objects.requireNonNull(e);
return backend.putIfAbsent(theKeyFunction.apply(e), e) == null;
}
@Override
public boolean contains(Object o) {
if(o == null) return false;
@SuppressWarnings("unchecked") E e = (E)o;
Object key;
try { key = theKeyFunction.apply(e); }
catch(ClassCastException ex) { return false; }
return backend.containsKey(key);
}
@Override
public boolean remove(Object o) {
if(o == null) return false;
@SuppressWarnings("unchecked") E e = (E)o;
Object key;
try { key = theKeyFunction.apply(e); }
catch(ClassCastException ex) { return false; }
return backend.remove(key) != null;
}
@Override
public void clear() {
backend.clear();
}
@Override
public boolean retainAll(Collection<?> c) {
return backend.values().retainAll(c);
}
@Override
public boolean removeIf(Predicate<? super E> filter) {
return backend.values().removeIf(filter);
}
@Override
public void forEach(Consumer<? super E> action) {
backend.values().forEach(action);
}
@Override
public Iterator<E> iterator() {
return backend.values().iterator();
}
@Override
public Spliterator<E> spliterator() {
return backend.values().spliterator();
}
@Override
public Object[] toArray() {
return backend.values().toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return backend.values().toArray(a);
}
}
为了简单起见,这个Set 不支持null。
这个类重写了一些它不需要的方法,以便在迭代或流式传输时提供更好的性能。除此之外,它相当简单。如果您认为“但是内部使用Map 的Set 效率很低”,请查看HashSet 或TreeSet 的源代码...
这个集合实现可以像这样测试
record Person(String name, int age) {}
Set<Person> nameSet = new CustomSet<>(Person::name);
Set<Person> ageSet = new CustomSet<>(Person::age);
for(String name: List.of("John", "Paul", "George", "Ringo")) {
for(int age: new int[] { 20, 24, 27, 31 }) {
Person p = new Person(name, age);
if(nameSet.add(p)) System.out.println("added " + p + " to nameSet");
if(ageSet.add(p)) System.out.println("added " + p + " to ageSet");
}
}
System.out.println();
System.out.println("nameSet: " + nameSet);
System.out.println("ageSet: " + ageSet);
System.out.println();
Person p = new Person("Paul", 100);
System.out.println("nameSet contains " + p + "? " + nameSet.contains(p));
System.out.println("ageSet contains " + p + "? " + ageSet.contains(p));
p = new Person("Bob", 27);
System.out.println("nameSet contains " + p + "? " + nameSet.contains(p));
System.out.println("ageSet contains " + p + "? " + ageSet.contains(p));
added Person[name=John, age=20] to nameSet
added Person[name=John, age=20] to ageSet
added Person[name=John, age=24] to ageSet
added Person[name=John, age=27] to ageSet
added Person[name=John, age=31] to ageSet
added Person[name=Paul, age=20] to nameSet
added Person[name=George, age=20] to nameSet
added Person[name=Ringo, age=20] to nameSet
nameSet: [Person[name=George, age=20], Person[name=John, age=20], Person[name=Ringo, age=20], Person[name=Paul, age=20]]
ageSet: [Person[name=John, age=20], Person[name=John, age=24], Person[name=John, age=27], Person[name=John, age=31]]
nameSet contains Person[name=Paul, age=100]?true
ageSet contains Person[name=Paul, age=100]?false
nameSet contains Person[name=Bob, age=27]?false
ageSet contains Person[name=Bob, age=27]?true
展示了对两组相等性的不同理解,这导致了与 TreeSet 相同的警告,比较器与 equals 不一致。混合具有不同键功能的集合可能会导致与混合具有不同比较器的排序集或将此类集合与普通哈希集混合一样的奇怪行为。
如果键由多个属性组成,则可以使用专用的键对象,但这并不意味着应用程序域对象必须是组合对象:
record MyData(int alpha, double beta, int foo, double bar) {}
Set<MyData> set = new CustomSet<>(d -> {
record Key(int alpha, double beta) {}
return new Key(d.alpha(), d.beta());
});
set.add(new MyData(1, 1.0, 100, 1.23));
System.out.println(set.contains(new MyData(1, 1.0, -1, Double.NaN))); // true
不带record 的旧Java 版本的解决方案有点冗长,但原理保持不变。如果您不需要最高性能,您还可以使用 List 键,因为它们具有有效的 equals 和 hashCode 实现:
// Java 8 compatible
Set<MyData> set = new CustomSet<>(d -> Arrays.asList(d.alpha(), d.beta()));