【问题标题】:A set-like collection with a customizable "equals"具有可自定义“等于”的集合状集合
【发布时间】:2022-01-13 04:15:03
【问题描述】:

我有一个数据类:

public class MyData {
  final Integer alpha;
  final Double beta;
  final Integer foo;
  final Double bar;
}

我需要.equals.hashCode 来获得涉及所有四个字段的常规定义。但我还有一个重要的要求:

鉴于大量的MyData 对象,我需要快速检查新的MyData 对象是否与.alpha.beta 字段中的任何现有对象匹配

不想想采取的三种方法:

  1. 复合对象:
public class MyData {
  final MyDataKey alphaAndBeta;
  final Integer foo;
  final Double bar;
}
public class MyDataKey {
  final Integer alpha;
  final Double beta;
}

虽然我随后可以对 HashSet<MyDataKey> 进行查找,但这并不优雅,因为该对象的所有其他用途都需要引用 dataObj.alphaAndBeta.alpha 而不是 dataObj.alpha

  1. 比较器:
public class OnlyAlphaAndBeta implements Comparator<MyData> {
  int compare(MyData a, MyData b) {...}
}

这会让new TreeSet&lt;MyData&gt;(new OnlyAlphaAndBeta()) 进行我想要的查找;但使用 O(log(N)) 而不是 O(1)。

  1. 多级查找:
public class MyDataLookup {
  Map<Integer, Set<Double>> existingAlphaBeta;
  
  boolean contains(MyData query) {
    Set<Double> betas = this.existingAlphaBeta.get(query.alpha);
    if (betas == null) {
      return false;
    }
    return betas.contains(query.beta);
  }

  boolean add(MyData toInsert) {...};
}

这在 O(1) 中完成了这项工作,但如果键不仅仅是 2 个字段怎么办?我可以继续为键中的每个字段嵌套Map&lt;A, Map&lt;B, Map&lt;C, ...&gt;&gt;&gt;,但这似乎不对。当然,我宁愿只计算一个哈希值并在一张表中查找它。


我认为我正在寻找类似于 HashSet 的东西,但它可以专门用于使用 .equals.hashCode 方法之外的东西其他,类似于 @987654339 @ 重新定义了 SortedSet 的排序。这样的集合将不再履行Set 合约,但它会“类似于集合”。

在大型、维护良好的 Java 实用程序库中是否存在类似的东西?或者,我是否忽略了一些明显的实现目标的方法?

【问题讨论】:

  • 您是否考虑过使用HashMap&lt;AlphaBetaKey, Set&lt;MyData&gt;&gt;,这应该提供高效的O(1) 查找?但是,此类地图需要与现有的Collection&lt;MyData&gt;分开收集/维护
  • “它不优雅,因为对象的所有其他用途都需要引用 dataObj.alphaAndBeta.alpha 而不是 dataObj.alpha。” → 为什么不使用 getter(和 setter)? dataObj.alphaAndBeta.alpha 变成了 dataObj.getAlpha(),这在 Java 中非常常用。事实上,原始字段访问通常不鼓励使用私有字段和 getter+setter。

标签: java collections


【解决方案1】:

我保证在写这个问题之前我确实尝试过寻找答案。但 24 小时后,我进行了一次更有成效的搜索,并找到了几个可行的答案:

【讨论】:

    【解决方案2】:

    使用Map 是正确的方法,但您可以将其封装在Set 实现中,具有“带有自定义equalsSet”的预期行为。

    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

    这个类重写了一些它不需要的方法,以便在迭代或流式传输时提供更好的性能。除此之外,它相当简单。如果您认为“但是内部使用MapSet 效率很低”,请查看HashSetTreeSet 的源代码...

    这个集合实现可以像这样测试

    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 键,因为它们具有有效的 equalshashCode 实现:

    // Java 8 compatible
    Set<MyData> set = new CustomSet<>(d -> Arrays.asList(d.alpha(), d.beta()));
    

    【讨论】:

      【解决方案3】:

      保留MyData 值中的Map,其中映射键由alphabeta 唯一定义。在此示例中,我将它们连接为 Strings:

      class MyDataLookup {
          Map<String, MyData> map = new HashMap<>();
      
          public MyData put(MyData value) {
              return map.put(getKey(value), value);
          }
      
          public boolean contains(MyData value) {
              return map.containsKey(getKey(value));
          }
      
          private static String getKey(MyData value) {
              return value.alpha +"_"+ value.beta;
          }
      }
      

      由此,您可以通过修改 MyDataLookup#getKey 轻松更改包含的定义(比如也包括 foo)。

      【讨论】:

        猜你喜欢
        • 2010-09-27
        • 1970-01-01
        • 1970-01-01
        • 2012-04-28
        • 2013-03-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多