比较与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
,它有两个字段: id
和quantity
。 id
顾名思义是对象的自然键, 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
工作得很好。 我们不关心这里的对象身份,也不想要总排序
但是,由于基于树的集合实现中的约束,必须确保他们编写的任何比较器:
- 与平等是一致的
- 提供所有可能对象的总排序