文章目錄
HsahMap的實現原理
簡要概括
- HashMap 基於 Hash 算法實現的,底層是由數組+鏈表/紅黑樹構成的,我們通過 put(key,value)存儲,get(key)來獲取。當傳入 key 時,HashMap 會根據 key. hashCode() 計算出 hash 值,根據 hash 值將 value 保存在 bucket 裏。當計算出的 hash 值相同時,我們稱之爲 hash 衝突,HashMap 的做法是用鏈表和紅黑樹存儲相同 hash 值的 value。當 hash 衝突的個數比較少時,使用鏈表,否則使用紅黑樹。
HashMap的存取實現
- HashMap通過鍵值對實現存取。
put()
方法:對key做null檢查。如果key是null,會被存儲到table[0],因爲null的hash值總是0。 key的hashcode()方法會被調用,然後計算hash值。hash值用來找到存儲Entry對象的數組的索引。有時候hash函數可能寫的很不好,所以JDK的設計者添加了另一個叫做hash()的方法,它接收剛纔計算的hash值作爲參數。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
Get()
:對key進行null檢查。如果key是null,table[0]這個位置的元素將被返回。
key的hashcode()方法被調用,然後計算hash值。indexFor(hash,table.length)用來計算要獲取的Entry對象在table數組中的精確的位置,使用剛纔計算的hash值。在獲取了table數組的索引之後,會迭代鏈表,調用equals()方法檢查key的相等性,如果equals()方法返回true,get方法返回Entry對象的value,否則,返回null。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
補充:
- HashMap有一個叫做Entry的內部類,它用來存儲key-value對。
- 上面的Entry對象是存儲在一個叫做table的Entry數組中。
- table的索引在邏輯上叫做“桶”(bucket),它存儲了鏈表的第一個元素。
- key的hashcode()方法用來找到Entry對象所在的桶。
- 如果兩個key有相同的hash值,他們會被放在table數組的同一個桶裏面。
- key的equals()方法用來確保key的唯一性。
有關知識的具體解析
一、Map的幾種類型
- Map就是一個值key對應一個value。
- Hashtable(線程安全)和HashMap(非線程安全)在代碼實現上,基本上是一樣的。現在Hashtable已經過時了(小寫的t,因爲sun當時的一個失誤,因爲是JDK1.0的產物,所以不方便改)。
- ConcurrentHashMap也是線程安全的,但性能比HashTable好很多,Hashtable是鎖整個Map對象,而ConcurrentHashMap是鎖Map的部分結構。
二、什麼是哈希表?
- 利用數組尋址容易,但插入和刪除困難。而鏈表是:尋址困難,插入和刪除容易。而哈希表便綜合兩者的特性,是一種尋址容易,插入刪除也容易的數據結構。
- 哈希表有多種不同的實現方法,最常用的方法—— 拉鍊法,可以理解爲“鏈表的數組”
- 一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。這些元素是按照什麼樣的規則存儲到數組中呢?一般情況是通過
hash(key)%len
獲得,也就是元素的key的哈希值對數組長度取模得到。 - 比如上述哈希表中12%16=12 , 28%16=12 , 108%16=12 , 140%16=12。所以12、28、108,140都存儲在數組下標爲12的位置。
- HashMap其實也是一個線性數組(
Entry[]
)實現的,所以可以理解爲其存儲數據的容器就是一個線性數組。但是一個線性的數組怎麼實現按鍵值對來存取數據呢?這裏HashMap是做了一些處理的。
三、什麼是哈希算法?
- Hash算法雖然被稱爲算法,但實際上它更像是一種思想。Hash算法沒有一個固定的公式,只要符合散列思想的算法都可以被稱爲是Hash算法。
- 哈希(hash)算法又稱爲散列算法,通過hash算法,可以將任意長度的信息轉換成一個固定長度的二進制數據,我們經常會使用十六進制值來表示轉換後的信息。
- 比如,數字123,使用md5的hash算法後,得到十六進制的值:202cb962ac59075b964b07152d234b70
- 哈希算法的特點:
(1)不同的信息,理論上得到的hash值不同,我們稱之爲“無碰撞”,或者發生“碰撞”的概率非常小。
(2)不可逆,hash算法是單向的,從hash值反向推導出原始信息是很困難的。所以,有些系統中,我們可以使用hash算法對密碼進行處理後保存。 - 哈希算法的應用
①
四、什麼是紅黑樹?
- 二叉樹(BST):
①左子樹結點的值小於等於根節點的值。
②右子樹結點的值大於等於根節點的值。
③左右子樹分開來也是單獨的二叉樹。
- 紅黑樹(RBT):紅黑樹是一種自平衡的二叉樹,除了符合二叉樹的基本特徵之外還引入了一些附加的條件。
①節點是紅色或黑色。
②根節點是黑色。
③每個葉子節點都是黑色的空節點(NIL節點)。
④每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)。
⑥從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
五、HashMap 和 Hashtable 有什麼區別?
- HashMap是非線程安全的,HashMap是Map的一個實現類,是將鍵映射到值的對象,不允許鍵值重複。允許空鍵和空值;由於非線程安全,HashMap的效率要較 Hashtable 的效率高一些。
- Hashtable 是線程安全的一個集合,不允許 null 值作爲一個 key 值或者value 值。
- Hashtable是sychronized,多個線程訪問時不需要自己爲它的方法實現同步,而HashMap 在被多個線程訪問的時候需要自己爲它的方法實現同步。
- 一般現在不建議用Hashtable:
①注意是小寫的t,這是sun公司的一個失誤,但是由於是JDK1.0的產物,所以沒有改
②是Hashtable是遺留類,內部實現很多沒優化和冗餘。
③即使在多線程環境下,現在也有同步的ConcurrentHashMap替代,沒有必要因爲是多線程而用HashTable。
如何解決hash衝突
產生hash衝突的原因
- 當我們通過put(key, value)向hashmap中添加元素時,需要通過hash函數確定元素究竟應該放置在數組中的哪個位置,因爲不同的元素可能通過hashcode()計算得到的哈希值相同,那麼不同的元素被放置在了數據的同一個位置時,後放入的元素會以鏈表的形式,插在前一個元素的尾部,這個時候我們稱發生了hash衝突。
解決方法
- 事實上,想讓hash衝突完全不發生,是不太可能的,我們能做的只是儘可能的降低hash衝突發生的概率。
①開放定址法
②鏈地址法(拉鍊法)
Java 中 HashMap 解決 Hash 衝突就是利用了這個方法,具體實現這裏暫時不做詳解,可以參考 Jdk HashMap 源碼進行理解。
③再哈希法
④建立公共溢出區