摘要:值得位數(shù)有的次方,如果直接拿散列值作為下標(biāo)訪問主數(shù)組的話,只要算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將得到的散列值與數(shù)組長度做一個與操作。
hashMap簡單介紹
hashMap是面試中的高頻考點,或許日常工作中我們只需把hashMap給new出來,調(diào)用put和get方法就完了。但是hashMap給我們提供了一個絕佳的范例,展示了編程中對數(shù)據(jù)結(jié)構(gòu)和算法的應(yīng)用,例如位運算、hash,數(shù)組,鏈表、紅黑樹等,學(xué)習(xí)hashMap絕對是有好處的。
廢話不多說,要想學(xué)習(xí)hashMap,必先明白其數(shù)據(jù)結(jié)構(gòu)。在java中,最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)就兩種,一種是數(shù)組,另外一個就是模擬指針(引用),一起來看下hashMap結(jié)構(gòu)圖:
從類定義上看,繼承于AbstractMap,并實現(xiàn)Map接口,其實就是里面定義了一些常用方法比如size(),isEmpty(),containsKey(),get(),put()等等,Cloneable,Serializable 的作用在之前l(fā)ist章節(jié)已講述過就不再重復(fù)了,整體來說類定義還是蠻簡單的
public class HashMap源碼分析extends AbstractMap implements Map , Cloneable, Serializable
接下來會帶領(lǐng)大家閱讀源碼,有些不重要的,會咔掉一部分。
//初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默認(rèn)加載因子,用來計算threshold static final float DEFAULT_LOAD_FACTOR = 0.75f; //鏈表轉(zhuǎn)成樹的閾值,當(dāng)桶中鏈表長度大于8時轉(zhuǎn)成樹 static final int TREEIFY_THRESHOLD = 8; //進行resize操作室,若桶中數(shù)量少于6則從樹轉(zhuǎn)成鏈表 static final int UNTREEIFY_THRESHOLD = 6; //當(dāng)桶中的bin樹化的時候,最小hashtable容量,最少是TREEIFY_THRESHOLD 的4倍 static final int MIN_TREEIFY_CAPACITY = 64;
//在樹化之前,桶中的單個bin都是node,實現(xiàn)了Entry接口 static class Node為什么hashMap容量總是2的冪次方?implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } }
//jdk1.8 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hashMap中的hash算法,影響hashMap效率的重要因素之一就是hash算法的好壞。hash算法的好壞,我們可以簡單的通過兩個因素判斷,1是否高效2是否均勻。
大家都知道key.hashCode調(diào)用的是key鍵值類型自帶的哈希函數(shù),返回int散列值。int值得位數(shù)有2的32次方,如果直接拿散列值作為下標(biāo)訪問hashMap主數(shù)組的話,只要hash算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將hash()得到的散列值與數(shù)組長度做一個與操作。如下函數(shù):
//或許大家會發(fā)現(xiàn)這個方法是jdk1.7,為什么不用1.8的呢?那是因為1.8里已經(jīng)去掉這個函數(shù),直接調(diào)用,為了講解方便,我從1.7中找出此方法方便學(xué)習(xí) static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
hashMap的長度必須是2的冪次方,最小為16.順便說一下這樣設(shè)計的好處。因為這樣(length-1)正好是一個高位掩碼,&運算會將高位置0,只保留低位數(shù)字。我們來舉個例子,假設(shè)長度為16,16-1=15,15的2進制表示為
00000000 00000000 00000000 00001111.隨意定義一個散列值做&運算,結(jié)果如所示:
10101010 11110011 00111010 01011010 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00001010
也就是說實際上只截取了最低的四位,也就是我們計算的索引結(jié)果。但是只取后幾位的話,就算散列值分布再均勻,hash碰撞也會很嚴(yán)重,如果hashcode函數(shù)本身不好,分布上成等差數(shù)列的漏洞,使最后幾個低位成規(guī)律性重復(fù),這就無比蛋疼了。這時候hash()函數(shù)的價值就體現(xiàn)出來了
h=key.hashcode() 11111011 01011111 00011100 01011011 h>>>16 ^ 00000000 00000000 11111011 01011111 ------------------------------------- 11111011 01011111 11100111 00000100 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00000100
(h = key.hashCode()) ^ (h >>> 16),16正好是32的一半,其目的是為了將自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,混合高低位信息,以此來加大低位的隨機性,而且混合后的低位存在部分高位的特征,算是變相的保留了高位信息。由此看來jdk1.8對于hash算法和計算索引值的設(shè)計就基本暴露在我們的眼前了,不得不佩服設(shè)計之巧妙。
//返回大于等于cap且距離最近的一個2的冪 //例子:cap=2 return 4; cap=9 return 16; 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; }
//hashMap數(shù)組,保留著鏈表的頭結(jié)點或者紅黑樹的跟結(jié)點。 //當(dāng)?shù)谝淮问褂玫臅r候,會對其初始化,當(dāng)需要擴容時,會調(diào)用resize方法。長度一定是2的冪 transient Nodeget方法解析[] table; //用來遍歷的set集合,速度快于keySet transient Set > entrySet; transient int size; //用來檢測使用iterator期間結(jié)構(gòu)是否發(fā)生變化,變化則觸發(fā)fail-fast機制 transient int modCount; //當(dāng)容器內(nèi)映射數(shù)量達到時,發(fā)生resize操作(threshold=capacity * load factor) int threshold; //加載因子,默認(rèn)0.75 final float loadFactor;
public V get(Object key) { Nodee; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node getNode(int hash, Object key) { Node [] tab; Node 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 )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; }
hash函數(shù)之前已經(jīng)研究過了,直接鎖定getNode()吧。通過hash函數(shù)算出hash值&上數(shù)組長度從而計算出索引值,然后遍歷比較key,返回對應(yīng)值。
put和resize方法解析public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { NodeentrySet和keySet[] tab; Node p; int n, i; //若table為空,則調(diào)用resize()進行初始化,并將長度賦值給n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根據(jù)(n-1)&hash算出索引,得到結(jié)點p,若p為null,則生成一個新的結(jié)點插入。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //若p不為null,則將p和插入結(jié)點的key與其hash值進行比較,若相同將p的引用同時賦值給e Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若不同,且結(jié)點p屬于樹節(jié)點,則調(diào)用putTreeVal() else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); else { //若不同,則將p當(dāng)做鏈表的頭結(jié)點,循環(huán)比較,若為null則新增節(jié)點,且循環(huán)次數(shù)大于等于TREEIFY_THRESHOLD - 1則從鏈表結(jié)構(gòu)轉(zhuǎn)為樹結(jié)構(gòu) 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; } //若鏈表中其中一結(jié)點的key與hash與插入結(jié)點一致,則跳出循環(huán) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果插入的key存在,則替換舊值并返回 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //若插入的key在map中不存在,則判斷size>thresold if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } //初始化數(shù)組和擴容使用 final Node [] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 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 [] newTab = (Node [])new Node[newCap]; table = newTab; //若原數(shù)組不為空,則對其中元素進行重新排列 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node 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 )e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } 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; }
hashMap中遍歷的方式是通過entrySet和keySet,為了保證其效率,建議用entrySet因為他的存儲結(jié)構(gòu)和hashMap一致。hashMap是如何維護entrySet的呢?通過閱讀源碼,發(fā)現(xiàn)在put的時候,并沒有對entrySet進行維護,且源碼中
entrySet方法只是new了個對象,那這個entrySet視圖的數(shù)據(jù)從哪而來呢?
public Set> entrySet() { Set > es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } final class EntrySet extends AbstractSet > { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator > iterator() { return new EntryIterator(); } } final class EntryIterator extends HashIterator implements Iterator > { public final Map.Entry next() { return nextNode(); } } abstract class HashIterator { Node next; // next entry to return Node current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node [] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } final Node nextNode() { Node [] t; Node e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } }
通過閱讀EntrySet,我們發(fā)現(xiàn)其iterator() 調(diào)用了EntryIterator(),而在對其進行實例化的時候會對其父類HashIterator進行實例化,從HashIterator的構(gòu)造方法和nextNode我們發(fā)現(xiàn),其返回的視圖就是作用于table的,所以無需重新開辟內(nèi)存。
總結(jié)本篇文章主要分析hashMap的存儲結(jié)構(gòu),分析了hashMap為什么容量始終是2的冪,分析了其hash算法的好壞和影響其效率的因素,同時也了解到了在put和get時做了哪些操作和其中數(shù)據(jù)結(jié)構(gòu)的變化。最后通過hashMap常見的遍歷方式,得出entrySet是便利效率最高的,且hashMap維護entrySet的方式。通過學(xué)習(xí),發(fā)現(xiàn)hashMap的設(shè)計非常優(yōu)秀,但無奈能力有限,無法將其精妙之處全部剖析開來。
下節(jié)預(yù)告:分析一下并發(fā)下的hashMap有可能造成的閉環(huán)問題和concurrentHashMap
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/70746.html
摘要:所謂拉鏈法就是將鏈表和數(shù)組相結(jié)合。若遇到哈希沖突,則將沖突的值加到鏈表中即可。在編寫程序中,要盡量避免。 目錄: 0-1. 簡介 0-2. 內(nèi)部結(jié)構(gòu)分析 0-2-1. JDK18之前 0-2-2. JDK18之后 0-3. LinkedList源碼分析 0-3-1. 構(gòu)造方法 0-3-2. put方法 0-3-3. get方法 0-3-4. resize方法 ...
摘要:若遇到哈希沖突,則將沖突的值加到鏈表中即可。之后相比于之前的版本,之后在解決哈希沖突時有了較大的變化,當(dāng)鏈表長度大于閾值默認(rèn)為時,將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時間。有序,唯一紅黑樹自平衡的排序二叉樹。 本文是最最最常見Java面試題總結(jié)系列第三周的文章。主要內(nèi)容: Arraylist 與 LinkedList 異同 ArrayList 與 Vector 區(qū)別 HashMap的底層...
摘要:下面總結(jié)一下集合常用的三個子類吧無序,允許為,底層是散列表紅黑樹,非線程同步有序,不允許為,底層是紅黑樹非線程同步迭代有序,允許為,底層是雙向鏈表,非線程同步從結(jié)論而言我們就可以根據(jù)自己的實際情況來使用了。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡單【源碼...
摘要:下面我來簡單總結(jié)一下的核心要點底層結(jié)構(gòu)是散列表數(shù)組鏈表紅黑樹,這一點和是一樣的。是將所有的方法進行同步,效率低下。而作為一個高并發(fā)的容器,它是通過部分鎖定算法來進行實現(xiàn)線程安全的。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡單【源碼剖析】 LinkedHas...
閱讀 2222·2021-11-18 10:02
閱讀 3480·2021-11-15 11:36
閱讀 1116·2019-08-30 14:03
閱讀 725·2019-08-30 11:08
閱讀 2761·2019-08-29 13:20
閱讀 3287·2019-08-29 12:34
閱讀 1375·2019-08-28 18:30
閱讀 1642·2019-08-26 13:34