【问题标题】:Comparator and equals()比较器和 equals()
【发布时间】:2010-12-03 09:44:28
【问题描述】:

假设我需要TreeSet,其中的元素按一些域逻辑排序。按照这种逻辑,一些不相等的元素的顺序无关紧要,因此 compare 方法可以返回 0,但在这种情况下,我无法将它们放入 TreeSet

那么,问题:这样的代码有什么缺点:

class Foo implements Comparable<Foo>{}
new TreeSet<Foo>(new Comparator<Foo>(){
    @Override
    public int compare(Foo o1, Foo o2) {
        int res = o1.compareTo(o2);
        if(res == 0 || !o1.equals(o2)){
            return o1.hashCode() - o2.hashCode();
        }
        return res;
    }
});

更新

好的。如果 equals()hashcode()compareTo() 方法之间应该始终保持一致,就像@S.P.Floyd - seanizer 和其他人所说的那样。 如果我删除Comparable 接口并将这个逻辑移到Comparator 中会更好甚至更好(我可以在不破坏封装的情况下做到这一点)?所以它会是:

class Foo{}
new TreeSet<Foo>(new Comparator<Foo>(){
    @Override
    public int compare(Foo o1, Foo o2) {
        //some logic start
        if(strictliBigger(o1, o2)){ return 1;}
        if(strictliBigger(o2, o1)){ return -1;}
        //some logic end
        if(res == 0 || !o1.equals(o2)){
            return o1.hashCode() - o2.hashCode();
        }
        return res;
    }
});

更新 2

如果我不需要稳定排序,System.identityHashCode(x) 会比 hashCode() 更好吗?

【问题讨论】:

  • 那么,您想编写自己的比较器来对树集中的结构排序吗?
  • strictliBigger 之后的返回值之一应该是 -1
  • @S.P.Floyd-seanizer,谢谢。固定。

标签: java equals comparator treeset


【解决方案1】:

虽然这可能有效,但远非最佳做法。

来自SortedSet docs

请注意,排序集维护的排序(无论是否提供显式比较器)如果排序集要正确实现 Set 接口,则必须与 equals 保持一致强>。 (参见Comparable 接口或Comparator 接口,了解与equals 一致的精确定义。)这是因为Set 接口是根据equals 操作定义的,但是有序集合使用其compareTo (或 compare) 方法,因此从排序集的角度来看,此方法认为相等的两个元素是相等的。一个有序集合的行为是明确定义的,即使它的排序与equals不一致;它只是不遵守 Set 接口的一般约定。

对于实现Comparable 的对象,方法equals()hashcode()compareTo() 之间应该始终保持一致。


恐怕SortedSet 不是您想要的,Guava MultiSet 也不够用(因为它不会让您独立检索多个相等的项目)。我认为您需要的是SortedList。我所知道的没有这样的野兽(可能在 commons-collections 中,但这些有点在遗留方面),所以我使用 Guava 的 ForwardingList 作为基类为您实现了一个。简而言之:这个 List 将几乎所有东西都委托给它在内部使用的 ArrayList,但它在其 add() 方法中使用 Collections.binarySearch() 来找到正确的插入位置,并在 @987654336 的所有可选方法上抛出 UnsupportedOperationException @ 和 ListIterator 在给定位置添加或设置值的接口。

构造函数与ArrayList 的构造函数相同,但每个构造函数还有一个带有自定义Comparator 的第二个版本。如果你不使用自定义的比较器,你的列表元素需要实现ComparableRuntimeExceptions 会在排序过程中发生。

public class SortedArrayList<E> extends ForwardingList<E> implements
    RandomAccess{

    private final class ListIteratorImpl extends ForwardingListIterator<E>{
        private final int start;
        public ListIteratorImpl(final int start){
            this.start = start;
        }

        @Override
        public void set(E element){throw new UnsupportedOperationException();}

        @Override
        public void add(E element){throw new UnsupportedOperationException();}

        @Override
        protected ListIterator<E> delegate(){return inner.listIterator(start);};

    }

    private Comparator<? super E> comparator;

    private List<E> inner;

    public SortedArrayList(){this(null, null, null);}

    @SuppressWarnings("unchecked")
    private SortedArrayList(
        final List<E> existing,
        final Collection<? extends E> values,
        final Comparator<? super E> comparator
    ){
        this.comparator =
            (Comparator<? super E>)
               (comparator == null
                   ? Ordering.natural()
                   : comparator   );
        inner = (
            existing == null
                ? (values == null
                      ? new ArrayList<E>(values)
                      : new ArrayList<E>()
                   )
                : existing;
    }

    public SortedArrayList(final Collection<? extends E> c){
        this(null, c, null);
    }

    public SortedArrayList(final Collection<? extends E> c,
        final Comparator<? super E> comparator){
        this(null, c, comparator);
    }

    public SortedArrayList(final Comparator<? super E> comparator){
        this(null, null, comparator);
    }

    public SortedArrayList(final int initialCapacity){
        this(new ArrayList<E>(initialCapacity), null, null);
    }

    public SortedArrayList(final int initialCapacity,
        final Comparator<? super E> comparator){
        this(new ArrayList<E>(initialCapacity), null, comparator);
    }

    @Override
    public boolean add(final E e){
        inner.add(
            Math.abs(
                Collections.binarySearch(inner, e, comparator)
            ) + 1,
            e
        );
        return true;
    }

    @Override
    public void add(int i, E e){throw new UnsupportedOperationException();}

    @Override
    public boolean addAll(final Collection<? extends E> collection){
        return standardAddAll(collection);
    }

    @Override
    public boolean addAll(int i,
        Collection<? extends E> es){
        throw new UnsupportedOperationException();
    }

    @Override
    protected List<E> delegate(){ return inner; }

    @Override
    public List<E> subList(final int fromIndex, final int toIndex){
        return new SortedArrayList<E>(
            inner.subList(fromIndex, toIndex),
            null,
            comparator
        );
    }

    @Override
    public ListIterator<E> listIterator(){ return new ListIteratorImpl(0); }

    @Override
    public ListIterator<E> listIterator(final int index){
        return new ListIteratorImpl(index);
    }

    @Override
    public E set(int i, E e){ throw new UnsupportedOperationException(); }

}

【讨论】:

  • 我更新了我的问题。如果将所有比较逻辑移动到比较器会更好,这样就会有这样的一致性。
  • 我更新了我的答案。 SortedSet 可能不适合您。
  • @seanizer:你真好。 +1
【解决方案2】:

注意:即使是两个 Foos f1,f2f1 != f2,你也可以得到 f1.hashCode() == f2.hashCode()!这意味着您不会使用 compare 方法获得稳定的排序。

【讨论】:

  • +1 表示两个哈希码可以相同,-1 因为如果 compare 返回 0,则认为两个对象相同(即使 equals() 返回 false)。
【解决方案3】:

Java 中没有规则规定两个对象的哈希码必须不同,因为它们不相等(因此 o1.hashCode() - o2.hashCode() 在您的情况下可以返回 0)。

同样equals() 的行为应该compareTo() 的结果一致。这不是必须,但如果你不能保持这一点,则表明你的设计存在很大缺陷。

我强烈建议查看对象的其他字段并使用其中一些字段来扩展您的比较,这样您就可以得到 != 0 的值,因为对象是 equals() == false

【讨论】:

  • 逻辑有缺陷;放在哪里都没关系。
【解决方案4】:

hashcode() 方法不保证任何less thangreater thancompare()equals() 应该产生相同的含义,但不是必须的。

据我所知,您的代码令人困惑(无意冒犯:)),您想将重复项添加到TreeSet。出于这个原因,你想出了这个实现。这就是原因,您不能将它们放在 TreeSet 中,引用自文档,

集合的行为是明确定义的 即使它的顺序不一致 等于;它只是不服从 Set 接口的通用合约。

所以,你需要用你的equals() 方法做一些事情,所以它永远不会返回真正的东西。最好的实现是,

public boolean equals(Object o) {
    return false;
}

顺便说一句,如果我的理解是正确的,为什么不改用List 并对其进行排序。

【讨论】:

    【解决方案5】:

    非常有趣的问题。 据我了解,您的问题是重复元素。

    我认为如果 o1.equals(o2) 它们的哈希码也可能相等。这取决于 Foo 类中 hashCode() 的实现。所以,我建议你改用 System.identityHashCode(x) 。

    【讨论】:

    • 如果 o1.equals(o2) 他们可以算作重复。
    【解决方案6】:

    您有一个具有可比性的 Foo 类,但希望在 TreeSet&lt;Foo&gt; 结构中使用不同的排序。那么你的想法就是正确的方法。使用该构造函数“否决”Foo 的自然排序。

    【讨论】:

    • 我可以将此逻辑移至比较器。会更好吗?我更新了我的问题。
    【解决方案7】:

    如果您对任何两个给定元素没有特定的预期顺序,但仍想认为它们不相等,那么无论如何您都必须返回一些指定的顺序。

    正如其他人所发布的,hashCode() 不是一个好的候选者,因为两个元素的 hashCode() 值很容易相等。 System.identityHashCode() 可能是更好的选择,但仍然不是完美的,因为即使 identityHashCode() 也不保证唯一值

    Guava arbitrary() Ordering 使用 System.identityHashCode() 实现 Comparator

    【讨论】:

      【解决方案8】:

      是的,正如上面其他人所说,在这里使用 hashCode() 并不安全。但是,如果您不关心 o1.compareTo(o2) == 0 方面相等的对象的顺序,您可以执行以下操作:

      public int compare(Foo o1, Foo o2) {
              int res = o1.compareTo(o2);
              if (res == 0 && !o1.equals(o2)) {
                  return -1;
              }
              return res;
      }
      

      【讨论】:

        【解决方案9】:
        int res = o1.compareTo(o2);
        
        if(res == 0 || !o1.equals(o2)){
            return o1.hashCode() - o2.hashCode();
        }
        

        可能会有问题,因为如果 2 个对象相等(即在您的 res == 0 中),那么这 2 个对象返回相同的哈希码。每个对象的哈希码都不是唯一的。


        编辑 @Stas,System.identityHashCode(Object x); 仍然无法帮助您。原因在javadoc上描述:

        返回相同的哈希码 给定的对象将由 默认方法hashCode(), 无论给定对象的 类覆盖hashCode()。哈希 空引用的代码为零。

        【讨论】:

        • System.identityHashCode() 会救我吗?
        • @精英绅士,我不明白。如果有两个不同的对象,它们的默认 hashCode() 很有可能会不同。
        • 这只是意味着System.identityHashCode()返回(x.hashCode())。因此,就您而言,使用o1.hashCode()System.identityHashCode(o1) 没有区别。
        • @Stas,您最好阅读 Javadocs,因为它有时会回答您的问题。
        【解决方案10】:

        这里有几个问题:

        • 哈希码通常不是唯一的,尤其是 System.identityHashCode 在现代 JVM 上不会是唯一的。

        • 这不是稳定性问题。我们正在对数组进行排序,但是创建了一个树结构。哈希码冲突将导致compare 返回零,这对于TreeSet 意味着一个对象获胜而另一个被丢弃 - 它不会降级为链接列表(线索名称中有“Set”)。

        • 从另一个哈希码中减去一个哈希码通常会出现整数溢出问题。这意味着比较不会是传递的(即它被破坏了)。幸运的是,在 Sun/Oracle 实现中,System.identityHashCode 总是返回正值。这意味着广泛的测试可能不会发现这种特殊类型的错误。

        我不认为有使用TreeSet 实现此目的的好方法。

        【讨论】:

          【解决方案11】:

          有两点可能是相关的,其中一种情况下的返回值显示为 -1,这取决于函数参数变量或相关使用国家/地区是否允许负值,以及您使用的方法是否允许正在使用是允许的。有标准的数据排列方法,例如选择器或选择排序,如果您的工作场所没有副本,通常可以从国家权威机构获得论文描述或代码。使用大于或小于之类的比较可以加快代码速度,并避免使用直接比较来进行相等性,隐含地直接插入到后面的脚本或代码中。

          【讨论】:

            猜你喜欢
            • 2023-03-15
            • 2020-05-28
            • 1970-01-01
            • 2014-08-07
            • 2013-02-16
            • 2010-12-17
            • 2016-02-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多