面试必会:HashMap 实现原理解读
??點擊上方?好好學(xué)java?,選擇?星標(biāo)?公眾號
重磅資訊、干貨,第一時間送達(dá) 今日推薦:用好Java中的枚舉,真的沒有那么簡單!個人原創(chuàng)+1博客:點擊前往,查看更多 作者:馮立彬 blog.csdn.net/fenglibing/article/details/91565912HashMap是Java開發(fā)當(dāng)中使用得非常多的一種數(shù)據(jù)結(jié)構(gòu),因為其可以快速的定位到需要查找到數(shù)據(jù),其最快的速度可以達(dá)到O(1),最差的時候也可以達(dá)到O(n)。本文以Java8中的HashMap做為分析原型,因為不同的JDK版本中的HashMap,可能存在著底層實現(xiàn)上的不一樣。
HashMap是通過數(shù)組存儲所有的數(shù)據(jù),每個元素所存放數(shù)組的下標(biāo),是根據(jù)該存儲元素的key的Hash值與該數(shù)組的長度減去1做與運(yùn)算,如下所示:
index = (length_of_array - 1) & hash_of_the_key;數(shù)組中存放元素的數(shù)據(jù)結(jié)構(gòu)使用了Node和TreeNode兩種數(shù)據(jù)結(jié)構(gòu),在單個Hash值對應(yīng)的存儲元素小于8個時,默認(rèn)值為Node的單向鏈表形式存儲,當(dāng)單個Hash值存儲的元素大于8個時,其會使用TreeNode的數(shù)據(jù)結(jié)構(gòu)存儲。
因為在單個Hash值對應(yīng)的元素小于等于8個時,其查詢時間最差為O(8),但是當(dāng)單個Hash值對應(yīng)的元素大于8個時,再通過Node的單向鏈表的方式進(jìn)行查詢,速度上就會變得更慢了;這個時候HashMap就會將Node的普通節(jié)點轉(zhuǎn)為TreeNode(紅黑樹)進(jìn)行存儲,這是由于TreeNode占用的空間大小約為常規(guī)節(jié)點的兩倍,但是其查詢速度可以得到保證,這個是通過空間換時間了。當(dāng)TreeNode中包括的元素變得比較少時,為了存儲空間的占用,也會轉(zhuǎn)換為Node節(jié)點單向鏈表的方式實現(xiàn),它們之間可以互相轉(zhuǎn)換的。
Node:
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}......}可以看到每個Node中包括了4個屬性,分別為:
hash值:當(dāng)前Node的Hash值 key:當(dāng)前Node的key value:當(dāng)前Node的value next:表示指向下一個Node的指針,相同hash值的Node,通過next進(jìn)行遍歷查找TreeNode:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}......}可以看到TreeNode使用的是紅黑樹(Red Black Tree)的數(shù)據(jù)結(jié)構(gòu),紅黑樹是一種自平衡二叉查找樹,在進(jìn)行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能,即使在最壞情況運(yùn)行時間也是非常良好的,并且在實踐中是非常高效的,它可以在O(log n)時間內(nèi)做查找、插入和刪除等操作,這里的n 是樹中元素的數(shù)目。
以下是一張關(guān)于HashMap存儲結(jié)構(gòu)的示意圖:寫入數(shù)據(jù)(一切皆在注釋中)
其方法如下:
//寫入數(shù)據(jù)public V put(K key, V value) {//首先根據(jù)hash方法,獲取對應(yīng)key的hash值,計算方法見后面return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判斷用戶存放元素的數(shù)組是否為空if ((tab = table) == null || (n = tab.length) == 0)//為空則進(jìn)行初使化,并將初使化后的數(shù)組賦值給變量tab,數(shù)組的長值賦值給變量nn = (tab = resize()).length;//判斷根據(jù)hash值與數(shù)組長度減1求與得到的下標(biāo),//從數(shù)組中獲取元素并將其賦值給變量p(后續(xù)該變量p可以繼續(xù)使用),并判斷該元素是否存在if ((p = tab[i = (n - 1) & hash]) == null)//如果不存在則創(chuàng)建一個新的節(jié)點,并將其放到數(shù)組對應(yīng)的下標(biāo)中tab[i] = newNode(hash, key, value, null);else {//根據(jù)數(shù)組的下標(biāo)取到了元素,并且該元素p且不為空,下面要判斷p元素的類型是Node還是TreeNodeNode<K,V> e; K k;//判斷該數(shù)組對應(yīng)下標(biāo)取到的第一值是不是與正在存入值的hash值相同、//key相等(可能是對象,也可能是字符串),如果相等,則將取第一個值賦值給變量eif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//判斷取的對象是不是TreeNode,如果是則執(zhí)行TreeNode的put方法else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//是普通的Node節(jié)點,//根據(jù)next屬性對元素p執(zhí)行單向鏈表的遍歷for (int binCount = 0; ; ++binCount) {//如果被遍歷的元素最后的next為空,表示后面沒有節(jié)點了,則將新節(jié)點與當(dāng)前節(jié)點的next屬性建立關(guān)系if ((e = p.next) == null) {//做為當(dāng)前節(jié)點的后面的一個節(jié)點p.next = newNode(hash, key, value, null);//判斷當(dāng)前節(jié)點的單向鏈接的數(shù)量(8個)是不是已經(jīng)達(dá)到了需要將其轉(zhuǎn)換為TreeNode了if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//如果是則將當(dāng)前數(shù)組下標(biāo)對應(yīng)的元素轉(zhuǎn)換為TreeNodetreeifyBin(tab, hash);break;}//判斷待插入的元素的hash值與key是否與單向鏈表中的某個元素的hash值與key是相同的,如果是則退出if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//判斷是否找到了與待插入元素的hash值與key值都相同的元素if (e != null) { // existing mapping for keyV oldValue = e.value;//判斷是否要將舊值替換為新值if (!onlyIfAbsent || oldValue == null)//滿足于未指定不替換或舊值為空的情況,執(zhí)行將舊值替換為新值e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}Hash值的計算方法:
// 計算指定key的hash值,原理是將key的hash code與hash code無符號向右移16位的值,執(zhí)行異或運(yùn)算。// 在Java中整型為4個字節(jié)32位,無符號向右移16位,表示將高16位移到低16位上,然后再執(zhí)行異或運(yùn)行,也// 就是將hash code的高16位與低16位進(jìn)行異或運(yùn)行。// 小于等于65535的數(shù),其高16位全部都為0,因而將小于等于65535的值向右無符號移16位,則該數(shù)就變成了// 32位都是0,由于任何數(shù)與0進(jìn)行異或都等于本身,因而hash code小于等于65535的key,其得到的hash值// 就等于其本身的hash code。static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}計算邏輯如下圖所示:讀取數(shù)據(jù)(一切皆在注釋中)
public V get(Object key) {Node<K,V> e;//根據(jù)Key獲取元素if ((e = getNode(hash(key), key)) == null)return null;if (accessOrder)afterNodeAccess(e);return 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語句的第一個判斷條件if ((tab = table) != null //將數(shù)組賦值給變量tab,將判斷是否為null&& (n = tab.length) > 0 //將數(shù)組的長值賦值給變量n&& (first = tab[(n - 1) & hash]) != null) {//判斷根據(jù)hash和數(shù)組長度減1的與運(yùn)算,計算出來的的數(shù)組下標(biāo)的第一個元素是不是為空//判斷第一個元素是否要找的元素,大部份情況下只要hash值太集中,或者元素不是很多,第一個元素往往都是需要的最終元素if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))//第一個元素就是要找的元素,因為hash值和key都相等,直接返回return first;if ((e = first.next) != null) {//如果第一元素不是要找到的元,則判斷其next指向是否還有元素//有元素,判斷其是否是TreeNodeif (first instanceof TreeNode)//是TreeNode則根據(jù)TreeNode的方式獲取數(shù)據(jù)return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {//是Node單向鏈表,則通過next循環(huán)匹配,找到就退出,否則直到匹配完最后一個元素才退出if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}//沒有找到則返回nullreturn null;}總結(jié)
以上是生活随笔為你收集整理的面试必会:HashMap 实现原理解读的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你是一直认为 count(1) 比 co
- 下一篇: 哈,你猜一个 TCP 连接上面能发多少个