20

来自 TreeMap 的 JavaDoc:

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

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

4

4 回答 4

33

这是一个简单但现实的示例,说明如果比较方法与 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.00.00具有不同的精度,即使它们具有相同的数值,它们也不相等。(有关解释,请参阅 此答案。)

TreeSet下面是一个例子,说明 a违反 的一般合同意味着什么Set。(与TreeMapand的情况相同,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表示它包含 object 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.

现在让我们将另一个对象添加到 a 中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并使用然后调用该TreeSet.contains方法——它使用比较方法。就其TreeSet而言,它等于HashSeths2。

现在当我们做

hs2.equals(ts)

比较是相反的。我们遍历TreeSet并获取它的元素,并询问hs2它是否是contains那个元素。由于HashSet.contains使用了equals,所以它返回 false,整体结果为 false。

于 2018-12-23T06:52:45.953 回答
22

假设我们有这个简单的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)key 获取学生也可以工作并返回"twenty two"。这意味着我们可以安全地使用Studentclass in TreeMap

然而改变studentsHashMap事情变糟了:

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))

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

于 2012-08-25T17:16:42.537 回答
18

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

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

所以理论上,JDK中的一个类可能compareToequals. 一个很好的例子是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);
}
于 2012-08-25T17:09:37.780 回答
7

这是另一个示例,说明何时与 equals AND总排序的一致性很重要。

假设我们有一个MyObject具有两个字段的对象:idquantityid顾名思义,它是对象的自然键,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

完美运行。我们在这里不关心对象身份,也不想要全序

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

  • 与equals一致
  • 提供所有可能对象的总排序
于 2015-01-04T09:53:56.113 回答