我研究了这个问题,我得到的答案并不让我满意,因为它们没有足够深入地解释这些事情。因此,众所周知,对于具有参数化自定义类的 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
对象,而上行将比较Ball
的color
字段引用的String
对象。
详
首先,让我们澄清术语。
这等于是 Object.equals,如果只有这两个对象(即"变量")指向内存中的同一对象,它将返回 true。
在equals
方法中,this
和Object 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.equals
与Object#equals
混淆.
- 第一个是实用程序类
Objects
上的static
方法 — 请注意复数s
。 - 第二个是在最终超类
Object
上定义的实例方法 — 请注意单数,末尾没有s
。 第二个由Ball
继承,但随后被Ball
类自己的实现覆盖。因此,Object#equals
提供的实现永远不会在你的方案中使用。
你说:
这个
equals
是Object.equals
如果只有这两个对象(又名"变量")指向内存中的同一对象,它将返回 true。
你在那里缩短它。Object.equals
方法执行与equals
方法相同的三阶段逻辑:首先是身份检查,其次是 null 检查,第三是在两个对象的equals
方法中实现的任何内容。
最重要的是,您将两个String
对象传递给Objects.equals
,而您对equals
的覆盖比较了两个Ball
对象。
顺便说一下,如果你的类的目的是透明和不可变地传达日期,你可以在 Java 16+ 中更简单地将类定义为记录。
在记录中,您只需声明每个成员字段的类型和名称。编译器隐式创建构造函数、getter、equals
&hashCode
和toString
。
此方法的默认值是利用每个成员字段。您可以覆盖要考虑成员字段子集的equals
和hashCode
。
这是您作为记录编写的整个Ball
类。
record Ball ( String color ) {}
用法:
Ball redBall = new Ball( "red" ) ;
Ball blueBall = new Ball( "blue" ) ;
boolean sameBall = redBall.equals( blueBall ) ; // false