等于和哈希代码如何在引擎盖下工作



我研究了这个问题,我得到的答案并不让我满意,因为它们没有足够深入地解释这些事情。因此,众所周知,对于具有参数化自定义类的 HashSet,有必要覆盖 hashCode 和 equals 以禁止重复。但在实践中,当我试图理解它是如何工作的时,我并不完全明白。 我有一堂课:

static class Ball {
String color;
public Ball(String color) {
this.color = color;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Ball ball = (Ball) o;
return Objects.equals(color, ball.color);
}
@Override
public int hashCode() {
return Objects.hash(color);
}
}

在平等方法中,一切都很清楚。如果两个"变量"指向内存中的同一对象,则它们是相等的;如果 O 为空或它们不属于同一类 - 它们不相等。 最后一行平等是我所关心的。 当我转到对象时:

public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

它再次说,如果两个"变量"引用同一个对象,那么它们是相等的,或者如果第一个对象不为空而等于第二个对象。这等于是 Object.equals,如果只有这两个对象(即"变量")指向内存中的同一对象,它将返回 true。 那么,这到底是如何运作的呢?一直在寻找一个明确的答案,但正如我所说,到目前为止我得到的东西根本不让我满意。

在你的课堂上,你解释得很好。

您缺少的部分是,在某些时候,您的代码将委托给color属性上的 equals 和 hashCode,该属性由java.lang.String类实现。

例如,请参阅 https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/lang/String.java#L1013 和 https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/lang/String.java#L1494

tl;博士

返回 Objects.equals(color, ball.color);

您在此处传递两个String对象,而不是Ball对象。

您对Object#equals的覆盖将比较Ball对象,而上行将比较Ballcolor字段引用的String对象。

首先,让我们澄清术语。

这等于是 Object.equals,如果只有这两个对象(即"变量")指向内存中的同一对象,它将返回 true。

equals方法中,thisObject o都是引用变量,而不是对象。任何一个变量都可以完全不保存引用 (null),或者任何一个变量都可以保存指向内存中其他位置的对象的引用(指针)。

接下来,我们可以检查您的代码。

equals的三个阶段

equals的逻辑分为三个阶段:

  • 身份检查
  • 空检查
  • 内容比较

第一阶段执行身份检查。我们查看两个引用变量是否引用相同的对象,相同的内存块。如果是这样,则无需进一步考虑:对象始终等于自身。所以返回true,工作完成。

第二阶段执行空检查。如果被比较的两个对象中的任何一个为空,我们报告false,意思是"不相等"。要执行空检查,我们跳过this。根据定义,this引用变量不能为 null。我们继续Object o.如果为 null,则报告false,作业已完成。

第三阶段比较内容。我们检查this引用的对象的内容。我们检查Object o引用的对象的内容。我们知道我们有两个单独的对象(两个单独的内存块),因为在代码的这一点上,我们通过了身份检查。

对于Ball类,您选择比较唯一的状态,即成员字段color。该成员字段包含对String对象的引用。因此,在将o转换为Ball ball之后,我们将this.color的字符串与ball.color的字符串进行比较。

这似乎是你理解的症结所在。 选角在这里至关重要。在成功地从Object o转换为Ball ball后,Java虚拟机在运行时知道所讨论的对象确实是一个Ball(或Ball的子类)。作为一个Ball,我们可以访问其color字段。

在对Objects.equals(color, ball.color)的调用中,我们正在比较两个String对象,而不是两个Ball对象。您可能会发现扩展该代码的清晰度。

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Ball ball = (Ball) o;
String thisColor = this.color ; 
String thatColor = ball.color ; 
boolean colorsAreTheSame = thisColor.equals( thatColor );
return colorsAreTheSame ; 
}

你说:

当我转到对象时:

您的代码将两个String对象传递给Objects.equal,而不是两个Ball对象。

顺便说一句,不要将Objects.equalsObject#equals混淆.

  • 第一个是实用程序类Objects上的static方法 — 请注意复数s
  • 第二个是在最终超类Object上定义的实例方法 — 请注意单数,末尾没有s。 第二个由Ball继承,但随后被Ball类自己的实现覆盖。因此Object#equals提供的实现永远不会在你的方案中使用。

你说:

这个equalsObject.equals如果只有这两个对象(又名"变量")指向内存中的同一对象,它将返回 true。

你在那里缩短它。Object.equals方法执行与equals方法相同的三阶段逻辑:首先是身份检查,其次是 null 检查,第三是在两个对象的equals方法中实现的任何内容。

最重要的是,您将两个String对象传递给Objects.equals,而您对equals的覆盖比较了两个Ball对象。


顺便说一下,如果你的类的目的是透明和不可变地传达日期,你可以在 Java 16+ 中更简单地将类定义为记录。

在记录中,您只需声明每个成员字段的类型和名称。编译器隐式创建构造函数、getter、equals&hashCodetoString

此方法的默认值是利用每个成员字段。您可以覆盖要考虑成员字段子集的equalshashCode

这是您作为记录编写的整个Ball类。

record Ball ( String color ) {}

用法:

Ball redBall = new Ball( "red" ) ;
Ball blueBall = new Ball( "blue" ) ;
boolean sameBall = redBall.equals( blueBall ) ;  // false

最新更新