搞懂 Java equals 和 hashCode 方法
分析完 Java List 容器的源碼后,本來想直接進入 Set 和 Map 容器的源碼分析,但是對于這兩種容器,內部存儲元素的方式的都是以鍵值對相關的,而元素如何存放,便與 equals
和 hashCode
這兩個方法密切相關。所以在分析 Map 家族之前,需要深入了解下這兩個方法,而且這兩個方法在面試的時候也屬于極有可能考察的問題。
跟往常一樣,本文也盡可能結合面試題來重點講解下 equals 和 hashCode 的使用以及意義。
概述
首先 equals
和 hashCode
兩個方法屬于 Object 基類的方法:
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
可以看出 equals
方法默認比較的是兩個對象的引用是否指向同一個內存地址。而 hashCode
這是一個 native 本地方法,其實默認的 hashCode
方法返回的就是對象對應的內存地址。
hasCode 方法的注釋這樣說的: This is typically implemented by converting the internal address of the object into an integer,
這一點我們通過 toString
方法也可以間接了解,我們都知道 toString 返回的是「類名@十六進制內存地址」,由源碼可以看出內存地址與 hashCode()
返回值相同。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
面試題目:
hashCode
方法返回的是對象的內存地址么?
答: Object 基類的hashCode
方法默認返回對象的內存地址,但是在一些場景下我們需要覆寫hashCode
函數,比如需要使用Map
來存放對象的時候,覆寫后hashCode
就不是對象的內存地址了。
equals 詳解
equals
方法既然是基類 Object
的方法,我們創建的所有的對象都擁有這個方法,并有權利去重寫這個方法。該方法返回一個 boolean 值,代表比較的兩個對象是否相同,這里的相同的條件由重寫 equals
方法的類來解決。比如我們都知道 :
String str1 = "abc";
String str2 = "abc";
str1.equals(str2);//true
顯然 String
類一定重寫了 equals
方法否則兩個 String
對象內存地址肯定不同。我們簡單看下 String
類的 equals
方法:
public boolean equals(Object anObject) {
//首先判斷兩個對象的內存地址是否相同
if (this == anObject) {
return true;
}
// 判斷連個對象是否屬于同一類型。
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//長度相同的情況下逐一比較 char 數組中的每個元素是否相同
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
從源碼我們也可以看出, equals
方法已經不單單是調用 this==obj
來判斷對象是否相同了。事實上所有 Java 定義好的一些現有的引用數據類型都重寫了該方法。當我們自己定義引用數據類型的時候我們應該依照什么原則去判定兩個對象是否相同,這就需要我們自己來根據業務需求來把握。但是我們都需要遵循以下規則:
自反性(reflexive)。對于任意不為 null 的引用值 x,
x.equals(x)
一定是 true。對稱性(symmetric)。對于任意不為 null 的引用值 x 和 y ,當且僅當
x.equals(y)
是 true 時,y.equals(x)
也是true。傳遞性(transitive)。對于任意不為 null 的引用值x、y和z,如果
x.equals(y)
是 true,同時y.equals(z)
是 true,那么x.equals(z)
一定是 true。一致性(consistent)。對于任意不為null的引用值x和y,如果用于equals比較的對象信息沒有被修改的話,多次調用時
x.equals(y)
要么一致地返回 true 要么一致地返回 false。對于任意不為 null 的引用值 x,
x.equals(null)
返回 false。
equals vs ==
說到 equals
怎么能不說 ==
,其實兩個在初學 Java 的時候給新手還是帶來了蠻多困惑的。對于這兩個的區別需要看比較的對象是什么樣的類型。
我們都知道 Java 數據類型可分為 基本數據類型 和 引用數據類型。基本數據類型包括 byte, short, int , long , float , double , boolen ,char
八種。對于基本數據類型 == 操作符判斷的是左右兩邊變量的值:
int a = 10;
int b = 10;
float c = 10.0f;
//以下輸出結果均為 true
System.out.println("(a == b) = " + (a == b));
System.out.println("(b == c) = " + (b == c));
而對于引用數據類型 == 操作符判斷就是等號兩邊的指向的對象的內存地址是否相同。也就是說通過 == 判斷的兩個引用數據類型變量,如果相同,則他們指向的肯定是同一個對象。
EntryClass entryClass1 = new EntryClass(1);
EntryClass entryClass2 = new EntryClass(1);
EntryClass entryClass3 = entryClass1;
// (entryClass1 == entryClass2) = false
System.out.println(" (entryClass1 == entryClass2) = " + (entryClass1 == entryClass2));
// (entryClass1 == entryClass3) = true
System.out.println(" (entryClass1 == entryClass3) = " + (entryClass1 == entryClass3));
equals 與 == 操作符的區別總結如下:
若 == 兩側都是基本數據類型,則判斷的是左右兩邊操作數據的值是否相等
若 == 兩側都是引用數據類型,則判斷的是左右兩邊操作數的內存地址是否相同。若此時返回 true , 則該操作符作用的一定是同一個對象。
Object 基類的 equals 默認比較兩個對象的內存地址,在構建的對象沒有重寫 equals 方法的時候,與 == 操作符比較的結果相同。
equals 用于比較引用數據類型是否相等。在滿足equals 判斷規則的前體系,兩個對象只要規定的屬性相同我們就認為兩個對象是相同的。
hashCode 方法
hashCode
方法并沒有 equals
方法使用的那么頻繁,說道 hashCode 方法就不得不結合 Java 的 Map 容器,類似于 HashMap
這種使用了哈希算法容器會根據對象的hashCode
返回值來初步確定對象在容器中的位置,然后內部再根據一定的 hash 算法來實現元素的存取。
hash 法簡介
hash 算法,又被成為散列算法,基本上,哈希算法就是將對象本身的鍵值,通過特定的數學函數運算或者使用其他方法,轉化成相應的數據存儲地址的。而哈希法所使用的數學函數就被稱為 『哈希函數』又可以稱之為散列函數。
說了這么多定義的東西,那這個 hash 算法究竟是干什么用的呢 ?我們可以通過一個例子來說明:
如果我們要在存放了的元素{0,4,6,9,28}
的數組中找到數值等于 6 的值的索引我們會怎么做?我們是不是需要遍歷一遍數組才能拿到對應的索引。在數組較大的時候這往往是低效率的。
如果我們能在數組存放的時候就按一定的規則放入元素,在我們想找某個元素的時候在根據之前定好的規則,就可以很快的得到我們想要的結果了。換句話說之前我們在數組中存放元素的順序可能是依照添加順序進行的,但是如果我們是按照一種既定的數學函數運算得到要放入元素的值,和數組角標的映射關系的話。那么我們在想取某個值的元素的時候就使用映射關系就可以找到對應的角標了。
在常見的 hash 函數中有一種最簡單的方法交「除留余數法」,操作方法就是將要存入數據除以某個常數后,使用余數作為索引值。 下面看個例子:
將 323 ,458 ,25 ,340 ,28 ,969, 77 使用「除留余數法」存儲在長度為11的數組中。我們假設上邊說的某個常數即為數組長度11。 每個數除以11以后存放的位置如下圖所示:
試想一下我們現在想要拿到 77 在數組中的位置,是不是只需要 arr[77%11] = 77
就可以了。
但是上述簡單的 hash 算法,缺點也是很明顯的,比如 77 和 88 對 11 取余數得到的值都是 0,但是角標為 0 位置已經存放了 77 這個數據,那88就不知道該去哪里了。上述現象在哈希法中有個名詞叫碰撞:
碰撞:若兩個不同的數據經過相同哈希函數運算后,得到相同的結果,那么這種現象就做碰撞。
于是在設計 hash 函數的時候我們就要盡可能做到:
- 降低碰撞的可能性
- 盡量將要存入的元素經過 hash 函數運算后的結果,盡量能夠均勻的分布在指定的容器(我們在稱之為桶)。
hashCode 方法 與 hash 算法的關系
其實 Java 中的有所的對象又擁有 hashCode 方法其實就是一種 hash 算法,只是有的類覆寫好提供給我們了,有些就需要我們手動去覆寫。比如我們可以看一下 String 提供給我們的 hashCode 算法:
public int hashCode() {
int h = hash;//默認是0
if (h == 0 && value.length > 0) {
char val[] = value;
// 字符串轉化的 char 數組中每一個元素都參與運算
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
前文說了 hashCode 方法與 java 中使用散列表的集合類息息相關,我們拿 Set 來舉例,我們都知道 Set 中是不允許存放重復的元素的。那么我們憑借什么來判斷已有的 Set 集合中是否有何要存入的元素重復的元素呢?有人可能會說我們可以通過 equals 來判斷兩個元素是否相同。那么問題又來,如果 Set 中已經有 10000個元素了,那么之后在存入一個元素豈不是要調用 10000 次 equals 方法。顯然這不合理,性能低到令人發指。那要怎么辦才能保證即高效又不重復呢?答案就在于 hashCode 這個函數。
經過之前的分析我們知道 hash 算法是使用特定的運算來得到數據的存儲位置的,那么 hashCode 方法就充當了這個特定的函數運算。這里我們可以簡單認為調用 hashCode 方法后得到數值就是元素的存儲位置(其實集合內部還做了進一步的運算,以保證盡可能的均勻分布在桶內)。
當 Set 需要存放一個元素的時候,首先會調用 hashCode 方法去查看對應的地址上有沒有存放元素,如果沒有則表示 Set 中肯定沒有相同的元素,直接存放在對應位置就好,但是如果 hashCode 的結果相同,即發生了碰撞,那么我們在進一步調用該位置元素的 equals 方法與要存放的元素進行比較,如果相同就不存了,如果不相同就需要進一步散列其它的地址。這樣我們就可以盡可能高效的保證了無重復元素的方法。
面試題: hashCode 方法的作用和意義
答: 在 Java 中 hashCode 的存在主要是用于提高容器查找和存儲的快捷性,如 HashSet, Hashtable,HashMap 等,hashCode是用來在散列存儲結構中確定對象的存儲地址的,
hashCode 和 equals 方法的關系
翻看Object 類對于 equals 方法的注釋上有這這么一條:
請注意,當這個方法被重寫時,通常需要覆蓋{@code hashCode}方法,以便維護{@code hashCode}方法的一般契約,該方法聲明相等對象必須具有相等的哈希碼.
可以看到如果我們出于某種原因復寫了 equals 方法我們需要按照約定去覆寫 hashCode 方法,并且使用 equals 比較相同的對象,必須擁有相等的哈希碼。
Object 對于 hashCode 方法也有幾條要求:
- 在 Java 應用程序執行期間,在對同一對象多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是將對象進行 equals 比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。
- 如果根據 equals(Object) 方法,兩個對象是相等的,那么對這兩個對象中的每個對象調用 hashCode 方法都必須生成相同的整數結果。
- 如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那么對這兩個對象中的任一對象上調用 hashCode 方法 不要求 一定生成不同的整數結果。但是,程序員應該意識到,為不相等的對象生成不同整數結果可以提高哈希表的性能。
結合 equals 方法的,我們可以做出如下總結:
調用 equals 返回 true 的兩個對象必須具有相等的哈希碼。
如果兩個對象的 hashCode 返回值相同,調用它們 equals 方法不一返回 true 。
我們先來看下第一個結論:調用 equals 返回 true 的兩個對象必須具有相等的哈希碼。為什么這么要求呢?比如我們還拿 Set 集合舉例,Set 首先會調用對象的 hashCode 方法尋找對象的存儲位置,如果兩個相同的對象調用 hashCode 方法得到的結果不同,那么造成的后果就是 Set 中存儲了相同的元素,而這樣的結果肯定是不對的。所以就要求 調用 equals 返回 true 的兩個對象必須具有相等的哈希碼。
那么第二條為什么 hashCode
返回值相同,兩個對象卻不一定相同呢?這是因為,目前沒有完美的 hash 算法能夠完全的避免 「哈希碰撞」,既然碰撞是無法完全避免的所以兩個不相同的對象總有可能得到相同的哈希值。所以我們只能盡可能的保證不同的對象的 hashCode
不相同。事實上,對于 HashMap
在存儲鍵值對的時候,就會發生這樣的情況,在 JDK 1.7 之前,HashMap
對鍵的哈希值碰撞的處理方式,就是使用所謂的‘拉鏈法’。 具體實現會在之后分析 HashMap
的時候說到。
總結
本文總結了 equals 方法和 hashCode 方法的作用和意義。并學習了在覆寫這兩個方法的時候需要注意的要求。需要注意的是,關于這兩個方法在面試的時候還是很有可能被問及的所以,我們至少要明白:
-
hashCode
返回值不一定對象的存儲地址,比如發生哈希碰撞的時候。 - 調用
equals
返回 true 的兩個對象必須具有相等的哈希碼。 - 如果兩個對象的
hashCode
返回值相同,調用它們equals
方法不一返回 true 。