从树形图的JavaDoc:
请注意,如果有序映射要正确实现Map接口,则有序映射(无论是否提供了明确的比较器)保持的顺序必须与equals一致。 (参见可比或比较用于一致的精确定义与equals)。这是因为Map接口是按照equals操作定义的,但地图使用其的compareTo执行所有关键比(或比较)方法,所以两个键被认为等于通过该方法是,从有序映射,相等的观点出发。 有序映射的行为是明确的,即使它的排序和equals不一致; 它只不过没有遵守Map接口的常规协定。
有些人能给出一个具体的例子来demonsrate如果排序不与equals一致时可能发生的问题? 就拿具有自然排序,即它实现可比例如用户定义的类。 也不要在JDK所有内部类保持不变式?
在可比的接口契约允许非一致的行为:
强烈推荐(虽然不是必需的)使自然排序与equals一致。
因此,在理论上,它有可能在JDK类有一个compareTo
有不一致equals
。 一个很好的例子是BigDecimal的 。
下面是一个比较,是不是与equals一致的一个人为的例子(它基本上说,所有字符串相等)。
输出:
尺寸:1
内容:{A = B}
public static void main(String[] args) {
Map<String, String> brokenMap = new TreeMap<String, String> (new Comparator<String>() {
@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<Student>
但不重写equals()
/ hashCode()
。 当然equals()
是不是一致compareTo()
-两个不同的学生有相同的age
是不相等的:
class Student implements Comparable<Student> {
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<Student, String>
:
Map<Student, String> students = new TreeMap<Student, String>();
students.put(new Student(25), "twenty five");
students.put(new Student(22), "twenty two");
students.put(new Student(26), "twenty six");
for (Map.Entry<Student, String> entry : students.entrySet()) {
System.out.println(entry);
}
System.out.println(students.get(new Student(22)));
结果很容易预测:学生根据自己的年龄很好排序(尽管在不同的顺序插入),并利用学生获取new Student(22)
重点工作以及返回"twenty two"
。 这意味着我们可以放心地使用Student
类TreeMap
。
然而改变students
到HashMap
,事情变坏:
Map<Student, String> students = new HashMap<Student, String>();
显然,项目的枚举返回“随机”为了因哈希-这很好,这并不违反任何Map
的合同。 但最后的说法是完全破碎。 因为HashMap
使用equals()
/ hashCode()
来比较的情况下,通过获取价值new Student(22)
键失败并返回null
!
这是什么的JavaDoc试图解释:这样的类将与TreeMap
,但可能无法与其他工作Map
的实现。 需要注意的是Map
操作记录,并规定在以下方面equals()
/ hashCode()
,如containsKey()
[...]当且仅当此映射包含键k的映射返回true,使得(key==null ? k==null : key.equals(k))
因此,我不相信,有一些implemente任何标准JDK类Comparable
,但未能实现equals()
/ hashCode()
对。
下面是如果比较方法与equals不一致,会发生什么简单的,但现实的例子。 在JDK, BigDecimal
实现Comparable
,但它的比较方法与equals不一致。 例如:
> BigDecimal z = new BigDecimal("0.0")
> BigDecimal zz = new BigDecimal("0.00")
> z.compareTo(zz)
0
> z.equals(zz)
false
这是因为比较法BigDecimal
只考虑数字值,但equals
还考虑了精度。 由于0.0
和0.00
有不同的精度,他们即使它们具有相同的数值不等。
下面是什么意思了一个例子TreeSet
违反的总承包Set
。 (这是用相同的情况TreeMap
和Map
,但它是一个有点容易使用套来证明。)让我们比较的结果contains
以获得元素进行设置和调用的结果equals
:
> TreeSet<BigDecimal> ts = new TreeSet<>()
> ts.add(z)
> ts.contains(z)
true
> z.equals(ts.iterator().next())
true
> ts.contains(zz)
true
> zz.equals(ts.iterator().next())
false
这里令人惊讶的是, TreeSet
说,它包含对象zz
,但它不等于这实际上是包含在集合的元素。 其原因是, TreeSet
使用它的比较方法( BigDecimal.compareTo
)来确定集合成员资格,而不是equals
。
现在让我们来比较TreeSet
给HashSet
:
> HashSet<BigDecimal> hs = new HashSet<>(ts)
> hs.equals(ts)
true
> ts.contains(zz)
true
> hs.contains(zz)
false
这很奇怪。 我们有两套是平等的,但一组说,它包含一个对象,但另一组说,它不包含相同的对象。 同样,这反映了这样的事实TreeSet
被而使用的比较方法HashSet
使用equals
。
现在,让我们的其他对象添加到HashSet
,看看会发生什么:
> HashSet<BigDecimal> hs2 = new HashSet<>()
> hs2.add(zz)
> ts.equals(hs2)
true
> hs2.equals(ts)
false
这才叫奇怪。 一组说,这等于给对方,但另一组说,这不等于第一! 要理解这一点,你需要了解的套平等确定。 两组被认为是相等如果一个)它们是相同大小的,和b)在另一集合中的每个元素也包含在这个组。 也就是说,如果你有
set1.equals(set2)
然后平等算法着眼于尺寸,然后将它遍历SET2,并且对于每个元件它检查该元素是否包含在集1。 这就是不对称的用武之地。当我们这样做
ts.equals(hs2)
这两套都是大小为1,所以我们进行了迭代步骤。 我们遍历hs2
和使用,然后调用TreeSet.contains
方法- 它使用的比较方法 。 至于TreeSet
来讲,它是等于HashSet
HS2。
现在,当我们做
hs2.equals(ts)
比较走另一条路。 我们遍历TreeSet
,并得到其元素,并询问hs2
是否contains
该元素。 由于HashSet.contains
使用等号 ,则返回false,并且总的结果是假的。
下面是当与平等的,总排序的一致性是实现重要的另一个例子。
假设我们有一个对象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;
}
};
使用MyObject
情况下配备该比较器在一个TreeMap,因为它比较不与equals(参见下面的完整代码) 一致 / TreeSet中失败。 让我们把它与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
完美的作品。 我们不关心对象的身份在这里,我们也希望全序
然而,由于基于树的集合实现约束,就必须确保任何比较他们写道:
文章来源: What does comparison being consistent with equals mean ? What can possibly happen if my class doesn't follow this principle?