“现在我将告诉您一些同样有用的方法:  equals(Object o) 和 hashCode()。”

“您可能已经记得,在 Java 中,比较引用变量时不会比较对象本身,而是比较对对象的引用。”

代码 解释
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j);
i 不等于 j
变量指向不同的对象。
即使对象包含相同的数据。
Integer i = new Integer(1);
Integer j = i;
System.out.println(i==j);
我等于 j。这些变量包含对同一对象的引用。

“是的,我记得那个。”

平等 。_

“equals方法是这里的标准解决方案。equals方法的目的是通过比较对象内部存储的内容来确定对象内部是否相同。”

“它是怎么做到的?”

“这与 toString() 方法非常相似。”

Object 类有自己的 equals 方法实现,它只是比较引用:

public boolean equals(Object obj)
{
return (this == obj);
}

“太好了……又回到那个时候了,是吗?”

“扬起你的下巴!这其实很棘手。”

“创建此方法是为了让开发人员可以在自己的类中覆盖它。毕竟,只有类的开发人员才知道比较时哪些数据是相关的,哪些不是。”

“你能举个例子吗?”

“当然。假设我们有一个表示数学分数的类。它看起来像这样:”

例子:
class Fraction
{
private int numerator;
private int denominator;
Fraction(int numerator, int denominator)
{
this.numerator  = numerator;
this.denominator = denominator;
}public boolean equals(Object obj)
{
if (obj==null)
return false;

if (obj.getClass() != this.getClass() )
return false;

Fraction other = (Fraction) obj;
return this.numerator* other.denominator == this.denominator * other.numerator;
}
}
示例方法调用:
Fraction one = new Fraction(2,3);
Fraction two = new Fraction(4,6);
System.out.println(one.equals(two));
方法调用将返回 true。
分数 2/3 等于分数 4/6

“现在,让我们来剖析一下这个例子。”

“我们覆盖了equals方法,因此Fraction对象将有自己的实现。

“方法中有几个检查:”

" 1) 如果传递给比较的对象是null,那么对象是不相等的。如果你能调用一个对象的equals方法,那么它肯定不是null。"

" 2) 类比较。如果对象是不同类的实例,那么我们不会尝试比较它们。相反,我们将立即使用return false来指示这些是不同的对象。"

" 3) 每个人从二年级就记得 2/3 等于 4/6。但是你如何检查呢?"

2/3 == 4/6
我们将两边都乘以两个除数(6 和 3),我们得到:
6 * 2 == 4 * 3
12 == 12
一般规则:
如果
a / b == c / d
那么
a * d == c * b

“因此,在equals方法的第三部分,我们将传递的对象转换为 Fraction 并比较分数。”

“明白了,如果我们简单地比较分子和分子,分母和分母,那么2/3不等于4/6。”

“现在我明白你说只有类的开发者才知道如何正确比较它的意思了。”

“是的,但这只是故事的一半。 还有另一种方法:hashCode()。

“关于 equals 方法的一切现在都说得通了,但为什么我们需要 hashCode ()?

“快速比较需要hashCode方法。”

equals方法有一个主要缺点:运行速度太慢。假设你有一个包含数百万个元素的 Set,需要检查它是否包含特定对象。你是怎么做到的?”

“我可以使用循环遍历所有元素,并将对象与集合中的每个对象进行比较。直到找到匹配项。”

“如果它不在那里?我们会进行一百万次比较,只是为了发现那个物体不在那里?这看起来不是很多吗?”

“是啊,连我都承认比较太多了,还有别的办法吗?”

“是的,你可以为此使用hashCode ()。

hashCode ( ) 方法为每个对象返回一个特定的数字。类的开发人员决定返回什么数字,就像他或她为 equals 方法所做的那样。

“让我们看一个例子:”

“想象一下,你有一百万个10位数字。然后,你可以让每个数字的hashCode是这个数字除以100后的余数。”

这是一个例子:

数字 我们的哈希码
1234567890 90后
9876554321 21
9876554221 21
9886554121 21

“是的,这是有道理的。我们用这个 hashCode 做什么?”

“我们不是比较数字,而是比较它们的哈希码。那样比较快。”

“只有当它们的hashCode相等时,我们才称之为equals。”

“是的,这样更快。但我们仍然需要进行一百万次比较。我们只是比较较小的数字,我们仍然需要为具有匹配哈希码的任何数字调用 equals。”

“不,你可以通过更少的比较来逃脱。”

“想象一下,我们的 Set 存储按hashCode分组或排序的数字(以这种方式对它们进行排序本质上是对它们进行分组,因为具有相同 hashCode 的数字将彼此相邻)。然后您可以非常快速且轻松地丢弃不相关的组。这就足够了每组检查一次,看看它的 hashCode 是否与对象的 hashCode 匹配。”

“想象一下,你是一名学生,正在寻找一个你可以一眼认出的朋友,而且我们认识的人住在 17 号宿舍。然后你就去大学的每个宿舍问,‘这是 17 号宿舍吗?’ 如果不是,那么你就忽略宿舍里的每个人,然后继续前进到下一个。如果答案是‘是’,那么你就开始走过每个房间,寻找你的朋友。”

“在这个例子中,宿舍号 (17) 是 hashCode。”

“实现 hashCode 函数的开发人员必须了解以下内容:”

a) 两个不同的对象可以有相同的hashCode  (不同的人可以住在同一个宿舍)

B) 相同的对象 (根据 equals 方法) 必须具有相同的 hashCode。.

C) 必须选择散列码,这样就不会有很多不同的对象具有相同的散列码。 如果有,那么哈希码的潜在优势就失去了(你到达宿舍 17,发现一半的大学住在那里。真可惜!)。

“现在最重要的是,如果你重写了equals方法,你绝对必须重写hashCode ()方法并遵守上面描述的三个规则。

“原因是这样的:在 Java 中,集合中的对象总是先使用 hashCode() 进行比较/检索, 然后再使用 equals 进行比较/检索。如果相同的对象具有不同的 hashCode,则对象将被认为是不同的,并且 equals 方法甚至不会被调用。

“在我们的分数示例中,如果我们使 hashCode 等于分子,则分数 2/3 和 4/6 将具有不同的 hashCode。分数相同并且 equals 方法表示它们相同,但它们的 hashCode 表示它们是不同的。如果我们在使用 equals 进行比较之前使用 hashCode 进行比较,那么我们就会得出结论,对象是不同的,我们甚至从未使用过 equals 方法。”

这是一个例子:

HashSet<Fraction>set = new HashSet<Fraction>();
set.add(new Fraction(2,3));System.out.println( set.contains(new Fraction(4,6)) );
如果hashCode() 方法返回分数的分子,则结果将为 false。并且不会在集合中找到
«new Fraction(4,6) » 对象。

“那么为分数实现 hashCode 的正确方法是什么?”

“这里你需要记住,相等的分数必须具有相同的哈希码。”

版本 1:hashCode 等于整数除法的结果。”

“对于 7/5 和 6/5,这将是 1。”

“对于 4/5 和 3/5,这将为 0。”

“但是这个选项不太适合比较故意小于 1 的分数。哈希码(整数除法的结果)将始终为 0。”

版本 2:hashCode 等于分母除以分子的整数结果。”

“此选项适用于分数小于 1 的情况。如果分数小于 1,则其倒数大于 1。如果我们反转所有分数,则比较不会受到任何影响。”

“我们的最终版本结合了两种解决方案:”

public int hashCode()
{
return numerator/denominator + denominator/numerator;
}

让我们使用 2/3 和 4/6 来测试它。它们应该具有相同的哈希码:

分数 2/3 分数 4/6
分子/分母 2 / 3 == 0 4 / 6 == 0
分母/分子 3 / 2 == 1 6 / 4 == 1
分子/分母
+
分母/分子
0 + 1 == 1 0 + 1 == 1

“目前为止就这样了。”

“谢谢,艾莉。那真的很有趣。”