一道面試題看 HashMap 的儲存方式

jobbole發表於2014-05-18

  我們公司招人喜歡問演算法題和一些基礎知識。今天我們一個面試官在面試候選人之前在辦公室對我們說他準備問一個這樣的問題:

  在HashMap 中存放的一系列鍵值對,其中鍵為某個我們自定義的型別。放入 HashMap 後,我們在外部把某一個 key 的屬性進行更改,然後我們再用這個 key 從 HashMap 裡取出元素,這時候 HashMap 會返回什麼?

  我們辦公室幾個人答案都不一致,有的說返回null,有的說能正常返回value。但不論答案是什麼都沒有確鑿的理由。我覺得這個問題挺有意思的,就寫了程式碼測試。結果是返回null。需要說明的是我們自定義的類重寫了 hashCode 方法。我想這個結果還是有點意外的,因為我們知道 HashMap 存放的是引用型別,我們在外面把 key 更新了,那也就是說 HashMap 裡面的 key 也更新了,也就是這個 key 的 hashCode 返回值也會發生變化。這個時候 key 的 hashCode 和 HashMap 對於元素的 hashCode 肯定一樣,equals也肯定返回true,因為本來就是同一個物件,那為什麼不能返回正確的值呢?

  先來看看一段測試程式碼:

  先解釋一下測試程式碼做到事。定義了一個person類,就兩個屬性。重寫了 hashCode 方法,還有一套geter和seter,沒什麼特別。測試類裡面先建立了三個person物件作為 key 。列印各個 key 的 hashCode 值。然後三個元素放到 HashMap ,接著更新其中一個 key 的name屬性,最後去取這個 key 的value。

public class Person {
 
    private String name;
    private int height;
 
    @Override
    public int hashCode() {
        System.out.println(this.name + ": HashCode() invoked!");
        return this.name.hashCode() + this.height;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getHeight() {
        return height;
    }
 
    public void setHeight(int height) {
        this.height = height;
    }
 
    @Override
    public String toString() {
        return "Name:" + this.name + "; height:" + this.height;
    }
}
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
 
public class HashmapTest {
 
    public static void main(String[] args) {
 
        Map<Person, String> testMap = new HashMap<Person, String>();
 
        Person p1 = new Person();
        p1.setName("Jakie");
        p1.setHeight(165);
 
        Person p2 = new Person();
        p2.setName("Jerry");
        p2.setHeight(175);
 
        Person p3 = new Person();
        p3.setName("Torres");
        p3.setHeight(160);
 
        System.out.println(p1 + ";hashcode:" + p1.hashCode() + "\n");
        System.out.println(p2 + ";hashcode:" + p2.hashCode() + "\n");
        System.out.println(p3 + ";hashcode:" + p3.hashCode() + "\n");
 
        System.out.println("************************");
        System.out.println("putting object into map");
        testMap.put(p1, "p1");
        testMap.put(p2, "p2");
        testMap.put(p3, "p3");
 
        System.out.println("************************");
        p2.setName("Jerry is now kelly");
 
        System.out.println("P2 hashcode after update:");
        System.out.println(p2 + ";hashcode:" + p2.hashCode() + "\n");
 
        System.out.println("**************************");
        System.out.println("Hash Code of elements in HashMap");
        for (Entry<Person, String> entry : testMap.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue() + ":"
                    + entry.getKey().hashCode());
            System.out.println();
            if (entry.getKey().getName().equals("Jakie")) {
                System.out.println("Jakie in map is the original jakie "
                        + (entry.getKey() == p1));
            } else if (entry.getKey().getName().equals("Jerry is now kelly")) {
                System.out
                        .println("Jerry is now kelly in map is the original Jerry "
                                + (entry.getKey() == p2));
            }
        }
 
        System.out.println("**********************");
        String p = testMap.get(p2);
        System.out.println("Final Result:" + p);
    }
}

  輸出:

Name:Jakie; height:165;hashcode:71336629
 
Name:Jerry; height:175;hashcode:71462829
 
Name:Torres; height:160;hashcode:-1784098647
 
************************
putting object into map
************************
P2 hashcode after update:
Name:Jerry is now kelly; height:175;hashcode:-711681872
 
**************************
Hash Code of elements in HashMap
Name:Jerry is now kelly; height:175:p2:-711681872
 
Jerry is now kelly in map is the original Jerry true
Name:Jakie; height:165:p1:71336629
 
Jakie in map is the original jakie true
Name:Torres; height:160:p3:-1784098647
 
**********************
Final Result:null

  從輸出我們可以知道, key 更新後 hashCode 確實更新了。而且 HashMap 裡面的物件就是我們原來的物件。最後的結果是null。

  我們來看一下 HashMap 的get方法原始碼:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

  可以看到先取得了一個table,這個table實際上是個陣列。然後在table裡面找對應 key 的value。找的標準就是hash等於傳入引數的hash, 並且滿足另外兩個條件之一:k = e.key,也就是說他們是同一個物件,或者傳入的 key 的equal目標的 key 。我們的問題出在那個hash(key.hashCode()),可以看到 HashMap 在儲存元素時是把 key 的 hashCode 再做了一次hash。得到的hash將最終作為元素儲存位置的依據。對應到我們的情況:第一次儲存時,hash函式採用key.hashCode作為引數得到了一個值,然後根據這個值把元素存到了某個位置。

  當我們再去取元素的時候,key.hashCode的值已經出現了變化,所以這裡的hash函式結果也發生了變化,所以當它嘗試去獲得這個 key 的儲存位置時就不能得到正確的值,導致最終找不到目標元素。要想能正確返回,很簡單,把Person類的 hashCode 方法改一下,讓它的 hashCode 不依賴我們要修改的屬性,但實際開發中肯定不能這麼幹,我們總是希望當兩個物件的屬性不完全相同時能返回不同的 hashCode 值。所以結論就是當把物件放到 HashMap 後,不要去修改 key 的屬性。

  以上都是很基礎的東西,但或許我們很多時候都沒注意到,瞭解這些基礎可以避免一些很詭異的bug。純屬拋磚引玉,如有謬誤請海涵和指出。

  本文作者: 伯樂線上 - 梧桐

相關文章