什么是比较符合平等是什么意思? 如果我的类不遵循这一原则也可能会发生什么?(What does

2019-06-17 21:03发布

从树形图的JavaDoc:

请注意,如果有序映射要正确实现Map接口,则有序映射(无论是否提供了明确的比较器)保持的顺序必须与equals一致。 (参见可比或比较用于一致的精确定义与equals)。这是因为Map接口是按照equals操作定义的,但地图使用其的compareTo执行所有关键比(或比较)方法,所以两个键被认为等于通过该方法是,从有序映射,相等的观点出发。 有序映射的行为是明确的,即使它的排序和equals不一致; 它只不过没有遵守Map接口的常规协定。

有些人能给出一个具体的例子来demonsrate如果排序不与equals一致时可能发生的问题? 就拿具有自然排序,即它实现可比例如用户定义的类。 也不要在JDK所有内部类保持不变式?

Answer 1:

在可比的接口契约允许非一致的行为:

强烈推荐(虽然不是必需的)使自然排序与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);
}


Answer 2:

假设我们有这个简单的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" 。 这意味着我们可以放心地使用StudentTreeMap

然而改变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))

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



Answer 3:

下面是如果比较方法与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违反的总承包Set 。 (这是用相同的情况TreeMapMap ,但它是一个有点容易使用套来证明。)让我们比较的结果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

现在让我们来比较TreeSetHashSet

> 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,并且总的结果是假的。



Answer 4:

下面是当与平等的,总排序的一致性是实现重要的另一个例子。

假设我们有一个对象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;
  }
};

使用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?