“現在我將告訴您一些同樣有用的方法:  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

“目前為止就這樣了。”

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