使用 Comparator 而不是 equals() 来比较两个 Java 集合。

21

问题陈述

我有两个相同类型的对象集合,我想进行比较。在这种情况下,我想根据一个属性进行比较,该属性不考虑对象的equals()方法。在我的例子中,我使用了排名的名称集合。

public class Name {
    private String name;
    private int weightedRank;

    //getters & setters

    @Override
    public boolean equals(Object obj) {
        return this.name.equals(obj.name); //Naive implementation just to show
                                           //equals is based on the name field.
    }
}

我想比较这两个集合来断言,在每个集合的位置i,该位置上每个名称的weightedRank值都相同。我搜索了一些资料,但在Commons Collections或任何其他API中都没有找到合适的方法,所以我想出了以下方法:

public <T> boolean comparatorEquals(Collection<T> col1, Collection<T> col2,
        Comparator<T> c)
{
    if (col1 == null)
        return col2 == null;
    if (col2 == null) 
        return false;

    if (col1.size() != col2.size())
        return false;

    Iterator<T> i1 = col1.iterator(), i2 = col2.iterator();

    while(i1.hasNext() && i2.hasNext()) {
        if (c.compare(i1.next(), i2.next()) != 0) {
            return false;
        }
    }

    return true;
}

问题

还有其他方法可以做到这一点吗?我是否错过了Commons Collections中的明显方法?

相关

我还在SO上发现了this question,虽然在那种情况下,我认为覆盖equals()更有意义。

编辑

类似于此的内容将很快进入Apache Commons Collections的发布版本(在撰写本文时)。请参见https://issues.apache.org/jira/browse/COLLECTIONS-446


我认为你的方法是最佳的。它非常干净且易懂。 - Mikita Belahlazau
由于带有此评论的答案已被删除:我选择了Comparator而不是其他一些接口,因为它是一个众所周知的接口,用于自定义对象比较,超出了equals()和hashCode()。自定义接口同样适用,但我不想“重复造轮子”。 - Matt Lachman
相关链接:https://issues.apache.org/jira/browse/COLLECTIONS-242 - Matt Lachman
三年后!只是想问一下,你是否知道:这个Equator的东西(非常棒)似乎正在半实现中。我特别考虑了collections4和具体的CollectionUtils:从我查看源代码来看,底部的方法使用了Equator,但是顶部的更强大的方法(如disjunction等)没有使用...你知道是否有人打算完成这项工作吗? - mike rodent
@mikerodent 是的,这个问题中描述的功能确实被纳入到了Collections 4中。我没有请求将该功能添加到disjunction中(也没有在那时需要它),所以我怀疑没有人这样做过。就像我一样,随时可以向Commons Collections项目提交请求。 :) - Matt Lachman
谢谢。是的,经过思考,实际上这并不容易!CollectionUtils中当前的CardinalityHelper类依赖于使用Objects自己的equals方法。当您查看Collection.add()时,例如,您会发现这不是由AbstractCollection甚至不是由AbstractSetAbstractList实现的:您必须等待具体类(例如ArrayList)。因此,将Equator纳入其中将意味着对CollectionUtils进行大规模的重写。可惜,因为disjunctionintersection是该类中最棒的东西。 - mike rodent
4个回答

6
您可以使用Guava Equivalence类来分离“比较”和“等价”的概念。您仍需要编写比较方法(据我所知,Guava没有此功能),该方法接受Equivalence子类而不是Comparator,但至少您的代码会更清晰,您可以根据任何等价标准比较集合。
使用一组包装了等价对象的集合(请参见Equivalence中的wrap方法)类似于sharakan提出的基于适配器的解决方案,但是等价实现将与适配器实现分离,使您能够轻松使用多个等价标准。

5
您可以使用自版本4以来添加到CollectionUtils的新isEqualCollection方法。该方法使用由Equator接口实现提供的外部比较机制。请查看此javadocs:CollectionUtils.isEqualCollection(...)Equator

1
是的,这是由于这篇 Stack Overflow 帖子的直接结果。我在上面提供了链接,但我也会在这里放一个链接:https://issues.apache.org/jira/browse/COLLECTIONS-446 - Matt Lachman

1
我不确定这种方法是否真的更好,但它是“另一种方法”...
取您最初的两个集合,并创建包含每个基本对象适配器的新集合。 适配器应该实现.equals().hashCode(),并基于Name.calculateWeightedRank()。 然后,您可以使用普通的集合相等性来比较适配器的集合。
* 编辑 *
使用Eclipse的标准hashCode / equals生成适配器。 您的代码将只调用adaptCollection,然后List.equals()两个结果。
public class Adapter {

    public List<Adapter> adaptCollection(List<Name> names) {
        List<Adapter> adapters = new ArrayList<Adapter>(names.size());

        for (Name name : names) {
            adapters.add(new Adapter(name));
        }

        return adapters;
    }


    private final int name;

    public Adapter(Name name) {
        this.name = name.getWeightedResult();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + name;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Adapter other = (Adapter) obj;
        if (name != other.name)
            return false;
        return true;
    }

}

我大部分能理解你的意思,但如果有更多的代码示例会更好。 - Matt Lachman
1
你能否做同样的事情并为集合创建一个包装器,而不是Name对象? - nattyddubbs
@nattyddubbs 当然,这是提高性能的一种方式。但是代价是代码会变得更加复杂,因为涉及到所有这些委托方法... - sharakan
我认为这是另一种不错的方法,但我可能还会坚持使用我的实现(因为我有偏见;-))。但是,这确实符合我的问题的标准,“还有其他方式吗?” 如果接下来的几天没有其他合适的答案,我将接受这个作为答案。 - Matt Lachman
是的,就像我说的,我不确定这种方式是否真的更好...如果有一个等效于Comparable的接口(EqualizableEqualizer?),可以用它来覆盖使用.equals(),那就太好了,但我从未听说过Java中有这样的接口。 - sharakan
在Commons Collections 4.0版本中有一个叫做Equator的东西,我在发布问题后发现了它。链接在此评论中:https://dev59.com/0c8i0IgBFxS5KdRjg-SE#UosHoYgBc1ULPQZFVby0 - Matt Lachman

0

编辑:移除旧答案。

你还有另一个选项,就是创建一个名为 Weighted 的接口,它可能是这个样子的:

public interface Weighted {
    int getWeightedRank();
}

然后让你的Name类实现这个接口。然后你可以将你的方法改成这样:

 public <T extends Weighted> boolean weightedEquals(Collection<T> col1, Collection<T> col2)
{
    if (col1 == null)
      return col2 == null;
     if (col2 == null) 
      return false;

  if (col1.size() != col2.size())
      return false;

  Iterator<T> i1 = col1.iterator(), i2 = col2.iterator();

  while(i1.hasNext() && i2.hasNext()) {
      if (i1.next().getWeightedRank() != i2.next().getWeightedRank()) {
          return false;
      }
  }

  return true;
}

然后,当您发现需要加权和比较的其他类时,您可以将它们放入您的集合中,并且它们也可以相互比较。这只是一个想法。


是的,但重点在于Comparator正在比较未被用于确定“普通”对象相等性(通过equals()hashCode())的值。 - Matt Lachman
我明白了,我会继续深入挖掘的 :-) - nattyddubbs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接