比较与相等是什么意思?如果我们班不遵循这个原则,会发生什么?

2ul0zpep  于 2021-07-09  发布在  Java
关注(0)|答案(4)|浏览(380)

从treemap的javadoc:
请注意,如果排序Map要正确实现Map接口,则排序Map所维护的顺序(无论是否提供显式比较器)必须与equals一致(请参阅comparable or comparator以获得与equals一致的精确定义。)这是因为map接口是根据equals操作定义的,但是map使用其compareto(或compare)方法执行所有键比较,因此从排序map的Angular 来看,此方法认为相等的两个键是相等的。一个排序Map的行为是明确定义的,即使它的排序与equals不一致;它就是不遵守Map界面的总合同。
有人能举一个具体的例子来说明如果排序与equals不一致可能出现的问题吗?以具有自然顺序的用户定义类为例,即它实现了comparable。jdk中的所有内部类都保持这个不变量吗?

7rtdyuoh

7rtdyuoh1#

下面是另一个例子,说明了实现equals和total顺序的一致性非常重要。
假设我们有个目标 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/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
  }
};

然而,这又不能适应treeset/treemap(请参阅下面的完整代码)这是因为排序关系不是total,即没有任何两个对象可以严格地放入排序关系中。在这个比较器中,当 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 (初始检查是否相等)
所有项目都是通过使用 idquantity 是平等的
完整测试代码:

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

效果很好。这里我们不关心对象标识,也不需要完全排序
但是,由于基于树的集合实现中的限制,必须确保它们编写的任何比较器:
一致性等于
提供对所有可能对象的总体排序

h79rfbju

h79rfbju2#

下面是一个简单但现实的例子,说明如果比较方法与equals不一致会发生什么。在jdk中, BigDecimal 工具 Comparable 但其比较方法与等号法不一致。例如:

> 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 .
现在让我们比较一下 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

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

set1.equals(set2)

然后相等算法查看大小,然后遍历set2,并检查每个元素是否包含在set1中。这就是不对称的原因。当我们这样做的时候

ts.equals(hs2)

两个集合的大小都是1,所以我们继续迭代步骤。我们迭代 hs2 然后使用 TreeSet.contains 方法——使用比较法。至于 TreeSet 是的,它等于 HashSet 高铁2号。
现在,当我们这样做

hs2.equals(ts)

这种比较则相反。我们在 TreeSet 得到它的元素,然后问 hs2 是否 contains 那个元素。自从 HashSet.contains 如果使用equals,则返回false,总体结果为false。

zpjtge22

zpjtge223#

可比接口的契约允许不一致的行为:
强烈建议(尽管不是必需的)自然顺序与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);
}
qv7cva1a

qv7cva1a4#

假设我们有这个简单的 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 .
然而改变 studentsHashMap 事情变得很糟:

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

显然,由于散列,项的枚举返回“随机”顺序-这很好,它不违反任何规则 Map 合同。但最后一句话完全被打破了。因为 HashMap 使用 equals() / hashCode() 要比较示例,请按 new Student(22) 密钥失败并返回 null !
这就是javadoc试图解释的:这样的类将与 TreeMap 但可能无法与其他人合作 Map 实现。请注意 Map 操作记录和定义如下: equals() / hashCode() ,例如。 containsKey() :
[…]返回true,当且仅当此Map包含键k的Map,使得 (key==null ? k==null : key.equals(k)) 因此,我不相信有任何标准的jdk类可以实现 Comparable 但未能实施 equals() / hashCode() 一对。

相关问题