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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/12123960/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-31 07:40:19  来源:igfitidea点击:

What does comparison being consistent with equals mean ? What can possibly happen if my class doesn't follow this principle?

javacomparisonequalscomparatorcomparable

提问by Geek

From the JavaDoc of TreeMap :

来自 TreeMap 的 JavaDoc :

Note that the ordering maintained by a sorted map (whether or not an explicit comparator is provided) must be consistent with equals if this sorted map is to correctly implement the Map interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Map interface is defined in terms of the equals operation, but a map performs all key comparisons using its compareTo (or compare) method, so two keys that are deemed equal by this method are, from the standpoint of the sorted map, equal. The behavior of a sorted map is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Map interface.

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

Can some one give an concrete example to demonsrate the problem that might occur if ordering is not consistent with equals ? Take for example User defined class that has a natural ordering i.e it implements Comparable . Also do all internal classes in JDK maintain this invariant?

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

采纳答案by assylias

The contract of the Comparable interfaceallows for non-consistent behaviour:

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

It is strongly recommended (though not required) that natural orderings be consistent with equals.

强烈建议(虽然不是必需的)自然顺序与 equals 一致。

So in theory, it is possible that a class in the JDK had a compareTonot consistent with equals. One good example is BigDecimal.

所以理论上,JDK 中的一个类有可能compareToequals. 一个很好的例子是BigDecimal

Below is a contrived example of a comparator that is not consistent with equals (it basically says that all strings are equal).

下面是一个与 equals 不一致的比较器的人为示例(它基本上表示所有字符串都相等)。

Output:

输出:

size: 1
content: {a=b}

大小: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);
}

回答by Stuart Marks

Here's a simple but realistic example of what can happen if a comparison method is inconsistent with equals. In the JDK, BigDecimalimplements Comparablebut its comparison method is inconsistent with equals. For example:

这是一个简单但实​​际的示例,说明如果比较方法与 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

This is because the comparison method of BigDecimalconsiders only the numeric value, but equalsalso considers the precision. Since 0.0and 0.00have different precisions, they are unequal even though they have the same numeric value.

这是因为比较方法BigDecimal只考虑数值,equals也考虑精度。由于0.00.00具有不同的精度,因此即使它们具有相同的数值,它们也不相等。

Here's an example of what it means for a TreeSetto violate the general contract of Set. (It's the same situation with TreeMapand Mapbut it's a bit easier to demonstrate using sets.) Let's compare the results of containsto the result of getting the element out of the set and calling equals:

下面是一个例子,说明 aTreeSet违反 的一般合同意味着什么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

The surprising thing here is that the TreeSetsays it contains the object zz, but it's unequal to the element that's actually contained in the set. The reason is that TreeSetuses its comparison method (BigDecimal.compareTo) to determine set membership, not equals.

这里令人惊讶的是,TreeSet它说它包含 object zz,但它不等于实际包含在集合中的元素。原因是TreeSet使用其比较方法 ( BigDecimal.compareTo) 来确定集合成员资格,而不是equals

Now let's compare TreeSetto HashSet:

现在让我们比较TreeSetHashSet

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

This is strange. We have two sets that are equal, but one set says that it contains an object but another set says that it doesn't contain the same object. Again, this reflects the fact that TreeSetis using the comparison method whereas HashSetis using equals.

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

Now let's add the other object to a HashSetand see what happens:

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

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

Now that's weird. One set says it's equal to the other, but the other set says it's not equal to the first! To understand this, you need to understand how equality of sets is determined. Two sets are considered equal if a) they are of the same size, and b) each element in the other set is also contained in this set. That is, if you have

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

set1.equals(set2)

then the equality algorithm looks at the sizes and then it iterates over set2, and for each element it checks whether that element is contained in set1. That's where the asymmetry comes in. When we do

然后等式算法查看大小,然后遍历 set2,并针对每个元素检查该元素是否包含在 set1 中。这就是不对称性的来源。当我们这样做时

ts.equals(hs2)

both sets are of size 1, so we proceed to the iteration step. We iterate over hs2and use then call the TreeSet.containsmethod -- which uses the comparison method. As far as the TreeSetis concerned, it's equal to the HashSeths2.

两个集合的大小都是 1,所以我们继续迭代步骤。我们迭代hs2并使用然后调用TreeSet.contains方法——它使用比较方法。就theTreeSet而言,它等于HashSeths2。

Now when we do

现在当我们做

hs2.equals(ts)

the comparison goes the other way. We iterate over the TreeSetand get its element, and ask hs2whether it containsthat element. Since the HashSet.containsuses equals, it returns false, and the overall result is false.

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

回答by Tomasz Nurkiewicz

Say we have this simple Studentclass implementing Comparable<Student>but not overriding equals()/hashCode(). Of course equals()is not consistent with compareTo()- two different students with the same agearen't equal:

假设我们有这个简单的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 + ")";
    }
}

We can safely use it in TreeMap<Student, String>:

我们可以安全地使用它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)));

The results are easy to predict: students are nicely sorted according to their age (despite being inserted in different order) and fetching student using new Student(22)key works as well and returns "twenty two". This means we can safely use Studentclass in TreeMap.

结果很容易预测:学生根据他们的年龄很好地排序(尽管以不同的顺序插入)并且使用new Student(22)key获取学生也可以工作并返回"twenty two"。这意味着我们可以安全地StudentTreeMap.

However change studentsto HashMapand things go bad:

但是,更改studentsHashMap并且事情变糟了:

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

Obviously the enumeration of items returns "random" order due to hashing - that's fine, it doesn't violate any Mapcontract. But the last statement is completely broken. Because HashMapuses equals()/hashCode()to compare instances, fetching value by new Student(22)key fails and returns null!

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

This is what the JavaDoc tries to explain: such classes will work with TreeMapbut might fail to work with other Mapimplementations. Note that Mapoperations are documented and defined in terms of equals()/hashCode(), e.g. containsKey():

这就是 JavaDoc 试图解释的:此类类可以使用,TreeMap但可能无法与其他Map实现一起使用。请注意,Map操作是根据equals()/记录和定义的hashCode(),例如containsKey()

[...] returns true if and only if this map contains a mapping for a key k such that (key==null ? k==null : key.equals(k))

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

Thus I don't believe there are any standard JDK classes that implemente Comparablebut fail to implement equals()/hashCode()pair.

因此,我不相信有任何标准的 JDK 类可以实现Comparable但无法实现equals()/hashCode()对。

回答by Ngx472

Here is another example of when consistency with equals ANDtotal ordering are important to implement.

这是另一个示例,说明何时实现与等于总排序的一致性很重要。

Say we have an object MyObjectwhich has two fields: idand quantity. idas its name suggests is the natural key of the object and quantityis just an attribute.

假设我们有一个MyObject具有两个字段的对象:idquantityid顾名思义是对象的自然键,quantity只是一个属性。

public class MyObject {
  int id;
  int quantity;
  ...
}

Let's imagine that we want to use a collections of MyObjectsorted by quantitydescending. The First comparator we can write is:

假设我们要使用MyObjectquantity降序排序的集合。我们可以写的第一个比较器是:

Comparator<MyObject> naiveComp = new Comparator<MyObject>() {
  @Override
  public int compare(MyObject o1, MyObject o2) {
    return o2.quantity - o1.quantity;
  }
};

Using MyObjectinstances equipped with this comparator in a TreeMap/TreeSet fails because it comparator is not consistent with equals(see full code below). Let's make it consistent with equals:

MyObject在 TreeMap/TreeSet 中使用配备此比较器的实例失败,因为它的比较器与 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
  }
};

However, this fails again to fit in TreeSet/TreeMap! (see full code below) This is because the ordering relation is not total, i.e. not any two objects can be strictly put in an ordering relationship. In this comparator, when quantityfields are equal, the resulting ordering is undetermined.

然而,这再次无法适应 TreeSet/TreeMap!(请参阅下面的完整代码)这是因为排序关系不是total,即不是任何两个对象都可以严格置于排序关系中。在这个比较器中,当quantity字段相等时,结果排序是不确定的。

A better comparator would be:

更好的比较器是:

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
  }
};

This comparator ensures that:

该比较器确保:

  • when compareTo returns 0 it implies that two objects are equal(initial check for equality)
  • all items are totally ordered by using idas a discriminant ordering field when quantityare equal
  • 当 compareTo 返回 0 时,这意味着两个对象是equal(初始检查是否相等)
  • idquantity相等时,所有项目都通过用作判别排序字段完全排序

Full Testing Code:

完整的测试代码:

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());
  }
}

Output:

输出:

                  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}]

Conclusion:

结论:

Although I think it is very legitimate to segregate identity from ordering conceptually. For instance, in relational database terms:

尽管我认为从概念上将身份与排序分开是非常合理的。例如,在关系数据库术语中:

select * from MyObjects order by quantity

works perfectly. We don't care about object identity here, nor we want total ordering

完美运行。我们不关心这里的对象标识,也不想要完全排序

However, due to constraints in tree based collections implementation, one has to ensure that any comparator they write:

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

  • is consistency with equals
  • provide a total ordering over all possible objects
  • 与equals保持一致
  • 提供所有可能对象的总排序