比较与等数一致是什么意思?如果我的班级不遵循这个原则,会发生什么?

来自 TreeMap 的 JavaDoc :

请注意,如果排序映射要正确实现 Map 接口,则排序映射(无论是否提供显式比较器)维护的排序必须与 equals 一致。(有关与等数一致的精确定义,请参见“比较”或“比较器”。之所以如此,是因为 Map 接口是根据 equals 运算定义的,但 Map 使用其 compareTo(或 compare) 方法执行所有键比较,因此从排序映射的角度来看,此方法视为相等的两个键是相等的。排序映射的行为是明确定义的,即使它的排序与 equals 不一致;它只是不遵守Map接口的总合同。

有人能举一个具体的例子来证明如果排序与等数不一致时可能发生的问题吗?以具有自然排序的用户定义类为例,即它实现了Compable。此外,JDK 中的所有内部类是否都保持此不变量?


答案 1

下面是一个简单但现实的示例,说明如果比较方法与 equals 不一致会发生什么情况。在 JDK 中,实现了,但其比较方法与 equals 不一致。例如:BigDecimalComparable

> BigDecimal z = new BigDecimal("0.0")
> BigDecimal zz = new BigDecimal("0.00")
> z.compareTo(zz)
0
> z.equals(zz)
false

这是因为比较方法只考虑数值,还考虑精度。由于 和 具有不同的精度,即使它们具有相同的数值,它们也是不相等的。(有关说明,请参阅此答案BigDecimalequals0.00.00

下面是 一个示例,说明 a 违反 总合同意味着什么。(这与 和 的情况相同,但使用集合进行演示会容易一些。让我们将 的结果与将元素从集合中取出并调用的结果进行比较:TreeSetSetTreeMapMapcontainsequals

> 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

这里令人惊讶的是,它包含对象,但它与集合中实际包含的元素不相等。原因是使用其比较方法 () 来确定集合成员身份,而不是 。TreeSetzzTreeSetBigDecimal.compareToequals

现在让我们比较一下:TreeSetHashSet

> HashSet<BigDecimal> hs = new HashSet<>(ts)
> hs.equals(ts)
true
> ts.contains(zz)
true
> hs.contains(zz)
false

这很奇怪。我们有两个相等的集合,但一个集合说它包含一个对象,而另一个集合说它不包含相同的对象。同样,这反映了使用比较方法而使用 .TreeSetHashSetequals

现在,让我们将另一个对象添加到 中,看看会发生什么:HashSet

> HashSet<BigDecimal> hs2 = new HashSet<>()
> hs2.add(zz)
> ts.equals(hs2)
true
> hs2.equals(ts)
false

这很奇怪。一组说它等于另一组,但另一组说它不等于第一组!要理解这一点,您需要了解如何确定集合的相等性。如果 a) 两个集合具有相同的大小,并且 b) 另一个集合中的每个元素也包含在此集合中,则认为两个集合相等。也就是说,如果您有

set1.equals(set2)

然后,相等算法查看大小,然后迭代 set2,对于每个元素,它检查该元素是否包含在 set1 中。这就是不对称的由来。当我们这样做时

ts.equals(hs2)

两个集合的大小均为 1,因此我们继续执行迭代步骤。我们迭代并使用然后调用该方法 - 它使用比较方法。就而言,它等于hs2。hs2TreeSet.containsTreeSetHashSet

现在,当我们这样做时

hs2.equals(ts)

比较是相反的。我们迭代并获取其元素,并询问它是否是该元素。由于使用相等,因此返回 false,并且总体结果为 false。TreeSeths2containsHashSet.contains


答案 2

假设我们有这个简单的类实现,但没有覆盖 /。当然不一致 - 两个不同的学生具有相同的不相等:StudentComparable<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"StudentTreeMap

然而,改变和事情变坏:studentsHashMap

Map<Student, String> students = new HashMap<Student, String>();

显然,由于散列,项目的枚举返回“随机”顺序 - 这很好,它不违反任何契约。但最后一句话完全被打破了。由于使用 / 来比较实例,因此按键获取值失败并返回 !MapHashMapequals()hashCode()new Student(22)null

这就是JavaDoc试图解释的:这样的类将工作,但可能无法与其他实现一起使用。请注意,操作是用 / 来记录和定义的,例如 containsKey()TreeMapMapMapequals()hashCode()

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

因此,我不相信有任何标准的JDK类实现但未能实现/对。Comparableequals()hashCode()


推荐