HashMap是怎樣存儲和快速查找的


參考:廖雪峰老師的java教程
我們都知道Map是一種鍵值對映射表,可以通過key快速查找對應的value.

以HashMap為例,觀察下面的代碼:

        Map<String ,Integer> map = new HashMap<>();
        map.put("apple",12);
        map.put("pear",10);
        map.put("origin",5);
        map.get("apple");  //12

HashMap之所以能根據key直接拿到value,,原因是它內部通過空間換時間的方法,用一個大數組存儲所有的value,並根據key直接計算出value應該存儲在那個索引:

┌───┐
0 │ │
├───┤
1 │ ●─┼───> Person("Xiao Ming")
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> Person("Xiao Hong")
├───┤
6 │ ●─┼───> Person("Xiao Jun")
├───┤
7 │ │
└───┘
如果key的值為"a",計算得到的索引總為 1 ,因此返回value為Person("Xiao Ming"),如果key的值為 "b",計算得到的索引總為5,因此返回value為Person("Xiao Hong"),這樣就不必遍歷整個數組,即可以直接讀取key對應的value.

當我們使用 key 存取value的時候,就會引起一個問題:

我們放入map的可以是字符串 "a",但是,當我們獲取Map的value時,不一定就是放入的那個key對象.

換句話講,兩個key應該是內容相同,但不一定是同一個對象.

@Test
    public void testHashMap(){
        String key1 = "a";
        Map<String,Integer> map = new HashMap<>();
        map.put(key1,123);
        String key2 = new String("a");
        int i = map.get(key2);
        System.out.println(i);  //123
        System.out.println(key1 == key2); // false  說明key1和key2是兩個對象
        System.out.println(key1.equals(key2));  //true    說明key1的內容和key2相同
    }

因為在Map內部,對key做比較是通過equals()實現的,這一點和List查找元素需要正確覆寫equals()是一樣的,即正確的Map必須保證:作為key的對象必須正確覆寫equals方法.

我們經常使用String 作為 key,因為String 已經正確覆寫equals方法.但如果我們放入的key 是一個自己寫的類,就必須保證正確覆寫equals方法.

我們再思考一下 HashMap 為什么能通過 key 直接計算出 value 存儲的索引.相同的key 對象(使用equals判斷時返回true)必須要計算出相同的索引,否則,相同的key每次取出value就不一定對.

通過key計算索引的方式就是調用key對象的hashCode()方法,它返回一個int整數.HashMap正是通過這個方法直接定位key對應的value的索引,繼而直接返回value.

因此,正確使用Map必須保證:

  • 作為key的對象必須正確覆寫equals()方法,相等的兩個key實例調用equals()必須返回True;
  • 作為key的對象還必須正確覆寫hashCode()方法,且hashCode()方法要嚴格遵循一下規范:
    • 如果兩個對象相等,則兩個對象的hashCode()必須相等;
    • 如果兩個對象不相等,則兩個對象的hashCode()盡量不要相等.

擴展

既然HashMap內部使用了數組,通過計算key的HashCode()直接定位value所在的索引,那么第一個問題就來了:hashCode()返回的int返回高達±21億,先不考慮負數,HashMap內部使用的數組得有多大?

實際上 HashMap初始化默認的數組大小只有 16,任何key,無論它的hashCode()有多大,都可以簡單地通過:

int index = key.hashCode() & 0xf;  //0xf = 15

把索引確定為0~15之間,即永遠不會超出數組范圍,上述算法只是一種最簡單的實現.

第二個問題:如果添加超過16個key-value到HashMap,數組不夠用怎么辦?

添加超過一定數量的key-value時,HashMap會在內部自動擴容,每次擴容一倍,即長度為16的數組擴展為長度為32,相應的,需要重新計算hashCode() 索引位置.例如:對長度為32的數組計算hashCode()對應的索引,計算方式要改為:

int index  = key.hashCode() & 0x1f; // 0x1f = 31

由於擴容會導致重新分布已有的key-value,所以,頻繁擴容對HashMap的性能影響很大.如果我們確定要使用一個10000個key-value的HashMap,更好的方式是創建HashMap時就指定容量:

Map<String,Integer> map = new HashMap<>(10000);

雖然指定容量是10000,但是HashMap內部的數組長度總是 \(2^n\),因此,實際數組長度被初始化為比10000大的16384(\(2^14\))

最后一個為題:如果兩個不相同的key,例如"a" 和"b",他們的hashCode()恰好是相同的(這種情況是完全有可能的,因為不相等的兩個實例,只要求hashCode()盡量不相等),那么,當我們放入:

map.put("a", new Person("Xiao Ming"));
map.put("b", new Person("Xiao Hong"));

由於計算出的數組索引相同,后面放入"Xiao Hong"會不會把"Xiao Ming"覆蓋了?

當然不會!使用Map的時候,只要key不相同,他們映射的value就不會互不干擾.但是,在hashMap內部,確實可能存在不同的key,映射到相同的hashCode(),即相同的數組索引上怎么辦?

我們就假設"a" 和"b" 這兩個key 最終計算出的索引都是5,那么,在HashMap的數組中,實際存儲的不是一個Person實例,而是一個List,它包含 兩個Entry,一個是"a"的映射,一個是"b"的映射:

┌───┐
0 │ │
├───┤
1 │ │
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> List<Entry<String, Person>>
├───┤
6 │ │
├───┤
7 │ │
└───┘
在查找的時候,例如:

Person p = map.get("a");

HashMap內部通過"a"找到的實際上是List<Entry<String,Person>>,它還需要遍歷這個list,並找到一個Entry,它的key字段是"a",才能返回對應的Person實例.

我們把不同的key具有相同的hashCode()的情況稱之為哈希沖突.在沖突的時候,一種最簡單的解決辦法是用List存儲hashCode()相同的key-value.顯然沖突的概率越大,這個list就越長,map的get()方法效率就越低,這就是為什么要盡量滿足條件二:

如果兩個對象不相等,則兩個對象的hashCode()盡量不要相等

hashCode()方法編寫得號,HashMap的工作效率就越高.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM