【问题标题】:Comparator and consistent with equals: How? (best practice)比较器和与equals一致:如何? (最佳实践)
【发布时间】:2016-10-06 08:40:38
【问题描述】:

我知道,为了符合 Java 的 Collections 的合同(并避免意外行为),任何提供的 Comparator 都应该与 equals 一致。

假设我有这样的人:

class Person {
  private String name;
  private String surname;
  private int age;


  public Person(String name, String surname, int age) {
     this.name = name;
     this.surname = surname;
     this.age = age;
  }

  public String getName() { return name; }
  public String getSurname() { return surname; }
  public int getAge() { return age; }
}

比较器是

class PersonComparator implements Comparator<Person> {

  @Override
  public int compareTo(Person p1, Person p2) {
    // throws NPE's
    return p1.getName().compareTo(p2.getName());
  }
}

现在我想要一些排序的集合(TreeMapSortedSet,... 无论如何),使用仅比较 Persons 名称的 Comparator。 这个 Comparator 会违反“consistent with equals”契约。但是我不想覆盖equals(Object o),因为在程序的其他部分,两个具有相同姓名、姓氏和年龄的人可能不同(就像在现实生活中一样)。

我希望在所选集合中,名称唯一标识一个人,即TreeSet&lt;Person&gt; 不能有“John Doe”和“John Smith”(同名)。

据我测试,这适用于 Java 集合的当前实现。

我的问题是:你如何“正确”地做到这一点,理想情况下不违反任何合同?如果可能的话,我想避免使用第三方库,当然我不想为了摆脱合同而自己实现数据结构。 我担心我的代码可能会在未来的 Java 版本中中断,因为它违反了合同。

【问题讨论】:

  • a Comparator 不必与 equals 一致。 It is generally the case, but &lt;i&gt;not&lt;/i&gt; strictly required that &lt;tt&gt;(compare(x, y)==0) == (x.equals(y))&lt;/tt&gt;. Generally speaking, any comparator that violates this condition should clearly indicate this fact. The recommended language is "Note: this comparator imposes orderings that are inconsistent with equals."
  • 如果你的Person 类中的所有数据都不足以清楚地识别一个人,那么你的模型可能不够用。最终,您会遇到多个 Person 实例代表 same 人的情况。因此,我建议添加一些唯一标识符并在 equals() 以及您的比较器中使用它。
  • @Thomas “这表明模型可能不足” - 谢谢,我认为这是一个很好的观点。 (我的实际用例完全不同,但我认为这一点可能仍然适用......)

标签: java collections equals


【解决方案1】:

您遇到的问题是 Set 忽略重复项。如果您说两个 Person 相同,它会将它们视为重复并删除其中一个。

return p1.getName().compareTo(p2.getName());

因此,如果您有两个名为“John”的人作为名字,他们只会在 TreeSet 中出现一次。

您需要的是比较您在 equals 中所做的相同字段,例如

int cmp = p1.getName().compareTo(p2.getName());
if (cmp == 0)
    cmp = p1.getSurname().compareTo(p2.getSurname());
if (cmp == 0)
    cmp = Integer.compare(p1.getAge() - p2.getAge());
return cmp;

这样你只会认为两个相等 == true 的人是重复的。

一种确保具有相同详细信息(但不同)的人不会被视为重复的方法是添加一个唯一的 id,可以是计数器或 UUID。

注意:合同不一定要一致,对于 BigDecimal,它并不完全一致,但在这种情况下,我认为您没有充分的理由将 John Smith 和 John Doe 视为相同人。

【讨论】:

  • 请注意,根据 OP,他想使用Object.equals(),即比较器可以为不同(即不相等)的实例返回 0。
  • 感谢 Peter,但我希望 Set 会放弃另一个 John。这是故意的。 (我的实际用例会更复杂,它是关于不重叠的时间间隔)。所以你看不出与equals不一致有什么问题吗?这是好的还是至少可以接受的做法?
  • @user2055010 从技术上讲,您可以违约,但这并不是一个好主意。您想为 John 保留哪条记录,因为不能保证它是第一个或最后一个。如果您想保留所有名字,我会使用Set&lt;String&gt; 的名字。可能是Map&lt;String, List&lt;Person&gt;&gt;
  • 顺便说一句,cmp = Integer.compare(p1.getAge() - p2.getAge()); 应该是 cmp = Integer.compare(p1.getAge(), p2.getAge());cmp = p1.getAge() - p2.getAge(); - 我觉得这里有一个错字:)
  • @Peter 事实上,我的用例是具有开始和结束时间的时间间隔。也许有不同的地图,这取决于我想要比较的内容(例如,以开始时间为关键)可能是要走的路。目前“重叠”等于“compareTo() == 0”效果很好,但是当出现新要求时,这种轻微的不准确可能会引发问题。谢谢!
【解决方案2】:

如果您使用的是 Java 8,我的建议是使用 Comparator 中的实用方法,而不是自己编写。

例如:

SortedSet<Person> people = new TreeSet<>(Comparator.comparing(Person::getName));

这将实现的所有复杂性留给内置方法。它还允许您利用各种可用方法来定义多个排序条件、处理空值等。因为这些方法是 API 的一部分,您可以确信您的代码在未来的版本中不会中断。

【讨论】:

    猜你喜欢
    • 2012-09-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多