1. 程式人生 > >淺談Java中物件的==、equals和hashCode

淺談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!"的引用,地址不同所以不相等。

這裡有個字面量的概念,而字面量是分配在常量池中的,需要了解的請戳:

聊聊Java的常量池

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&trade; 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結構準備的,個人覺得有以下兩方面:

  1. 計算下標:
    看過HashMap原始碼的朋友應該知道,HashMap實際上是一個物件陣列,而陣列的下標是通過hashCode、HashMap的大小及位運算而來的(這裡就不展開);
  2. 判斷衝突:
    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