【问题标题】:compareTo and equals in PriorityQueuesPriorityQueues 中的 compareTo 和 equals
【发布时间】:2011-06-25 14:00:28
【问题描述】:

我对所有“如果 c 对 S 施加的排序与 equals 不一致,排序集(或排序映射)将表现得很奇怪”有点困惑。 Javadoc 中的警告。 我什至不确定 PriorityQueue 是我需要的...

我的情况是这样的: 我有一个带有整数时间戳和其他一些字段的类 Event。 我正在寻找一种数据结构,我可以在其中插入这些事件并按时间戳对事件进行排序。 不同的事件可以有相同的时间戳,所以 - 如果我理解正确的话 - compareTo 和 equals 会不一致。

我的第一个方法是让 Event 实现 Comparable 并像这样提供 compareTo: 公共 int compareTo(事件 e){ 返回 this.timestamp - e.getTimestamp(); }

我不明白我应该如何解决这个问题。我考虑过创建一个自定义比较器,但是在比较器的 javadoc 中也会弹出关于奇怪行为的相同警告。 我不想插入多个相等的事件实例,我只想让它们按时间戳排序。

提前感谢您的帮助:)

编辑:
我只希望事件按时间戳排序。很可能,两个不同的事件具有相同的时间戳。所以 compareTo 将返回 0,因为它们具有相同的时间戳并且在排序时是相等的。但是 equals() 不会返回 true,因为它们是不同的事件。
我不确定,使用 PriorityQueue 是否正确。我查看了 SortedSet,但它对 compareTo 和 equals 的一致性有相同的警告。
也许我从错误的角度解决这个问题,我不知道......

【问题讨论】:

  • 你能覆盖equals吗?
  • @Yet Another Geek:是的,我可以。

标签: java equals compareto


【解决方案1】:

不同的事件可以有相同的时间戳

以及按时间戳对事件进行排序

后一个要求有些不清楚。 Collection 的迭代器是否应该按排序顺序返回实例?或者如果您在循环中poll(),该集合是否应该按排序顺序返回其以前的内容?

iterator()按顺序返回元素

PriorityQueue 不是这种情况。您可以使用SortedSet,但这些要求排序顺序与equals 一致,正如您正确指出的那样,您无法实现。据我所知,JDK 中没有Collection 可以将其元素保持在排序顺序中,以便将某些元素视为相等的排序顺序。但是,您可以使用数组或ArrayList,并在更改后使用Arrays.sortCollection.sort 手动对其进行排序。如果集合很少更改,这是我会选择的方法。如果它经常变化,你将不得不超越 JDK 或自己实现数据结构。

poll() 按排序顺序返回元素

这就是优先队列的好处。 PriorityQueue 不要求Comparator(或Comparable 的实现)与equals 一致;它的 JavaDoc 清楚地写道:

这个队列的head是相对于指定顺序的最少元素。如果多个元素被绑定为最小值,则头部是这些元素之一——绑定被任意打破。

此外,JDK 6 中PriorityQueue 的实现使用equals 仅实现indexOf(E)contains(Object)remove(Object),它们都没有以任何方式使用比较器。因此,对于这个 Collection,确实没有办法与 equals 保持一致。

可比与比较器

请注意,就与 equals 的一致性而言,实现 Comparable 或 Comparator 并不重要。对于SortedSet,必须与equals一致,对于PriorityQueueCollection.sortArrays.sort,两者都不必。

TreeSet 以及与equals 的一致性

从 cmets 中解脱出来:

TreeSet 是一个 SortedSet 并明确声明只依赖 compareTo/compare。它明确表示:“集合的行为是明确定义的,即使它的顺序与 equals 不一致;它只是不遵守 Set 接口的一般约定。”

如果您引用,请引用所有相关部分。全文如下:

请注意,如果要正确实现Set 接口,集合维护的顺序(无论是否提供显式比较器)必须与equals 一致。 [...] 这是因为Set 接口是根据equals 操作定义的,但是TreeSet 实例使用其compareTo(或compare)方法执行所有元素比较,所以两个从集合的角度来看,这种方法认为相等的元素是相等的。集合 的行为是明确定义的,即使它的顺序与 equals 不一致;它只是不遵守Set 接口的一般约定。

所以是的,它是明确定义的,但它不能满足问题的要求:如果您传递 TreeSet.addEvent 与集合中另一个 Event 的时间戳相同,则新的 @987654356 @ 将被视为重复而不添加,即使 Events 不是 equal。该问题询问有关对Collection 进行排序的问题;这不应该消除重复排序键的Events,不是吗?

【讨论】:

  • TreeSet 是一个 SortedSet 并明确声明只依赖 compareTo/compare。它明确表示:“集合 的行为是明确定义的,即使它的顺序与 equals 不一致;它只是不遵守 Set 接口的一般约定。无论如何+1非常好的答案
  • @meriton:我不迭代集合,所以迭代器不需要按顺序返回元素。目前,我正在使用 peek() 和 remove() 从 PriorityQueue 中检索最少的元素。所以你的意思是,在我的情况下, compareTo 和 equals 之间的不一致不是问题吗?
  • 没错。顺便说一句,使用poll() 的效率略高于peek(),然后是remove(Object)
  • @meriton:太棒了。顺便说一句,我正在使用 peek() 和 remove(),因为我首先必须确定是否要从队列中删除头部。
  • @meriton 在谈到 SortedSet 是否是正确的数据结构时,您是对的。但是您说:“据我所知,JDK 中没有 Collection 可以将其元素保持在排序顺序中,以实现与 equals 不一致的排序顺序。”但是 TreeSet 实际上做到了。但它通过它的比较器来对待元素相等。并且一个集合不包含重复项。所以是的,在这种情况下,这是错误的数据结构。对于 PriorityQueue 的旧实现和它的 remove 方法也是如此,它通过比较器处理元素相等 - 据我所知。
【解决方案2】:

如果 c 对 S 施加的排序与 equals 不一致,则排序集(或排序图)将表现异常。

这只是意味着当且仅当e1.equals(e2) 然后e1.compareTo(e2) == 0

当且仅当!e1.equals(e2)e1.compareTo(e2) != 0

这就是使这两种方法保持一致所必须做的。

因此,通过实现 compareTo 的方式,您还应该将 equals() 覆盖为:

@Override
public boolean equals(Event e) {
    return this.timestamp.equals(e.timestamp);
}

注意:我不知道时间戳的数据类型,但如果是原始类型,请使用== 而不是equals() 作为覆盖方法。

【讨论】:

  • 应该是if and only if。所以反过来也是这样:如果e1.compareTo(e2) == 0 那么e1.equals(e2)
  • @Fatal 很好的观察,添加到答案中。
  • @Marcelo:我理解 compareTo 和 equals 保持一致意味着什么。但是,正如问题中所述,我可以有具有相同时间戳的不同事件。这些事件在 equals() 的意义上不相等,但 compareTo 将返回 0。
  • @foobar 你必须实现一个比较器然后才能不违反合同。我更新了我的答案。
  • @foobar 你能告诉我是什么决定了相等性,或者 Event 类还有哪些其他属性吗?
【解决方案3】:

当您实现 Comparable 时,您还应该覆盖 equals(Object),因为当且仅当 equals 返回 true 时,compareTo 才应该返回零。

compareTo(T) 仅当且仅当 equals(Object) 返回 true 时才应返回零。

这还不是全部。由于另一个合同,您应该/必须在覆盖 equals 时覆盖 hashCode()

相同的对象必须具有相同的哈希码。

public class Event implements Comparable<Event> {

    private long timestamp;

    public long getTimestamp() {
        return this.timestamp;
    }

    @Override
    public int compareTo(Event o) {
        return (this.timestamp < o.timestamp ? -1
                : (this.timestamp == o.timestamp ? 0 : 1));
    }

    @Override
    public int hashCode() {
        return (int) (this.timestamp ^ (this.timestamp >>> 32));
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Event) {
            return this.timestamp == ((Event) obj).timestamp;
        }

        return false;
    }

}

compareToequalshashCode 的实现取自您在 java.lang.Long 中看到的实现。您也可以通过 Eclipse 等 IDE 生成这些方法。

如果您希望在 equals 必须返回 false(如另一条评论中所述)时评估为 0 的 compareTo,那么您必须实现 Comparator 而不是实现 Comparable。

public class EventComparator implements Comparator<Event>, Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public int compare(Event o1, Event o2) {
        return (o1.getTimestamp() < o2.getTimestamp() ? -1
                : (o1.getTimestamp() == o2.getTimestamp() ? 0 : 1));
    }

}

【讨论】:

  • 但是 Comparator 上的 JavaDoc 也声明了关于不一致 compareTo 和 equals 的警告。我不确定他们所说的“奇怪行为”是什么意思,以及它是否会在我的用例中体现出来。所以也许我可以忽略警告?
  • @foobar 取决于您使用的实现。虽然Set 说你必须与equals 一致TeeSet 在类注释中明确表示它的实现只依赖于compareTo。您可以将 Comparator 提供给 TreeSet 构造函数。对于PriorityQueue,需要了解更多信息。一般来说,你应该有一致的compareToequals。但是您也可以为构造函数提供一个比较器。我不建议使用具有不一致 compareTo 和 equals 的 PriorityQueue,因为即使提供了 Comparator,实现也依赖于 compareTo 和 equals。
  • “我不建议使用具有不一致 compareTo 和 equals 的 PriorityQueue,因为即使提供了 Comparator,实现也依赖于 compareTo 和 equals。” [需要引用]
  • 你是对的,meriton。我的知识似乎已经过时了。他们将删除的实现从 Java 5 更改为 6。我将编辑我的答案。谢谢你的坚持!
猜你喜欢
  • 1970-01-01
  • 2011-10-10
  • 2021-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-19
  • 1970-01-01
相关资源
最近更新 更多