比较与equals一致意味着什么? 如果我的class级不遵循这个原则,可能会发生什么?

从TreeMap的JavaDoc:

请注意,如果此有序映射要正确实现Map接口,则由有序映射维护的排序(无论是否提供显式比较器)必须与equals一致。 (请参阅Comparable或Comparator以获得与equals一致的精确定义。)这是因为Map接口是根据equals操作定义的,但是map使用compareTo(或compare)方法执行所有键比较,因此两个键从排序地图的角度来看,通过这种方法被视为相等的是相等的。 即使排序与equals不一致,也可以很好地定义有序映射的行为。 它只是没有遵守Map接口的一般合同。

有人可以给出一个具体的例子来说明如果排序与equals不一致可能会出现的问题吗? 举例来说,用户定义的类具有自然顺序,即它实现了Comparable。 JDK中的所有内部类都保持这个不变量吗?

Comparable接口的契约允许不一致的行为:

强烈建议(尽管不是必需的)自然排序与equals一致。

所以从理论上讲,JDK中的类可能有一个与equals不一致的equals . 一个很好的例子是BigDecimal 。

下面是一个与equals不一致的比较器的设计示例(它基本上表示所有字符串都相等)。

输出:

大小:1
内容:{a = b}

 public static void main(String[] args) { Map brokenMap = new TreeMap (new Comparator() { @Override public int compare(String o1, String o2) { return 0; } }); brokenMap.put("a", "a"); brokenMap.put("b", "b"); System.out.println("size: " + brokenMap.size()); System.out.println("content: " + brokenMap); } 

假设我们有这个简单的Student类实现Comparable但不覆盖equals() / hashCode() 。 当然equals()compareTo()不一致 – 两个age相同的学生不相等:

 class Student implements Comparable { private final int age; Student(int age) { this.age = age; } @Override public int compareTo(Student o) { return this.age - o.age; } @Override public String toString() { return "Student(" + age + ")"; } } 

我们可以在TreeMap安全地使用它:

 Map students = new TreeMap(); students.put(new Student(25), "twenty five"); students.put(new Student(22), "twenty two"); students.put(new Student(26), "twenty six"); for (Map.Entry entry : students.entrySet()) { System.out.println(entry); } System.out.println(students.get(new Student(22))); 

结果很容易预测:学生根据他们的年龄进行了很好的分类(尽管按照不同的顺序插入),并且使用new Student(22)关键作品取得学生并返回"twenty two" 。 这意味着我们可以在TreeMap安全地使用Student类。

然而,将students改为HashMap并且事情变得糟糕:

 Map students = new HashMap(); 

显然,由于散列,项目的枚举返回“随机”顺序 – 这很好,它不违反任何Map合同。 但最后一句话完全被打破了。 因为HashMap使用equals() / hashCode()来比较实例,所以通过new Student(22)键获取值失败并返回null

这就是JavaDoc试图解释的内容:这些类可以与TreeMap但可能无法与其他Map实现一起使用。 请注意, Map操作是以equals() / hashCode()forms记录和定义的,例如containsKey()

[…]当且仅当此映射包含键k的映射时才返回true (key==null ? k==null : key.equals(k))

因此,我不相信有任何标准的JDK类实现Comparable但无法实现equals() / hashCode()对。

下面是另一个与equals 和AND排序的一致性实现重要的例子。

假设我们有一个对象MyObject ,它有两个字段: idquantityid顾名思义是对象的自然键, quantity只是一个属性。

 public class MyObject { int id; int quantity; ... } 

让我们假设我们想要使用按quantity递减排序的MyObject集合。 我们可以写的第一个比较器是:

 Comparator naiveComp = new Comparator() { @Override public int compare(MyObject o1, MyObject o2) { return o2.quantity - o1.quantity; } }; 

在TreeMap / TreeSet中使用配备此比较器的MyObject实例会失败,因为它的比较器与equals不一致 (请参阅下面的完整代码)。 让它与equals保持一致:

 Comparator slightlyBetterComp = new Comparator() { @Override public int compare(MyObject o1, MyObject o2) { if (o1.equals(o2)) { return 0; } if (o1.quantity == o2.quantity) { return -1; // never 0 } return o2.quantity - o1.quantity; // never 0 } }; 

但是,这再次失败以适应TreeSet / TreeMap! (参见下面的完整代码)这是因为排序关系不是完全的 ,即不能将任何两个对象严格地置于排序关系中。 在此比较器中,当quantity字段相等时,结果排序未确定。

一个更好的比较器是:

 Comparator betterComp = new Comparator() { @Override public int compare(MyObject o1, MyObject o2) { if (o1.equals(o2)) { return 0; } if (o1.quantity == o2.quantity) { return o1.id - o2.id; // never 0 } return o2.quantity - o1.quantity; // never 0 } }; 

该比较器确保:

  • 当compareTo返回0时,它意味着两个对象equal (初始检查是否相等)
  • quantity相等时,所有项目都通过使用id作为判别式排序字段来完全排序

完整测试代码:

 package treemap; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; public class MyObject { int id; int quantity; public MyObject(int id, int quantity) { this.id = id; this.quantity = quantity; } @Override public int hashCode() { int hash = 7; hash = 97 * hash + this.id; return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final MyObject other = (MyObject) obj; if (this.id != other.id) { return false; } return true; } @Override public String toString() { return "{" + id + ", " + quantity + "}"; } public static void main(String[] args) { String format = "%30.30s: %s\n"; Map map = new HashMap(); map.put(new MyObject(1, 100), 0); map.put(new MyObject(2, 100), 0); map.put(new MyObject(3, 200), 0); map.put(new MyObject(4, 100), 0); map.put(new MyObject(5, 500), 0); System.out.printf(format, "Random Order", map.keySet()); // Naive non-consisten-with-equal and non-total comparator Comparator naiveComp = new Comparator() { @Override public int compare(MyObject o1, MyObject o2) { return o2.quantity - o1.quantity; } }; Map badMap = new TreeMap(naiveComp); badMap.putAll(map); System.out.printf(format, "Non Consistent and Non Total", badMap.keySet()); // Better consisten-with-equal but non-total comparator Comparator slightlyBetterComp = new Comparator() { @Override public int compare(MyObject o1, MyObject o2) { if (o1.equals(o2)) { return 0; } if (o1.quantity == o2.quantity) { return -1; // never 0 } return o2.quantity - o1.quantity; // never 0 } }; Map slightlyBetterMap = new TreeMap(naiveComp); slightlyBetterMap.putAll(map); System.out.printf(format, "Non Consistent but Total", slightlyBetterMap.keySet()); // Consistent with equal AND total comparator Comparator betterComp = new Comparator() { @Override public int compare(MyObject o1, MyObject o2) { if (o1.equals(o2)) { return 0; } if (o1.quantity == o2.quantity) { return o1.id - o2.id; // never 0 } return o2.quantity - o1.quantity; // never 0 } }; Map betterMap = new TreeMap(betterComp); betterMap.putAll(map); System.out.printf(format, "Consistent and Total", betterMap.keySet()); } } 

输出:

  Random Order: [{5, 500}, {4, 100}, {3, 200}, {2, 100}, {1, 100}] Non Consistent and Non Total: [{5, 500}, {3, 200}, {4, 100}] Consistent but Not Total: [{5, 500}, {3, 200}, {4, 100}] Consistent and Total: [{5, 500}, {3, 200}, {1, 100}, {2, 100}, {4, 100}] 

结论:

虽然我认为从概念上将身份与排序分开是非常合理的。 例如,在关系数据库术语中:

 select * from MyObjects order by quantity 

工作得很好。 我们不关心这里的对象身份,也不想要总排序

但是,由于基于树的集合实现中的约束,必须确保他们编写的任何比较器:

  • 与平等是一致的
  • 提供所有可能对象的总排序