標籤:java集合類 java java編程 java集合源碼解析 源碼
Map如我們所知,儲存的是鍵值對,它的基本單位是實現了Map.Entry<K,V>的Node<K,V>,Node 的屬性如下:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next;}
看定義就能知道它的作用了,能夠看到它儲存了一個指向下一個節點的對象next,由此我們是大概能夠想到它的儲存方式由鏈表進行儲存,其實,通過翻閱資料我們也能夠清楚的知道,HashMap其實是一個鏈表數組。它的結構如:
看到這之後,再看源碼,應該能夠清楚好多。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { /* * HashMap 的預設初識容量 * */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * HashMap的最大容量,假如儲存1 << 30,需要在棧中存在2 ^ 30個指向指定堆 * 空間的對象空間,在32位機下,每個這樣的對象空間為4位元組;也就是說所有的 * 記憶體空間都會被用來儲存Map,而jvm的堆大小和棧大小都是有上限的所以,一般 * 很難達到這麼大 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 預設的載入因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 鏈表數組,它的索引稍後會詳細說明 */ transient Node<K,V>[] table; /** * 使用keySet()和values()時會用到 */ transient Set<Map.Entry<K,V>> entrySet; /** * 儲存了索引值對的數量 */ transient int size; /** * 當前Map的修改次數,保持一致性 */ transient int modCount; /** * 數值為(capacity * load factor),是否進行resize的判斷闕值 * */ int threshold; /** * Map的載入因子 * * @serial */ final float loadFactor; } 它有四個建構函式,如下:
HashMap() HashMap(int initialCapacity) HashMap(int initialCapacity, float loadFactor) HashMap(Map<? extends K,? extends V> m)
假如沒有傳入initialCapacity 或 loadFactor時會使用預設的 1<< 4也就是16 和 0.75兩個參數。
當傳入了initialCapacity 時,不會直接使用該值,而是計算大於他的最小2的 冪值。在jdk1.8相對於之前版本是由改進的:
//jdk 1.8 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; }//jdk 1.7 while (capacity < initialCapacity) capacity <<= 1; 每次想到這兩個代碼都很激動,代碼很美,但也會產生一個疑問,我們在學資料結構的時候,書上教我們雜湊表的大小應該設為大於指定值得最小質數,而這明顯不符合說明,其實,這是為後續好多操作提供了方便。
這樣操作之後n 其實是一個大於指定值得最小2的冪值再少1的值,假如傳入的是9,n的值便是15,單看十進位好像沒什麼,但是化為2進位是1111,全部為1的話,能夠很好的進行很多的操作。
計算出大小,分配空間之後我們便可以進行儲存工作了。
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;}
我們拋開TreeXXX的先不管,只看對table的操作,可以知道,節點在table中的索引其實是通過 hash & (n - 1) ,其中n 其實就是table[]的長度。而hash的計算是通過下列方法的調用得到:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}它能夠保證在計算時,hashCode 高位的可用,使存入元素儘可能分散開來,同也可以觀察到,元素存在的索引值之和key有關。
從整體來看,當傳入值時,通過傳入的hash值與capacity-1進行 & 操作得出索引i,通過訪問p = table[i],判斷p是否為空白或者p.key 是否與key相等,當為空白時,直接插入元素;當p.key == key時,更新value,不相等時,在鏈表最後面插入指定結點。
newNode代碼如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 有一個操作 在我們去面試的時候,面試官可能會高度興趣,resize();
在上代碼之前,我們先來聊聊它的主要操作。
在不考慮極端的情況下,當之前的capacity大於0時,新的capacity會變成之前的兩倍,而闕值也會同樣增長;當之前的capacity小於0,但是闕值大於0,新的capacity就賦值為闕值;capacity和闕值等於0時,會被賦值為他們的預設值。容量規定好了,就需要把之前Map裡的鍵值對存入到新的容器table裡面,因為是通過計算雜湊值確定索引,所以註定不能像List一樣直接進行copy,於是只有重新根據每個Entry中儲存的hash與capacity-1來計算索引。這是我們都會想到的。但是通過翻閱源碼,我們又可以發現它的精妙之處。先附上代碼吧。
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) { 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<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; } 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; }
看完代碼,我們來說說它的精妙,當一個數擴大成之前的兩倍之後例如 8 -> 16,它們減一的值得二進位碼 111 -> 1111;可以看到增加了一個1000,也就是原來容量的大小。當有一個hash值與增大之後的容量減一也就是15進行與運算時,實際上與之前容量為8時的索引也就是變了與1000與的位,將容量擴大成之前的兩倍時,可以看成是增加了一個與之前容器一樣大的容器。很明顯索引數值小的會放在容器的前面,大的放在之後。所以我們只需要計算hash 和 之前容量的值進行與運算的結果是否為1,就能知道原來的值是放在原處還是放在新增容器相對應的位置。只看文字有點難以理解,我們來看個圖片:
假如需要將某個節點重新分配,它只有如上兩種位置可以存放,當它的hash & oldCapacity == 1時,存在New地區;否則在Old地區。同時他也說明,鍵值對仍在固定位置或者New區相對應的固定位置,不可能Old地區中的簡直到跑到Old地區的其他位置。
再看看具體代碼:
首先先看 if(e.next == null),當它的next為null時,說明在這個位置上就只有一個鍵值對,所以,可以直接插入,當不為null時,為了提高效率,每個地區都存在tail 和head 節點,當tail != null時,直接將新節點插入在tail之後,否則加入到 head中。它的hi 和 lo地區就像我們圖中的New和Old地區。
從上可以看到,每次講容量擴大成2的冪級數,是非常漂亮的設計,其中很大的提高了效率。
從源碼分析java集合【HashMap】