先說Java之外的,什么是拉鏈法?怎么解決沖突的:
拉鏈法解決沖突的做法是:將所有關鍵字為同義詞的結點鏈接在同一個單鏈表中。
若選定的散列表長度為m,則可將散列表定義為一個由m個頭指針組成的指針數組t[0..m-1]。凡是散列地址為i的結點,均插入到以t為頭指針的單鏈表中。
t中各分量的初值均應為空指針。在拉鏈法中,裝填因子α可以大於1,但一般均取α≤1。
換句話說:HashCode是使用Key通過Hash函數計算出來的,由於不同的Key,通過此Hash函數可能會算的同樣的HashCode,
所以此時用了拉鏈法解決沖突,把HashCode相同的Value連成鏈表. 但是get的時候根據Key又去桶里找,如果是鏈表說明是沖突的,此時還需要檢測Key是否相同
在解釋下,Java中HashMap是利用“拉鏈法”處理HashCode的碰撞問題。
在調用HashMap的put方法或get方法時,都會首先調用hashcode方法,去查找相關的key,當有沖突時,再調用equals方法。
hashMap基於hasing原理,我們通過put和get方法存取對象。
當我們將鍵值對傳遞給put方法時,他調用鍵對象的hashCode()方法來計算hashCode,然后找到bucket(哈希桶)位置來存儲對象。
當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當碰撞發生了,對象將會存儲在鏈表的下一個節點中。
hashMap在每個鏈表節點存儲鍵值對對象。當兩個不同的鍵卻有相同的hashCode時,他們會存儲在同一個bucket位置的鏈表中。
鍵對象的equals()來找到鍵值對。HashMap的put和get方法源碼如下:
/** * Returns the value to which the specified key is mapped, * or if this map contains no mapping for the key. * * 獲取key對應的value */ public V get(Object key) { if (key == null) return getForNullKey(); //獲取key的hash值 int hash = hash(key.hashCode()); // 在“該hash值對應的鏈表”上查找“鍵值等於key”的元素 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; } /** * Offloaded version of get() to look up null keys. Null keys map * to index 0. * 獲取key為null的鍵值對,HashMap將此鍵值對存儲到table[0]的位置 */ private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } /** * Returns <tt>true</tt> if this map contains a mapping for the * specified key. * * HashMap是否包含key */ public boolean containsKey(Object key) { return getEntry(key) != null; } /** * Returns the entry associated with the specified key in the * HashMap. * 返回鍵為key的鍵值對 */ final Entry<K,V> getEntry(Object key) { //先獲取哈希值。如果key為null,hash = 0;這是因為key為null的鍵值對存儲在table[0]的位置。 int hash = (key == null) ? 0 : hash(key.hashCode()); //在該哈希值對應的鏈表上查找鍵值與key相等的元素。 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 != null && key.equals(k)))) return e; } return null; } /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * 將“key-value”添加到HashMap中,如果hashMap中包含了key,那么原來的值將會被新值取代 */ public V put(K key, V value) { //如果key是null,那么調用putForNullKey(),將該鍵值對添加到table[0]中 if (key == null) return putForNullKey(value); //如果key不為null,則計算key的哈希值,然后將其添加到哈希值對應的鏈表中 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; //如果這個key對應的鍵值對已經存在,就用新的value代替老的value。 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
從HashMap的put()和get方法實現中可以與拉鏈法解決hashCode沖突解決方法相互印證。
並且從put方法中可以看出HashMap是使用Entry<K,V>來存儲數據。數據節點Entry的數據結構如下:
// Entry是單向鏈表。 // 它是 “HashMap鏈式存儲法”對應的鏈表。 // 它實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; //指向下一個節點 Entry<K,V> next; final int hash; /** * Creates new entry. * 輸入參數包括"哈希值(h)", "鍵(k)", "值(v)", "下一節點(n)" */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判斷兩個Entry是否相等 // 若兩個Entry的“key”和“value”都相等,則返回true。 // 否則,返回false public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }
從這段代碼中,我們可以看出Entry是一個單向鏈表,這也是我們為什么說HashMap是通過拉鏈法解決hash沖突的原因。Entry實現了Map.Entry接口。