中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JDK源碼分析(5)之 HashMap 相關

發布時間:2020-08-01 22:50:26 來源:網絡 閱讀:256 作者:沙漏半杯 欄目:編程語言

HashMap作為我們最常用的數據類型,當然有必要了解一下他內部是實現細節。相比于 JDK7 在JDK8 中引入了紅黑樹以及hash計算等方面的優化,使得 JDK8 中的HashMap效率要高于以往的所有版本,本文會詳細介紹相關的優化,但是主要還是寫 JDK8 的源碼。

一、整體結構

1. 類定義

public?class?HashMap<K,V>?extends?AbstractMap<K,V>??implements?Map<K,V>,?Cloneable,?Serializable?{}

JDK源碼分析(5)之 HashMap 相關

可以看到HashMap是完全基于Map接口實現的,其中AbstractMapMap接口的骨架實現,提供了Map接口的最小實現。
HashMap看名字也能猜到,他是基于哈希表實現的(數組+鏈表+紅黑樹):

JDK源碼分析(5)之 HashMap 相關

2. 構造函數和成員變量

public?HashMap(int?initialCapacity)public?HashMap()public?HashMap(Map<??extends?K,???extends?V>?m)public?HashMap(int?initialCapacity,?float?loadFactor)?{??if?(initialCapacity?<?0)????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+?initialCapacity);??if?(initialCapacity?>?MAXIMUM_CAPACITY)
????initialCapacity?=?MAXIMUM_CAPACITY;??if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))????throw?new?IllegalArgumentException("Illegal?load?factor:?"?+?loadFactor);??this.loadFactor?=?loadFactor;??this.threshold?=?tableSizeFor(initialCapacity);
}

HashMap一共有四個構造函數,其主要作用就是初始化loadFactorthreshold兩個參數:

  • threshold:擴容的閾值,當放入的鍵值對大于這個閾值的時候,就會發生擴容;

  • loadFactor:負載系數,用于控制閾值的大小,即threshold = table.length * loadFactor;默認情況下負載系數等于0.75,當它值越大時:哈希桶空余的位置越少,空間利用率越高,同時哈希沖突也就越嚴重,效率也就越低;相反它值越小時:空間利用率越低,效率越高;而0.75是對于空間和效率的一個平衡,通常情況下不建議修改;

但是對于上面構造函數當中this.threshold = tableSizeFor(initialCapacity);,這里的閾值并沒有乘以負載系數,是因為在構造函數當中哈希桶table[]還沒有初始化,在往里put數據的時候才會初始化,而tableSizeFor是為了得到大于等于initialCapacity的最小的2的冪;

transient?Node<K,V>[]?table;????????????//?哈希桶transient?Set<Map.Entry<K,V>>?entrySet;?//?映射關系Set視圖transient?int?size;?????????????????????//?鍵值對的數量transient?int?modCount;?????????????????//?結構修改次數,用于實現fail-fast機制

哈希桶的結構如下:

static?class?Node<K,V>?implements?Map.Entry<K,V>?{??final?int?hash;???????//?用于尋址,避免重復計算
??final?K?key;
??V?value;
??Node<K,V>?next;
??...??public?final?int?hashCode()?{????return?Objects.hashCode(key)?^?Objects.hashCode(value);
??}
}

其中Node<K,V> next還有一個TreeNode子類用于實現紅黑樹,需要注意的是這里的hashCode()所計算的hash值只用于在遍歷的時候獲取hash值,并非尋址所用hash;

二、Hash表

既然是Hash表,那么最重要的肯定是尋址了,在HashMap中采用的是除留余數法,即table[hash % length],但是在現代CPU中求余是最慢的操作,所以人們想到一種巧妙的方法來優化它,即length為2的指數冪時,hash % length = hash & (length-1),所以在構造函數中需要使用tableSizeFor(int cap)來調整初始容量;

/**
?*?Returns?a?power?of?two?size?for?the?given?target?capacity.
?*/static?final?int?tableSizeFor(int?cap)?{??int?n?=?cap?-?1;
??n?|=?n?>>>?1;
??n?|=?n?>>>?2;
??n?|=?n?>>>?4;
??n?|=?n?>>>?8;
??n?|=?n?>>>?16;??return?(n?<?0)???1?:?(n?>=?MAXIMUM_CAPACITY)???MAXIMUM_CAPACITY?:?n?+?1;
}

首先這里要明確:

  • 2的冪的二進制是,1后面全是0

  • 有效位都是1的二進制加1,就可以得到2的冪

以33為例,如圖:

JDK源碼分析(5)之 HashMap 相關

因為int是4個字節32位,所以最多只需要將高位的16位與低位的16位做或運算就可以得到2的冪,而int n = cap - 1;是為了避免cap本身就是2的冪的情況;這個算是真是厲害,看了很久才看明白,實在汗顏。

計算 hash

static?final?int?hash(Object?key)?{??int?h;??return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);
}

這里重新計算hash是因為在hash & (length-1)計算下標的時候,實際只有hash的低位參與的運算容易產生hash沖突,所以用異或是高位的16位也參與運算,以減小hash沖突,要理解這里首先要明白,

  • & 操作之后只會保留下都是1的有效位

  • length-1(2的n次方-1)實際上就是n和1

  • & 操作之后hash所保留下來的也只有低位的n個有效位,所以實際只有hash的低位參與了運算

具體如圖所示:

JDK源碼分析(5)之 HashMap 相關

三、重要方法講解

對于Map而言最重要的當然是GetPut等操作了,所以下面將介紹與之相關的操作;

1. put方法

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;??//?如果沒有初始化哈希桶,就使用resize初始化
??if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????n?=?(tab?=?resize()).length;??//?如果hash對應的哈希槽是空的,就直接放入
??if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????tab[i]?=?newNode(hash,?key,?value,?null);??else?{
????Node<K,V>?e;?K?k;????//?如果已經存在key,就替換舊值
????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
??????e?=?p;????//?如果已經是樹節點,就用putTreeVal遍歷樹賦值
????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);??????????//?如果鏈表長度大于8,則轉換為紅黑樹
??????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1st
????????????treeifyBin(tab,?hash);??????????break;
????????}????????//?找到key對應的節點則跳出遍歷
????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??????????break;
????????p?=?e;
??????}
????}????//?e是最后指向的節點,如果不為空,說明已經存在key,則替換舊的value
????if?(e?!=?null)?{?//?existing?mapping?for?key
??????V?oldValue?=?e.value;??????if?(!onlyIfAbsent?||?oldValue?==?null)
????????e.value?=?value;
??????afterNodeAccess(e);??????return?oldValue;
????}
??}??//?新增節點時結構改變modCount加1
??++modCount;??if?(++size?>?threshold)
????resize();
??afterNodeInsertion(evict);??return?null;
}

具體過程如圖所示:

JDK源碼分析(5)之 HashMap 相關

2. resize方法

final?Node<K,V>[]?resize()?{
??Node<K,V>[]?oldTab?=?table;??int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;??int?oldThr?=?threshold;??int?newCap,?newThr?=?0;??if?(oldCap?>?0)?{????//?如果hash桶已經完成初始化,并且已達最大容量,則直接返回
????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
??????threshold?=?Integer.MAX_VALUE;??????return?oldTab;
????}????//?如果擴大2倍沒有超過最大容量,則擴大兩倍
????else?if?((newCap?=?oldCap?<<?1)?<?MAXIMUM_CAPACITY?&&?oldCap?>=?DEFAULT_INITIAL_CAPACITY)
??????newThr?=?oldThr?<<?1;?//?double?threshold
??}??//?如果threshold已經初始化,則初始化容量為threshold
??else?if?(oldThr?>?0)??????//?initial?capacity?was?placed?in?threshold
????newCap?=?oldThr;??//?如果threshold和哈希桶都沒有初始化,則使用默認值
??else?{????????????????????//?zero?initial?threshold?signifies?using?defaults
????newCap?=?DEFAULT_INITIAL_CAPACITY;
????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
??}??//?重新計算threshold
??if?(newThr?==?0)?{????float?ft?=?(float)newCap?*?loadFactor;
????newThr?=?(newCap?<?MAXIMUM_CAPACITY?&&?ft?<?(float)MAXIMUM_CAPACITY???(int)ft?:?Integer.MAX_VALUE);
??}
??threshold?=?newThr;??@SuppressWarnings({"rawtypes","unchecked"})
??Node<K,V>[]?newTab?=?(Node<K,V>[])new?Node[newCap];
??table?=?newTab;??if?(oldTab?!=?null)?{????for?(int?j?=?0;?j?<?oldCap;?++j)?{
??????Node<K,V>?e;??????if?((e?=?oldTab[j])?!=?null)?{
????????oldTab[j]?=?null;????????//?如果只有一個節點,則直接重新放置節點
????????if?(e.next?==?null)
??????????newTab[e.hash?&?(newCap?-?1)]?=?e;????????//?如果是樹節點,則將紅黑樹拆分后,重新放置
????????else?if?(e?instanceof?TreeNode)
??????????((TreeNode<K,V>)e).split(this,?newTab,?j,?oldCap);????????//?將鏈表拆分為原位置和高位置兩條鏈表
????????else?{?//?preserve?order
??????????Node<K,V>?loHead?=?null,?loTail?=?null;
??????????Node<K,V>?hiHead?=?null,?hiTail?=?null;
??????????Node<K,V>?next;??????????do?{
????????????next?=?e.next;????????????//?節點重新放置后在原位置
????????????if?((e.hash?&?oldCap)?==?0)?{??????????????if?(loTail?==?null)
????????????????loHead?=?e;??????????????else
????????????????loTail.next?=?e;
??????????????loTail?=?e;
????????????}????????????//?節點重新放置后位置+oldCap
????????????else?{??????????????if?(hiTail?==?null)
????????????????hiHead?=?e;??????????????else
????????????????hiTail.next?=?e;
??????????????hiTail?=?e;
????????????}
??????????}?while?((e?=?next)?!=?null);??????????//?放置低位置鏈表
??????????if?(loTail?!=?null)?{
????????????loTail.next?=?null;
????????????newTab[j]?=?loHead;
??????????}??????????//?放置高位置鏈表
??????????if?(hiTail?!=?null)?{
????????????hiTail.next?=?null;
????????????newTab[j?+?oldCap]?=?hiHead;
??????????}
????????}
??????}
????}
??}??return?newTab
}

上面的擴容過程需要注意的是,因為哈希桶長度總是2的冪,所以在擴大兩倍之后原來的節點只可能在原位置或者原位置+oldCap,具體判斷是通過(e.hash & oldCap) == 0實現的;

  • 之前將了 & 操作只保留了都是1的有效位

  • oldCap 是2的n次方,實際也就是在n+1的位置為1,其余地方為0

  • 因為擴容是擴大2倍,實際上也就是在hash上取了 n+1位,那么就只需要判斷多取的第n+1位是否為0

如圖所示:

JDK源碼分析(5)之 HashMap 相關

3. get方法

public?V?get(Object?key)?{
??Node<K,V>?e;??return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;
}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;
}

相較于其他方法get方法就要簡單很多了,只是用hash取到對應的hash槽,在依次遍歷即可。

4. clone方法

public?Object?clone()?{
??HashMap<K,V>?result;??try?{
????result?=?(HashMap<K,V>)super.clone();
??}?catch?(CloneNotSupportedException?e)?{????//?this?shouldn't?happen,?since?we?are?Cloneable
????throw?new?InternalError(e);
??}
??result.reinitialize();
??result.putMapEntries(this,?false);??return?result;
}

對于clone方法這里有一個需要注意的地方,result.putMapEntries(this, false),這里在put節點的時候是用的this,所以這只是淺復制,會影響原map,所以在使用的時候需要注意一下;

至于其他方法還有很多,但大致思路都是一致的,大家可以在看一下源碼。

四、HashMap不同版本對比

1. hash均勻的時候使用get

Number Of RecordsJava 5Java 6Java 7Java 8
10,0004 ms3 ms4 ms2 ms
100,0007 ms6 ms8 ms4 ms
1,000,00099 ms15 ms14 ms13 ms

2. hash不均勻的時候使用get

Number Of RecordsJava 5Java 6Java 7Java 8
10,000197 ms154 ms132 ms15 ms
100,00030346 ms18967 ms19131 ms177 ms
1,000,0003716886 ms2518356 ms2902987 ms1226 ms
10,000,000OOMOOMOOM5775 ms

3. hash均勻的時候使用put

Number Of RecordsJava 5Java 6Java 7Java 8
10,00017 ms12 ms13 ms10 ms
100,00045 ms31 ms34 ms46 ms
1,000,000384 ms72 ms66 ms82 ms
10,000,0004731 ms944 ms1024 ms99 ms

4. hash不均勻的時候使用put

Number Of RecordsJava 5Java 6Java 7Java 8
10,000211 ms153 ms162 ms10 ms
100,00029759 ms17981 ms17653 ms93 ms
1,000,0003527633 ms2509506 ms2902987 ms333 ms
10,000,000OOMOOMOOM3970 ms

從以上對比可以看到 JDK8 的 HashMap 無論 hash 是否均勻效率都要好得多,這里面hash算法的改良功不可沒,并且因為紅黑樹的引入使得它在hash不均勻甚至在所有key的hash都相同的情況,任然表現良好。

總結

  1. 擴容需要重排所有節點特別損耗性能,所以估算map大小并給定一個合理的負載系數,就顯得尤為重要了。

  2. HashMap 是線程不安全的。

  3. 雖然 JDK8 中引入了紅黑樹,將極端hash的情況影響降到了最小,但是從上面的對比還是可以看到,一個好的hash對性能的影響仍然十分重大,所以寫一個好的hashCode()也非常重要。


向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

西宁市| 花垣县| 专栏| 元朗区| 本溪| 台南县| 日土县| 恩平市| 宁远县| 棋牌| 合阳县| 宁陵县| 北京市| 微山县| 黄大仙区| 汪清县| 钟祥市| 临桂县| 旬阳县| 乌兰察布市| 万山特区| 高邮市| 荣成市| 天峨县| 融水| 离岛区| 陕西省| 正宁县| 谢通门县| 巧家县| 米易县| 禹城市| 吴忠市| 彭州市| 杭锦旗| 哈密市| 若尔盖县| 明光市| 镇坪县| 泗水县| 兰州市|