參考:廖雪峰老師的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的工作效率就越高.