淺談Java中物件的==、equals和hashCode
目錄
運算子 ==
Java中的==是比較兩個物件在JVM中的地址。
@Test
public void compareAddress() {
String str1 = "Hello World!";
String str2 = "Hello World!";
String str3 = new String ("Hello World!");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
}
上述程式碼中:
- str1 == str2為true,是因為str1和str2都是字串字面值“Hello World!”的引用,指向同一塊地址,所以相等;
- str1 == str3為false,是因為通過new產生的物件在堆中,str3是堆中物件的引用,而是str1是指向字串字面值"Hello World!"的引用,地址不同所以不相等。
這裡有個字面量的概念,而字面量是分配在常量池中的,需要了解的請戳:
equals()
equals()是超類Object中的方法。原始碼如下:
/**
* Indicates whether some other object is "equal to" this one.
* <p>
* The {@code equals} method implements an equivalence relation
* on non-null object references:
* <ul>
* <li>It is <i>reflexive</i>: for any non-null reference value
* {@code x}, {@code x.equals(x)} should return
* {@code true}.
* <li>It is <i>symmetric</i>: for any non-null reference values
* {@code x} and {@code y}, {@code x.equals(y)}
* should return {@code true} if and only if
* {@code y.equals(x)} returns {@code true}.
* <li>It is <i>transitive</i>: for any non-null reference values
* {@code x}, {@code y}, and {@code z}, if
* {@code x.equals(y)} returns {@code true} and
* {@code y.equals(z)} returns {@code true}, then
* {@code x.equals(z)} should return {@code true}.
* <li>It is <i>consistent</i>: for any non-null reference values
* {@code x} and {@code y}, multiple invocations of
* {@code x.equals(y)} consistently return {@code true}
* or consistently return {@code false}, provided no
* information used in {@code equals} comparisons on the
* objects is modified.
* <li>For any non-null reference value {@code x},
* {@code x.equals(null)} should return {@code false}.
* </ul>
* <p>
* The {@code equals} method for class {@code Object} implements
* the most discriminating possible equivalence relation on objects;
* that is, for any non-null reference values {@code x} and
* {@code y}, this method returns {@code true} if and only
* if {@code x} and {@code y} refer to the same object
* ({@code x == y} has the value {@code true}).
* <p>
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
* @param obj the reference object with which to compare.
* @return {@code true} if this object is the same as the obj
* argument; {@code false} otherwise.
* @see #hashCode()
* @see java.util.HashMap
*/
public boolean equals(Object obj) {
return (this == obj);
}
註釋翻譯:
指示其他某個物件是否與此物件“相等”。
equals 方法在非空物件引用上實現相等關係:
- 自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。
- 對稱性:對於任何非空引用值 x 和 y,當且僅當y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。 * 傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回true。
- 一致性:對於任何非空引用值 x 和 y,多次呼叫 x.equals(y) 始終返回 true 或始終返回false,前提是物件上 equals 比較中所用的資訊沒有被修改。
- 對於任何非空引用值 x,x.equals(null) 都應返回false。
Object 類的 equals 方法實現物件上差別可能性最大的相等關係;即,對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個物件時,此方法才返回 true(x == y 具有值 true)。
注意:當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。
程式碼非常簡單,預設的equals()直接使用運算子==,也就是比較物件的地址。
Object是超類,那equals()也就是所有類都可以使用的方法,但是不是所有類都只使用了Object的equals()實現呢?
答案是否定的。
String中的equals()
先看這段程式碼:
@Test
public void strEquals() {
String str1 = new String("Hello World!");
String str2 = new String("Hello World!");
System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // true
}
- str1 == str2為false,這個上面已經說過;
- str1.equals(str2)為ture,如果按Object的equals()應當返回false,因為兩個通過new出來的物件分配在堆中,它們的引用理應不同。但是實際卻和我們想的相反,可見String已經重寫過equals()。
開啟String的equals()原始碼,果不其然它已經被重寫了:
/**
* Compares this string to the specified object. The result is {@code
* true} if and only if the argument is not {@code null} and is a {@code
* String} object that represents the same sequence of characters as this
* object.
*
* @param anObject
* The object to compare this {@code String} against
*
* @return {@code true} if the given object represents a {@code String}
* equivalent to this string, {@code false} otherwise
*
* @see #compareTo(String)
* @see #equalsIgnoreCase(String)
*/
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
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;
}
註釋翻譯:
將此字串與指定的物件比較。當且僅當該引數不為 null,並且是與此物件表示相同字元序列的 String 物件時,結果才為 true。
那就很好理解為什麼上面的程式碼中str1.equals(str2)為ture了,因為這兩個字串擁有相同的字元序列。
Integer中的equals()
原始碼:
/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is an {@code Integer} object that
* contains the same {@code int} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
註釋翻譯:
比較此物件與指定物件。當且僅當引數不為 null,並且是一個與該物件包含相同 int 值的 Integer 物件時,結果為 true。
Long中equals()
原始碼:
/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is a {@code Long} object that
* contains the same {@code long} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
註釋翻譯:
將此物件與指定物件進行比較。當且僅當該引數不是 null,且 Long 物件與此物件包含相同的 long 值時,結果才為 true。
從上面可以看出Long和Integer這種數值型類的equals()實現是非常相似的,類似的還有很多包裝型別:Short、Byte、Float、Double…,這裡就不一一熬述了。如果說==是比較物件的地址,那麼equals()則是比較物件的值。
hashCode()
大家都知道,重寫equals()必須也要重寫hashCode(),那hashCode()究竟是幹嘛的呢?我們在看equals()原始碼的時候也沒看到呼叫hashCode(),它們之間感覺“八竿子打不著”呀!
實踐是檢驗真理的唯一標準,我們直接看Object的hashCode()原始碼:
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
是個本地方法,似乎也看不出什麼么蛾子,但是註釋很多,我們不妨從註釋入手。
註釋翻譯:
返回該物件的雜湊碼值。支援此方法是為了提高雜湊表(例如 java.util.HashMap 提供的雜湊表)的效能。
hashCode 的常規協定是:
- 在 Java 應用程式執行期間,在對同一物件多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是將物件進行 equals 比較時所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。
- 如果根據 equals(Object) 方法,兩個物件是相等的,那麼對這兩個物件中的每個物件呼叫 hashCode 方法都必須生成相同的整數結果。
- 如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼對這兩個物件中的任一物件上呼叫 hashCode 方法不要求一定生成不同的整數結果。但是,程式設計師應該意識到,為不相等的物件生成不同整數結果可以提高雜湊表的效能。
實際上,由 Object 類定義的 hashCode 方法確實會針對不同的物件返回不同的整數。(這一般是通過將該物件的內部地址轉換成一個整數來實現的,但是 Java 程式語言不需要這種實現技巧。)
從註釋理解,此方法是為一些hash結構準備的,個人覺得有以下兩方面:
- 計算下標:
看過HashMap原始碼的朋友應該知道,HashMap實際上是一個物件陣列,而陣列的下標是通過hashCode、HashMap的大小及位運算而來的(這裡就不展開); - 判斷衝突:
HashMap判斷雜湊衝突是先判斷兩個key的hashCode是否相等,如果hashCode不相等就證明了key不相同,不是雜湊衝突。而hashCode相等的兩個key其equals結果為false,這兩個key也是不相同的,但這種是雜湊衝突。這時有些人可能要問了,這直接使用equals方法不就可以判斷雜湊衝突了嗎?確實是可以,但是認真想下兩個長度為n的String型別用equals判斷其是否相同,就必須逐一匹配每個字元是否相同,可見其時間複雜度是O(n),無疑很低效。
JDK設計者為什麼要約定hashCode這些規則,可以看出無論是算雜湊陣列的下標還是判斷雜湊衝突都是有利的,就像註釋裡說的提高雜湊表的效能。
如果根據 equals(Object) 方法,兩個物件是相等的,那麼對這兩個物件中的每個物件呼叫 hashCode 方法都必須生成相同的整數結果。
這句話同時也回答了重寫equals()為什麼同時也要重寫hashCode()。試想一下,如果我們寫了一個類,它的兩個例項呼叫equals()是true的,但各自呼叫它們hashCode()卻出現兩個不同的值。然後我們把這個類作為HashMap的key,會發現這個HashMap用起來有很多莫名其妙的現象,那就是因為我們沒有遵循JDK設計者的約定,導致HashMap沒法正確地判斷Key是雜湊衝突還是同值覆蓋。
大家感興趣可以看一下String、Integer、Long…這些常用類是如何實現hashCode()的。
參考:
- JDK1.8