这是另一个示例,说明何时与 equals AND总排序的一致性很重要。
假设我们有一个MyObject
具有两个字段的对象:id
和quantity
。
id
顾名思义,它是对象的自然键,quantity
只是一个属性。
public class MyObject {
int id;
int quantity;
...
}
假设我们要使用按降序MyObject
排序的集合。quantity
我们可以写的第一个比较器是:
Comparator<MyObject> naiveComp = new Comparator<MyObject>() {
@Override
public int compare(MyObject o1, MyObject o2) {
return o2.quantity - o1.quantity;
}
};
在 TreeMap/TreeSet 中使用MyObject
配备此比较器的实例会失败,因为它的比较器与 equals 不一致(请参阅下面的完整代码)。让我们让它与equals一致:
Comparator<MyObject> slightlyBetterComp = new Comparator<MyObject>() {
@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<MyObject> betterComp = new Comparator<MyObject>() {
@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
(初始检查是否相等)
- 当相等时,所有项目都通过
id
用作判别排序字段来完全排序quantity
完整的测试代码:
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<MyObject, Object> 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<MyObject> naiveComp = new Comparator<MyObject>() {
@Override
public int compare(MyObject o1, MyObject o2) {
return o2.quantity - o1.quantity;
}
};
Map<MyObject, Object> 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<MyObject> slightlyBetterComp = new Comparator<MyObject>() {
@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<MyObject, Object> slightlyBetterMap = new TreeMap(naiveComp);
slightlyBetterMap.putAll(map);
System.out.printf(format, "Non Consistent but Total", slightlyBetterMap.keySet());
// Consistent with equal AND total comparator
Comparator<MyObject> betterComp = new Comparator<MyObject>() {
@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<MyObject, Object> 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
完美运行。我们在这里不关心对象身份,也不想要全序
但是,由于基于树的集合实现的限制,必须确保他们编写的任何比较器: