你好!今天我们将讨论 Java 中的两个重要方法:
在今天的课程中,我们将讨论这些概念的详细信息。相信我,我们有话要说!但在继续介绍新内容之前,让我们回顾一下我们已经介绍过的内容 :) 如您所知,使用运算符比较两个对象通常不是一个好主意,因为
当然,只有DNA检测才能提供保证。两个人可以有相同的眼睛颜色、发型、鼻子,甚至疤痕——世界上有很多人,无法保证没有分身。但我们需要一个可靠的机制:只有DNA测试的结果才能让我们做出准确的结论。
在我们前面的例子中,对于类
equals()
和hashCode()
. 这不是我们第一次见到他们:CodeGym 课程从一个简短的课程开始equals()
——如果您忘记了或者以前没有看过,请阅读…… 
==
比较==
的是引用。这是我们最近一课的汽车示例:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
控制台输出:
false
看起来我们创建了两个相同的Car
对象:两个car对象对应字段的值是一样的,但是比较的结果还是false。我们已经知道原因了:car1
和car2
引用指向不同的内存地址,所以它们不相等。但是我们仍然想比较两个对象,而不是两个引用。比较对象的最佳解决方案是equals()
方法。
equals() 方法
您可能还记得我们不是从头开始创建此方法,而是覆盖它:该equals()
方法是在Object
类中定义的。也就是说,以通常的形式,它没什么用处:
public boolean equals(Object obj) {
return (this == obj);
}
这就是equals()
方法在类中的定义方式Object
。这又是一次参考文献的比较。他们为什么要这样做?那么,该语言的创造者如何知道您的程序中的哪些对象被认为是相等的,哪些不是?:) 这是该方法的要点equals()
——类的创建者是确定在检查类的对象是否相等时使用哪些特征的人。然后重写equals()
类中的方法。如果你不太理解“决定哪些特征”的含义,我们来看一个例子。这是一个代表男人的简单类:Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
// Getters, setters, etc.
}
假设我们正在编写一个程序,需要确定两个人是同卵双胞胎还是长相相似。我们有五个特征:鼻子大小、眼睛颜色、发型、疤痕的存在和 DNA 测试结果(为简单起见,我们将其表示为整数代码)。您认为这些特征中的哪一个可以让我们的程序识别同卵双胞胎? 
equals()
这对我们的方法意味着什么?我们需要在Man
类,考虑到我们程序的要求。该方法应该比较int dnaCode
两个对象的字段。如果它们相等,则对象相等。
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
真的那么简单吗?并不真地。我们忽略了一些事情。对于我们的对象,我们只确定了一个与建立对象相等性相关的字段:dnaCode
. 现在假设我们没有 1 个相关字段,而是 50 个相关字段。如果两个对象的所有 50 个字段都相等,则对象相等。这样的场景也是有可能的。主要问题是通过比较 50 个字段来确定相等性是一个耗时且资源密集的过程。现在想象一下,除了我们的Man
类之外,我们还有一个Woman
类,其字段与Man
. 如果另一个程序员使用我们的类,他或她可以轻松地编写如下代码:
public static void main(String[] args) {
Man man = new Man(........); // A bunch of parameters in the constructor
Woman woman = new Woman(.........); // The same bunch of parameters.
System.out.println(man.equals(woman));
}
在这种情况下,检查字段值是没有意义的:我们可以很容易地看到我们有两个不同类的对象,所以它们不可能相等!这意味着我们应该向该equals()
方法添加一个检查,比较被比较对象的类。很高兴我们想到了这一点!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
但也许我们忘记了其他事情?嗯...至少,我们应该检查我们不是在比较一个对象和它自己!如果引用A和B指向同一个内存地址,那么它们就是同一个对象,我们不需要浪费时间去比较50个字段。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
添加检查也没有坏处null
:没有对象可以等于null
。因此,如果方法参数为 null,则没有必要进行额外检查。考虑到所有这些,我们的equals()
类方法Man
如下所示:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
我们执行上述所有初始检查。在一天结束时,如果:
- 我们正在比较同一类的两个对象
- 并且比较的对象不是同一个对象
- 并且传递的对象不是
null
dnaCode
两个对象的领域。覆盖该equals()
方法时,请务必遵守以下要求:
-
自反性。
当该
equals()
方法用于将任何对象与自身进行比较时,它必须返回 true。
我们已经满足了这个要求。我们的方法包括:if (this == o) return true;
-
对称。
若是
a.equals(b) == true
,便b.equals(a)
要返回true
。
我们的方法也满足这个要求。 -
传递性。
如果两个对象等于某个第三个对象,则它们必须彼此相等。
如果a.equals(b) == true
和a.equals(c) == true
,则b.equals(c)
还必须返回 true。 -
坚持。
equals()
仅当涉及的字段更改时,结果才必须更改。如果两个对象的数据不变,那么结果equals()
一定是一样的。 -
与 的不等式
null
。对于任何对象,
a.equals(null)
必须返回 false
这不仅仅是一组一些“有用的建议”,而是一个严格的约定,在 Oracle 文档中列出
hashCode() 方法
现在让我们谈谈hashCode()
方法。为什么有必要?出于完全相同的目的——比较对象。但是我们已经有了equals()
!为什么是另一种方法?答案很简单:提高性能。在 Java 中使用该方法表示的哈希函数hashCode()
为任何对象返回一个固定长度的数值。在 Java 中,该hashCode()
方法为任何对象返回一个 32 位数字 ( int
)。比较两个数字比使用该方法比较两个对象要快得多equals()
,尤其是当该方法考虑许多字段时。如果我们的程序比较对象,使用散列码就简单多了。只有当对象基于hashCode()
方法相等时,比较才会进行到equals()
方法。顺便说一句,这就是基于散列的数据结构的工作原理,例如,熟悉的HashMap
! 该hashCode()
方法与equals()
方法一样,由开发人员覆盖。和 一样equals()
,该hashCode()
方法在 Oracle 文档中有详细说明的官方要求:
-
如果两个对象相等(即
equals()
方法返回真),那么它们必须具有相同的散列码。否则,我们的方法将毫无意义。正如我们上面提到的,
hashCode()
检查应该首先进行以提高性能。如果哈希码不同,那么检查将返回 false,即使根据我们定义方法的方式对象实际上是相等的equals()
。 -
如果
hashCode()
在同一对象上多次调用该方法,则每次都必须返回相同的数字。 -
规则 1 不适用于相反的方向。两个不同的对象可以具有相同的哈希码。
hashCode()
方法返回一个int
. Anint
是一个 32 位数字。它的值范围有限:从 -2,147,483,648 到 +2,147,483,647。换句话说,一个 . 的可能值刚刚超过 40 亿个int
。现在假设您正在创建一个程序来存储地球上所有人的数据。每个人都会对应自己的Person
对象(类似于班级Man
)。地球上生活着约 75 亿人。换句话说,无论我们为转换编写的算法多么巧妙Person
对象到 int,我们根本没有足够的可能数字。我们只有 45 亿个可能的 int 值,但有比这更多的人。这意味着无论我们多么努力,一些不同的人都会有相同的哈希码。当这种情况发生时(两个不同对象的哈希码一致),我们称之为碰撞。覆盖该hashCode()
方法时,程序员的目标之一是尽量减少潜在的冲突次数。考虑到所有这些规则,类hashCode()
中的方法会是什么样子Person
?像这样:
@Override
public int hashCode() {
return dnaCode;
}
惊讶吗?:) 如果您查看要求,您会发现我们完全遵守这些要求。根据,我们的方法返回 true 的对象equals()
也将相等hashCode()
。如果我们的两个Person
对象 in 相等equals
(即它们具有相同的dnaCode
),那么我们的方法返回相同的数字。让我们考虑一个更困难的例子。假设我们的程序应该为汽车收藏家选择豪华汽车。收藏可能是一种复杂的爱好,具有许多特点。一辆特定的 1963 年汽车的价格可能是 1964 年汽车的 100 倍。一辆1970年的红车比同年同品牌的蓝车贵100倍。 
Person
,我们丢弃了大部分字段(即人类特征)作为无关紧要的并且只使用了dnaCode
比较领域。我们现在在一个非常特殊的领域工作,其中没有无关紧要的细节!这是我们的LuxuryAuto
课程:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
// ...getters, setters, etc.
}
现在我们必须考虑比较中的所有领域。任何错误都可能使客户损失数十万美元,因此最好还是过于安全:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
在我们的equals()
方法中,我们没有忘记我们之前谈到的所有检查。但是现在我们比较对象的三个字段中的每一个。对于这个程序,我们需要绝对的平等,即每个领域的平等。怎么样hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
我们类中的字段model
是一个字符串。这很方便,因为String
类已经覆盖了hashCode()
方法。我们计算该model
字段的哈希码,然后将其他两个数字字段的总和添加到它。Java 开发人员有一个减少冲突次数的简单技巧:在计算哈希码时,将中间结果乘以一个奇素数。最常用的数字是 29 或 31。我们现在不会深入研究数学上的微妙之处,但以后请记住,将中间结果乘以足够大的奇数有助于“散布”哈希函数的结果,并且,因此,减少具有相同哈希码的对象的数量。对于我们hashCode()
在 LuxuryAuto 中的方法,它看起来像这样:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
您可以在StackOverflow 上的这篇文章以及Joshua Bloch 的Effective Java一书中 阅读更多关于此机制的所有复杂信息。最后,还有一点值得一提。每次我们覆盖equals()
andhashCode()
方法时,我们都会选择在这些方法中考虑的某些实例字段。这些方法考虑相同的字段。但是我们可以考虑equals()
和中的不同领域hashCode()
吗?从技术上讲,我们可以。但这是一个坏主意,原因如下:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
这是我们的equals()
和类hashCode()
的方法LuxuryAuto
。该hashCode()
方法保持不变,但我们model
从equals()
方法中删除了该字段。模型不再是该equals()
方法比较两个对象时使用的特征。但是在计算哈希码时,该字段仍会被考虑在内。结果是什么?让我们创造两辆车并找出答案!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Are these two objects equal to each other?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("What are their hash codes?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Are these two objects equal to each other?
true
What are their hash codes?
-1372326051
1668702472
错误!equals()
通过对方法和方法使用不同的字段hashCode()
,我们违反了为它们建立的契约!根据equals()
方法相等的两个对象必须具有相同的哈希码。我们为他们收到了不同的价值观。此类错误可能会导致绝对令人难以置信的后果,尤其是在处理使用散列的集合时。因此,当您覆盖equals()
and时hashCode()
,您应该考虑相同的字段。这节课很长,但你今天学到了很多东西!:) 现在是时候回去解决任务了!
GO TO FULL VERSION