java 1.8 HashMap的實現原理
1. hash 表
-
數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找查找等方式,可將查找複雜度提高爲O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)
-
線性鏈表:對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)
-
二叉樹: 對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均複雜度均爲O(logn)。
-
哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)
在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即使用鏈表處理衝突,同一hash值的鍵值對會被放在同一個位桶裏,當桶中元素較多時,通過key值查找的效率較低。
而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8),時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。
-
HashMap: 由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作來講,仍需遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好
2. HashMap幾個關鍵成員變量
-
initialCapacity:初始容量。指的是 HashMap 集合初始化的時候自身的容量。可以在構造方法中指定;如果不指定的話,總容量默認值是 16 。需要注意的是初始容量必須是 2 的冪次方。
-
size:當前 HashMap 中已經存儲着的鍵值對數量,即 HashMap.size()
-
loadFactor:加載因子。所謂的加載因子就是 HashMap (當前的容量/總容量) 到達一定值的時候,HashMap 會實施擴容。加載因子也可以通過構造方法中指定,默認的值是 0.75 。舉個例子,假設有一個 HashMap 的初始容量爲 16 ,那麼擴容的閥值就是 0.75 * 16 = 12 。也就是說,在你打算存入第 13 個值的時候,HashMap 會先執行擴容。
-
threshold:擴容閥值。即 擴容閥值 = HashMap 總容量 * 加載因子。當前 HashMap 的容量大於或等於擴容閥值的時候就會去執行擴容。擴容的容量爲當前 HashMap 總容量的兩倍。比如,當前 HashMap 的總容量爲 16 ,那麼擴容之後爲 32 。
3. JDK1.7中的HashMap實現原理
-
HashMap底層維護一個數組,數組中的每一項都是一個Entry,Entry是HashMap中的一個靜態內部類
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構 int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算
-
HashMap的整體結構如下
-
HashMap構造器: HashMap有4個構造器,其他構造器如果用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值,initialCapacity默認爲16,loadFactory默認爲0.75。
-
鏈表解決hash衝突
-
如果 table 數組爲空時先創建數組,並且設置擴容閥值;
-
如果 key 爲空時,調用 putForNullKey 方法特殊處理;
-
計算 key 的哈希值;
-
根據第三步計算出來的哈希值和當前數組的長度來計算得到該key在數組中的索引,其實索引最後的值就等於 hash%table.length ;如果數組的長度爲16, 那麼hash值爲1、17、33,它們計算後(1%16、17%16、33%16)得到的數組小標都爲1,這個時候就會產生hash衝突,引入鏈表,把1、17、33的用一個鏈表保存起來,這個時候Entry實體裏面就是一個列表。
-
遍歷該數組索引下的整條鏈表,如果之前已經有一樣的 key ,那麼直接覆蓋value
-
如果該 key 之前沒有,那麼就進入addEntry 方法。
-
-
put方法
public V put(K key, V value) { //如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16) if (table == EMPTY_TABLE) { inflateTable(threshold); } //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上 if (key == null) return putForNullKey(value); int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻 int i = indexFor(hash, table.length);//獲取在table中的實際位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗 addEntry(hash, key, value, i);//新增一個entry return null; }
-
如果 table 數組爲空時先創建數組,並且設置擴容閥值;
-
如果 key 爲空時,調用 putForNullKey 方法特殊處理;
-
計算 key 的哈希值;
-
根據第三步計算出來的哈希值和當前數組的長度來計算得到該key在數組中的索引,其實索引最後的值就等於 hash%table.length ;
-
遍歷該數組索引下的整條鏈表,如果之前已經有一樣的 key ,那麼直接覆蓋 value
-
如果該 key 之前沒有,那麼就進入 addEntry 方法。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
當發生哈希衝突並且size大於閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度爲之前數組2倍的新的數組,然後將當前的Entry數組中的元素全部傳輸過去,擴容後的新數組長度爲之前的2倍
-
-
get方法
public V get(Object key) { //如果key爲null,則直接去table[0]處去檢索即可。 if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
get方法通過key值返回對應value,如果key爲null,直接去table[0]處檢索。我們再看一下getEntry這個方法
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //通過key的hashcode值計算hash值 int hash = (key == null) ? 0 : hash(key); //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄 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; }
從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。
4. 重寫了equals(),爲什麼還要重寫hashCode()呢?
hashCode是用於查找使用的,用於定位在數組中的下標,而equals是用於比較兩個對象的是否相等的,我們重寫一個類的equals方法,而它的hashcode不重寫,將導致定位數組下標的時候,定位到不同下標,put進入一個對象,取出來的時候爲null.
public class demo {
private static class Student{
int id;
String userName;
public Student(int id, String userName) {
this.id = id;
this.userName = userName;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()){
return false;
}
Student student = (Student) o;
//兩個對象是否等值,通過id來確定
return this.id == student.id;
}
}
public static void main(String []args){
HashMap<Student,String> map = new HashMap<Student, String>();
Student student = new Student(1,"小明");
//put到hashmap中去
map.put(student,"小明");
//get取出,從邏輯上講應該能輸出“小明”
System.out.println("結果:"+map.get(new Student(1,"小明")));
}
}
結果爲:NULL
5. JDK1.8中的HashMap
5.1 概述
如果說成百上千個節點在hash時發生碰撞,存儲一個鏈表中,那麼如果要查找其中一個節點,那就不可避免的花費O(N)的查找時間,這將是多麼大的性能損失。這個問題終於在JDK8中得到了解決。再最壞的情況下,鏈表查找的時間複雜度爲O(n),而紅黑樹一直是O(logn),這樣會提高HashMap的效率。
JDK7中HashMap採用的是位桶+鏈表的方式,即我們常說的散列鏈表的方式,而JDK8中採用的是位桶+鏈表/紅黑樹(有關紅黑樹請查看紅黑樹)的方式,也是非線程安全的。當某個位桶的鏈表的長度達到某個閥值的時候,這個鏈表就將轉換成紅黑樹。
JDK8中,當同一個hash值的節點數大於8時,將不再以單鏈表的形式存儲了,會被調整成一顆紅黑樹。這就是JDK7與JDK8中HashMap實現的最大區別。
前面產生衝突的那些KEY對應的記錄只是簡單的追加到一個鏈表後面,這些記錄只能通過遍歷來進行查找,但是超過這個閾值後HashMap開始將列表升級成一個二叉樹,使用哈希值作爲樹的分支變量,如果兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹裏。
5.2 數組元素Node
JDK中Entry的名字變成了Node,原因是和紅黑樹的實現TreeNode相關聯。
transient Node<k,v>[] table;//存儲(位桶)的數組</k,v>
//Node是單向鏈表,它實現了Map.Entry接口
static class Node<k,v> implements Map.Entry<k,v> {
final int hash;
final K key;
V value;
Node<k,v> next;
//構造函數Hash值 鍵 值 下一個節點
Node(int hash, K key, V value, Node<k,v> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
5.3 紅黑樹
//紅黑樹
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
TreeNode<k,v> parent; // 父節點
TreeNode<k,v> left; //左子樹
TreeNode<k,v> right;//右子樹
TreeNode<k,v> prev; // needed to unlink next upon deletion
boolean red; //顏色屬性
TreeNode(int hash, K key, V val, Node<k,v> next) {
super(hash, key, val, next);
}
//返回當前節點的根節點
final TreeNode<k,v> root() {
for (TreeNode<k,v> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}