Java的equals與hashcode對比分析 | wingjay

最近在閱讀《Effective Java》第3章裏讀到了關於 equals() 和 hashcode() 的一些介紹,這兩個方法是很多Java程序員容易弄混的,因此本文針對這兩個方法的用法和具體實現來做一些介紹。

equals() 與 hashcode() 的用處?

我們一般用equals()來比較兩個對象的邏輯意義上的值是否相同。舉個例子:

class Person {
    String name;
    int age;
    long id;
}

我們現在有兩個Person的對象,person1 和person2,那麼什麼時候這兩個是相等的呢?對於兩個人而言,我們認爲如果他們倆名字、年齡和ID都完全一樣,那麼就是同一個人。也就是說,如果

person1.name = person2.name
person1.age = person2.age
person1.id = person2.id

那麼我們就認爲 person1.equals(person2)=true。這就是表示equals是指二者邏輯意義上相等即可。

而 hashcode() 則是對一個對象進行hash計算得到的一個散列值,它有以下特點:

  1. 對象x和y的hashcode相同,不代表兩個對象就相同(x.equals(y)=true),可能存在hash碰撞;不過hashcode如果不相同,那麼一定是兩個不同的對象
  2. 如果兩個對象的equals()相等,那麼hashcode一定相等。
    所以我們一般可以用hashcode來快速比較兩個對象互異,因爲如果x.hashcode() != y.hashcode(),那麼x.equals(y)=false

equals() 的特性

很多時候我們想要重寫某個自定義object的equals()方法,那麼一定要記住,你的equals()方法必須滿足下面四個條件:

  1. 自反性:對於非null的對象x,必須有 x.equals(x)=true
  2. 對稱性:如果 x.equals(y)=true,那麼y.equals(x)必須也爲true
  3. 傳遞性:如果x.equals(y)=true而且y.equals(z)=true,那麼x.equals(z)必須爲true
  4. 對於非null的對象x,一定有x.equals(null)=false

如何重寫 equals() 方法呢?

一般而言,如果你要重寫 equals() 方法,有下面一套模版代碼可以參考:

  1. 首先使用 == 來判斷兩個對象是否引用相同
  2. 使用 instanceof 來判斷兩個對象是否類型相同
  3. 如果類型相同,則把待比較參數轉型;
  4. 比較兩個對象內部每個邏輯值是否相等,只有全部相等才返回true,或者返回false;
  5. 測試這個方法是否能滿足上面幾個特性。

Java 源碼 String 裏 equals() 和 hashcode() 實現

看完上面的特性和重寫方法你可能有點頭大,下面我們來看一下Java裏的 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;
}

可以看到,上面的方法依次執行了下面的步驟:

  1. 比較引用this == anObject
  2. 判斷類型 anObject instanceof String
  3. 轉型 String anotherString = (String)anObject
  4. 比較邏輯值 對 String 而言,首先要 length 相等 n == anotherString.value.length;然後要每一個字符相等,見代碼,最後返回結果。

下面我寫了一段測試代碼來驗證是否符合上面幾點特性:

private static void testStringEquals() {
    String x = "First";
    String y = "First";
    String z = new String("First");
    System.out.println(x.equals(x));
    System.out.println((x.equals(y) && y.equals(x)));
    if (x.equals(y) && y.equals(x)) {
        System.out.println(x.equals(z));
    }
    System.out.println(x.equals(null));
}

打印結果如下:

true
true
true
false

說明是符合的。

然後我們再看下 hashcode() 的源代碼實現,我們知道,hashcode的含義是計算hash散列值,其實就是對一個對象快速計算一個散列值,用來判異使用:只要 hashcode() 不同,那麼兩個對象一定不同。下面我們看下 String 是如何計算自己的hash值的。

private final char value[]; /** The value is used for character storage. */
private int hash; /** Cache the hash code for the string Default to 0 */

public int hashCode() {
    int h = hash; 
    if (h == 0 && value.length > 0) { 
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

其中用來計算 hashcode 主要是這段代碼

for (int i = 0; i < value.length; i++) {
    h = 31 * h + val[i];
}

其中,value是內部存儲string值的字符數組。計算hashcode的方法就是依次遍歷每一個字符,乘以31後再加上下一個字符。例如"a"的hashcode就是 97;"aa"的hashcode是 31*97+97=3104。因此可以看出,hashcode不同的兩個 String 對象一定不是同一個對象

謹記:重寫 equals() 時要保證:兩個equal的對象一定有相同的hashcode

很多人在重寫 equals() 時忽視了這一點,沒有保證兩個equal的對象具備相同的hashcode,從而導致了奇怪的錯誤。

下面舉一個例子,我先只重寫 PhoneNumberWithoutHashcode 的 equals() 方法:

class PhoneNumberWithoutHashcode {
    final short countryCode;
    final short number;
    public PhoneNumberWithoutHashcode(int countryCode, int number) {
        this.countryCode = (short) countryCode;
        this.number = (short) number;
    }

    @Override
    public boolean equals(Object obj) {
        // 1. check == reference
        if(obj == this) {
            return true;
        }
        // 2. check obj instance
        if (!(obj instanceof PhoneNumberWithoutHashcode))
            return false;

        // 3. compare logic value
        PhoneNumberWithoutHashcode anObj = (PhoneNumberWithoutHashcode) obj;
        return anObj.countryCode == this.countryCode 
                && anObj.number == this.number;
    }        
}

下面我們來創建兩個相同的對象,看看它們的 equals() hashcode() 返回值如何。

private static void test() {
    PhoneNumberWithoutHashcode p1 = new PhoneNumberWithoutHashcode(86, 123123);
    PhoneNumberWithoutHashcode p2 = new PhoneNumberWithoutHashcode(86, 123123);
    System.out.println("p1.equals(p2)=" + p1.equals(p2));
    System.out.println("p1.hashcode()=" + p1.hashCode());
    System.out.println("p2.hashcode()=" + p2.hashCode());    
}

可以得到結果如下:

p1.equals(p2)=true
p1.hashcode()=1846274136
p2.hashcode()=1639705018

可以看出,二者是 equals 的,但是hashcode不一樣。這違背了 Java準則,會導致什麼結果呢?

private static void test() {
    PhoneNumberWithoutHashcode p1 = new PhoneNumberWithoutHashcode(86, 123123);
    PhoneNumberWithoutHashcode p2 = new PhoneNumberWithoutHashcode(86, 123123);
    System.out.println("p1.equals(p2)=" + p1.equals(p2));
    
    HashMap<PhoneNumberWithoutHashcode, String> map = new HashMap<>();
    map.put(p1, "TheValue");
    System.out.println("Result: " + map.get(p2));
}

讀者覺得會打印什麼呢?Result: TheValue 嗎?我們來看下運行結果:

p1.equals(p2)=true
Result:  null

問題來了,p1和p2是equal的,但是確不是同樣的key,至少對於HashMap而言,它們倆不是同一個key,爲什麼呢?

我們看一下 HashMap 是怎麼put和get的吧。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

從這段代碼可以看到,p1 和 p2 被存儲時就計算了一次 hash(key),如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

其實就是調用了 key.hashCode() 方法,而我們知道雖然 p1.equals(p2)=true,但是p1.hashCode() != p2.hashCode(),因此 p1 和 p2 對 HashMap 而言壓根就是兩個 key,當然互相取不到對方的 value了。

那麼要如何改進這個類呢?我們再來實現它的 hashcode 方法吧。

class PhoneNumber {
    protected final short countryCode;
    protected final short number;

    public PhoneNumber(int countryCode, int number) {
        this.countryCode = (short) countryCode;
        this.number = (short) number;
    }

    @Override
    public boolean equals(Object obj) {
        // 1. check == reference
        if (this == obj)
            return true;

        // 2. check obj instance
        if (!(obj instanceof PhoneNumber))
            return false;

        // 3. compare logic value
        PhoneNumber target = (PhoneNumber) obj;
        return target.number == this.number
                && target.countryCode == this.countryCode;
    }

    @Override
    public int hashCode() {
        return (31 * this.countryCode) + this.number;
    }
}

這時我們的測試代碼:

private static void test() {
    PhoneNumber p1 = new PhoneNumber(86, 12);
    PhoneNumber p2 = new PhoneNumber(86, 12);
    System.out.println("p1.equals(p2)=" + p1.equals(p2));
    System.out.println("p1.hashcode()=" + p1.hashCode());
    System.out.println("p2.hashcode()=" + p2.hashCode());

    HashMap<PhoneNumber, String> map = new HashMap<>(2);
    map.put(p1, "TheValue");
    System.out.println("Result: " + map.get(p2));
}

打印結果如下:

p1.equals(p2)=true
p1.hashcode()=88076
p2.hashcode()=88076
Result: TheValue

說明重寫hashcode後就能保證 PhoneNumberHashMap 里正常運行了,畢竟像這種 HashMap HashSet 之類的都要基於對象的hash值。

小結

如果存在遺漏錯誤歡迎讀者提出,謝謝。

wingjay

281665-9ffa921d5b9d214a.jpg
Android技術·面試技巧·職業感悟
發佈了45 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章