JavaRush /Java 博客 /Random-ZH /等于和 hashCode 合约或其他任何东西
Aleksandr Zimin
第 1 级
Санкт-Петербург

等于和 hashCode 合约或其他任何东西

已在 Random-ZH 群组中发布
当然,绝大多数 Java 程序员都知道方法彼此密切equals相关hashCode,并且建议在其类中一致地重写这两个方法。少数人知道为什么会这样,以及如果违反这条规则会产生什么悲惨的后果。我建议考虑这些方法的概念,重复它们的目的并理解它们为何如此相关。和上一篇关于加载类的文章一样,我为自己写了这篇文章,以便最终揭示问题的所有细节,并且不再返回第三方来源。因此,我很乐意接受建设性的批评,因为如果某个地方存在差距,就应该将其消除。唉,这篇文章实在是太长了。

等于覆盖规则

Java 中需要一个方法equals()来确认或否认两个同源对象在逻辑上相等的事实。也就是说,在比较两个对象时,程序员需要了解它们的有效字段是否相等。所有字段不必相同,因为该方法equals()意味着逻辑相等。但有时并没有特别需要使用这种方法。正如他们所说,避免使用特定机制出现问题的最简单方法就是不使用它。还应该注意的是,一旦违反合同,equals您就无法理解其他对象和结构将如何与您的对象交互。并且随后查找错误原因将非常困难。

何时不重写此方法

  • 当类的每个实例都是唯一的时。
  • 在更大程度上,这适用于那些提供特定行为而不是设计用于处理数据的类。例如,类Thread. 对于他们来说equals,类提供的方法的实现Object已经绰绰有余了。另一个例子是枚举类 ( Enum)。
  • 事实上,类不需要确定其实例的等价性。
  • 例如,对于一个类,java.util.Random根本不需要将该类的实例相互比较,以确定它们是否可以返回相同的随机数序列。仅仅是因为这个类的性质甚至不暗示这种行为。
  • 当您要扩展的类已经有自己的方法实现equals并且该实现的行为适合您时。
  • 例如,对于类SetListMap实现分别equalsAbstractSetAbstractListAbstractMap中。
  • equals最后,当类的范围是privateorpackage-private并且您确定永远不会调用此方法时,无需重写。

等于合同

重写方法时,equals开发人员必须遵守 Java 语言规范中定义的基本规则。
  • 反身性
  • 对于任何给定值x,表达式x.equals(x)必须返回true
    给定- 意思是这样的x != null
  • 对称
  • 对于任何给定值xy,只有在返回时才x.equals(y)应返回。 truey.equals(x)true
  • 传递性
  • 对于任何给定值xy并且z,如果x.equals(y)返回truey.equals(z)返回truex.equals(z)则必须返回该值true
  • 一致性
  • 对于任何给定值,x重复y调用x.equals(y)将返回先前调用此方法的值,前提是用于比较两个对象的字段在调用之间没有更改。
  • 比较空
  • 对于任何给定值,x调用x.equals(null)必须返回false

等于违反合同

许多类,例如 Java Collections Framework 中的类,都依赖于 method 的实现equals(),因此您不应该忽视它,因为 违反该方法的约定可能会导致应用程序运行不合理,并且在这种情况下查找原因将相当困难。根据自反性原理,每个对象都必须等价于它自己。如果违反了这个原则,当我们将一个对象添加到集合中,然后使用该方法搜索它时,contains()我们将无法找到刚刚添加到集合中的对象。对称条件规定任何两个对象都必须相等,无论它们比较的顺序如何。例如,如果您有一个类仅包含一个字符串类型的字段,则equals将该字段与方法中的字符串进行比较将是不正确的。因为 在反向比较的情况下,该方法将始终返回值false
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
根据传递性 条件,如果三个对象中的任何两个相等,那么在这种情况下,所有三个对象都必须相等。当需要通过添加有意义的组件来扩展某个基类时,很容易违反此原则。例如,对于Point具有坐标的类xy您需要通过展开它来添加点的颜色。为此,您需要声明一个ColorPoint具有适当字段的类color。因此,如果在扩展类中我们调用equals父方法,并且在父方法中我们假设只比较坐标xy,那么不同颜色但坐标相同的两个点将被认为是相等的,这是不正确的。在这种情况下,有必要教派生类区分颜色。为此,您可以使用两种方法。但一会违反对称性规则,二会违反传递性规则。
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
在这种情况下,调用point.equals(colorPoint)将返回值true,比较colorPoint.equals(point)将返回false,因为 期望一个“它的”类的对象。因此,对称性规则被违反。第二种方法涉及在没有有关点颜色的数据的情况下进行“盲”检查,即我们有类Point。或者检查颜色是否有相关信息,即比较该类的对象ColorPoint
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
这里违反了传递性 原则,如下所示。假设有以下对象的定义:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
因此,虽然满足p1.equals(p2)和 的相等性p2.equals(p3)p1.equals(p3)但它会返回值false。同时,第二种方法在我看来看起来不太有吸引力,因为 在某些情况下,算法可能是盲目的,无法完全执行比较,而您可能不知道。 一点诗 一般来说,据我了解,这个问题没有具体的解决方案。一位名叫 Kay Horstmann 的权威作者认为,您可以使用返回对象类的instanceof方法调用来替换运算符的使用getClass(),并且在开始比较对象本身之前,确保它们属于同一类型,并且不注意它们共同起源的事实。因此,对称性传递性规则将得到满足。但与此同时,在街垒的另一边站着另一位在广泛圈子里同样受人尊敬的作家约书亚·布洛赫(Joshua Bloch),他认为这种做法违反了芭芭拉·利斯科夫(Barbara Liskov)的替代原则。该原则指出“调用代码必须在不知情的情况下以与其子类相同的方式对待基类。 ” 而在霍斯特曼提出的解决方案中,显然违反了这一原则,因为它取决于实现。总之,事情显然是暗的。还应该指出的是,Horstmann 阐明了应用他的方法的规则,并用简单的英语写道,您需要在设计类时决定策略,如果相等性测试仅由超类执行,您可以通过执行的操作instanceof。否则,当检查的语义根据派生类而变化并且方法的实现需要在层次结构中下移时,您必须使用方法getClass()。反过来,Joshua Bloch 提议放弃继承并使用对象组合,方法是在类中包含一个ColorPointPoint,并提供访问方法asPoint()来获取专门关于点的信息。这将避免违反所有规则,但是,在我看来,这将使代码更难以理解。第三个选项是使用 IDE 自动生成 equals 方法。顺便说一句,Idea 再现了 Horstmann 一代,允许您选择在超类或其后代中实现方法的策略。最后,下一个一致性规则规定,即使对象x没有y改变,再次调用它们也x.equals(y)必须返回与之前相同的值。最终规则是任何对象都不应该等于null。这里一切都清楚了null——这就是不确定性,物体等于不确定性吗?目前还不清楚,即false.

确定等于的通用算法

  1. this检查对象引用和方法参数是否相等o
    if (this == o) return true;
  2. 检查链接是否已定义o,即是否已定义null
    如果以后比较对象类型时,会用到运算符instanceof,则可以跳过此项,因为false本例中会返回此参数null instanceof Object
  3. 根据上述描述和您自己的直觉,this使用o运算符instanceof或方法来比较对象类型。getClass()
  4. 如果子类中重写了某个方法equals,请务必进行调用super.equals(o)
  5. 将参数类型转换o为所需的类。
  6. 对所有重要对象字段进行比较:
    • 对于基本类型(除了floatdouble),使用运算符==
    • 对于参考字段,您需要调用他们的方法equals
    • 对于数组,可以使用循环迭代或方法Arrays.equals()
    • 对于类型floatdouble需要使用相应包装类的比较方法Float.compare()Double.compare()
  7. 最后,回答三个问题:所实现的方法是否对称传递性同意吗?其他两个原则(自反性确定性)通常是自动执行的。

HashCode覆盖规则

哈希是从对象生成的数字,描述其在某个时间点的状态。该数字在 Java 中主要用于哈希表,例如HashMap. 在这种情况下,基于对象获取数字的哈希函数必须以确保元素在哈希表中相对均匀分布的方式实现。当函数为不同的键返回相同的值时,还可以最大限度地减少冲突的可能性。

合约哈希码

为了实现哈希函数,语言规范定义了以下规则:
  • 对同一个对象调用一次或多次方法hashCode必须返回相同的哈希值,前提是计算该值所涉及的对象字段没有改变。
  • 如果对象相等,则在两个对象上调用方法hashCode应始终返回相同的数字(equals在这些对象上调用方法会返回true)。
  • 对两个不相等的对象调用方法hashCode必须返回不同的哈希值。虽然这个要求不是强制性的,但应该考虑到它的实现会对哈希表的性能产生积极的影响。

equals 和 hashCode 方法必须一起重写

根据上述约定,当重写代码中的方法时equals,您必须始终重写该方法hashCode。由于事实上一个类的两个实例是不同的,因为它们位于不同的内存区域,因此必须根据某些逻辑标准对它们进行比较。因此,两个逻辑上等效的对象必须返回相同的哈希值。 如果仅重写其中一种方法会发生什么情况?
  1. equals是的,hashCode不是

    假设我们equals在类中正确定义了一个方法,并hashCode决定将该方法保留在类中Object。那么从方法的角度来看,equals这两个对象在逻辑上是相等的,而从方法的角度来看,hashCode它们将没有任何共同点。因此,通过将对象放入哈希表中,我们面临着无法通过键取回它的风险。
    例如,像这样:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    显然,正在放置的对象和正在搜索的对象是两个不同的对象,尽管它们在逻辑上是相等的。但是因为 它们具有不同的哈希值,因为我们违反了合同,我们可以说我们在哈希表内部的某个地方丢失了对象。

  2. hashCode是的,equals不是。

    hashCode如果我们重写该方法并equals从类继承该方法的实现,会发生什么情况Object。如您所知,equals默认方法只是将指针与对象进行比较,确定它们是否引用同一个对象。假设hashCode我们已经根据所有规范编写了该方法,即使用 IDE 生成它,并且对于逻辑上相同的对象,它将返回相同的哈希值。显然,通过这样做,我们已经定义了一些用于比较两个对象的机制。

    因此,理论上应该执行上一段的例子。但我们仍然无法在哈希表中找到我们的对象。尽管我们会接近这一点,因为至少我们会找到一个哈希表篮子,该对象将位于其中。

    要成功地在哈希表中搜索到对象,除了比较键的哈希值外,还需要判断键与搜索到的对象的逻辑相等性。也就是说,equals如果不重写该方法就没有办法。

确定hashCode的通用算法

在这里,在我看来,你不应该太担心并在你最喜欢的 IDE 中生成该方法。因为所有这些位的左右移动都是为了寻找黄金比例,即正态分布——这是针对完全顽固的家伙的。就我个人而言,我怀疑我能否比同一个想法做得更好更快。

而不是下结论

因此,我们看到方法在 Java 语言中equals扮演着hashCode明确定义的角色,旨在获得两个对象的逻辑相等特征。在方法的情况下,equals这与比较对象有直接关系,在hashCode间接方法的情况下,当有必要时,比如说,确定对象在哈希表或类似数据结构中的大致位置,以便提高搜索对象的速度。除了合同之外,equals还有hashCode一个与对象比较相关的要求。这就是compareTo接口方法Comparableequals. 此要求要求开发商必须x.equals(y) == true在 时返回x.compareTo(y) == 0。也就是说,我们看到两个对象的逻辑比较在应用程序中的任何地方都不应该矛盾,并且应该始终一致。

来源

有效的 Java,第二版。约书亚·布洛赫. 免费翻译一本非常好的书。 Java,专业人士的图书馆。第 1 卷。基础知识。凯·霍斯特曼。 少一点理论,多一点实践。但一切都没有像布洛赫那样详细分析。虽然有同样的equals()观点。 图片中的数据结构。HashMap 一篇关于 Java 中的 HashMap 设备的非常有用的文章。而不是看来源。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION