java基礎--HashMap解決hash衝突的方法
在Java程式語言中,最基本的結構就是兩種,一種是陣列,一種是模擬指標(引用),所有的資料結構都可以用這兩個基本結構構造,HashMap也一樣。當程式試圖將多個 key-value 放入 HashMap 中時,以如下程式碼片段為例:
HashMap<String,Object> m=new HashMap<String,Object>();m.put("a", "rrr1");
m.put("b", "tt9");
m.put("c", "tt8");
m.put("d", "g7");
m.put("e", "d6");
m.put("f", "d4");
m.put("g", "d4");
m.put("h", "d3");
m.put("i", "d2");
m.put("j", "d1");
m.put("k", "1");
m.put("o", "2");
m.put("p", "3");
m.put("q", "4");
m.put("r", "5");
m.put("s", "6");
m.put("t", "7");
m.put("u", "8");
m.put("v", "9");
HashMap 採用一種所謂的“Hash 演算法”來決定每個元素的儲存位置。當程式執行 map.put(String,Obect)方法 時,系統將呼叫String的 hashCode() 方法得到其 hashCode 值——每個 Java 物件都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。得到這個物件的 hashCode 值之後,系統會根據該 hashCode 值來決定該元素的儲存位置。原始碼如下:
Java程式碼- public V put(K key, V value) {
- if (key == null)
- return
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- //判斷當前確定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那麼新值覆蓋原來的舊值,並返回舊值。
- //如果存在相同的hashcode,那麼他們確定的索引位置就相同,這時判斷他們的key是否相同,如果不相同,這時就是產生了hash衝突。
- //Hash衝突後,那麼HashMap的單個bucket裡儲存的不是一個 Entry,而是一個 Entry 鏈。
- //系統只能必須按順序遍歷每個 Entry,直到找到想搜尋的 Entry 為止——如果恰好要搜尋的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),
- //那系統必須迴圈到最後才能找到該元素。
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, key, value, i);
- returnnull;
- }
上面程式中用到了一個重要的內部介面:Map.Entry,每個 Map.Entry 其實就是一個 key-value 對。從上面程式中可以看出:當系統決定儲存 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的儲存位置。這也說明了前面的結論:我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的儲存位置之後,value 隨之儲存在那裡即可.HashMap程式經過我改造,我故意的構造出了hash衝突現象,因為HashMap的初始大小16,但是我在hashmap裡面放了超過16個元素,並且我遮蔽了它的resize()方法。不讓它去擴容。這時HashMap的底層陣列Entry[] table結構如下:
Hashmap裡面的bucket出現了單鏈表的形式,散列表要解決的一個問題就是雜湊值的衝突問題,通常是兩種方法:連結串列法和開放地址法。連結串列法就是將相同hash值的物件組織成一個連結串列放在hash值對應的槽位;開放地址法是通過一個探測演算法,當某個槽位已經被佔據的情況下繼續查詢下一個可以使用的槽位。java.util.HashMap採用的連結串列法的方式,連結串列是單向連結串列。形成單鏈表的核心程式碼如下:
Java程式碼- void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry<K,V> e = table[bucketIndex];
- table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
- if (size++ >= threshold)
- resize(2 * table.length);
- bsp;
上面方法的程式碼很簡單,但其中包含了一個設計:系統總是將新新增的 Entry 物件放入 table 陣列的 bucketIndex 索引處——如果 bucketIndex 索引處已經有了一個 Entry 物件,那新新增的 Entry 物件指向原有的 Entry 物件(產生一個 Entry 鏈),如果 bucketIndex 索引處沒有 Entry 物件,也就是上面程式程式碼的 e 變數是 null,也就是新放入的 Entry 物件指向 null,也就是沒有產生 Entry 鏈。
HashMap裡面沒有出現hash衝突時,沒有形成單鏈表時,hashmap查詢元素很快,get()方法能夠直接定位到元素,但是出現單鏈表後,單個bucket 裡儲存的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜尋的 Entry 為止——如果恰好要搜尋的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須迴圈到最後才能找到該元素。
當建立 HashMap 時,有一個預設的負載因子(load factor),其預設值為 0.75,這是時間和空間成本上一種折衷:增大負載因子可以減少 Hash 表(就是那個 Entry 陣列)所佔用的記憶體空間,但會增加查詢資料的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);減小負載因子會提高資料查詢的效能,但會增加 Hash 表所佔用的記憶體空間。