layout: post
title: HashMap源碼閱讀
date: 2020-02-02
author: xiepl1997
tags: 源碼閱讀java
下面是JDK11中HashMap的源碼分析,對代碼的分析將主要以註釋的方式來體現。node
HashMap是基於Map接口實現的哈希表,實現了Map接口中的全部操做,並且HashMap容許鍵爲空值,也容許值爲空值,與之對應的是Hashtable,Hashtable不能將鍵和值設置爲空。HashMap不能保證元素的順序,特別是,它不能保證隨着時間的推移保持順序不變。算法
HashMap爲基本操做(get和put)提供了恆定的時間性能,假設散列函數在木桶(buckets)中適當地分散了元素。集合(Collection)的迭代須要的時間與HashMap實例的「容量」和它的大小成比例。所以,若是迭代的性能很重要,要求很高,那麼不將初始容量設置得過高(或負載因素太低)是很是重要的。數組
HashMap是線程不安全的集合,即當多線程訪問時,同一時刻若是沒法保證只有一個線程修改HashMap,則會毀壞HashMap,拋出ConcurrentModificationException。緩存
HashMap底層使用哈希表(數組 + 單鏈表),當鏈表過長會將鏈表轉成紅黑樹以實現O(logn)時間複雜度內查找。
HashMap的定義爲class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable
安全
1.Node
2.KeySet
3.Values
4.EntrySet
5.HashIterator
6.KeyIterator
7.ValueIterator
8.EntryIterator
9.HashMapSpliterator
10.KeySpliterator
11.ValueSpliterator
12.EntrySpliterator
13.TreeNode 表明紅黑樹節點,HashMap中對紅黑樹的操做的方法都在此類中數據結構
HashMap採用的擴容策略是,每次加倍的方式。這樣,原來位置的Entry在新擴展的數組中要麼依然在原來的位置,要麼在原來的位置+原來的容量
的位置。多線程
HashMap經過hash()函數(也叫「擾動函數」)來計算hash值,方法爲key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
,計算出來的hash值存放在Node.hash中。函數
hash的值計算至關於將高16位與底16位進行異或,結果是高16位不變,底16位變成異或的新結果。爲何這樣作呢,緣由是HashMap擴容以前的數組大小才爲16,散列值是不能直接拿來用的。在進行長度取模運算時採用的只是取二進制中的最右端的幾位,並無用到高位二進制的信息,作帶來的結果就是hash結果分佈不太均勻。而將高16位和底16位異或後就可讓低位附帶高位的信息,加大低位的隨機性。源碼分析
在對散列值作完高低位的異或操做後,在對異或結果進行對長度的取模獲得最終的結果。具體參考JDK源碼中HashMap的hash方法原理是什麼?-胖君的回答-知乎
在hash計算中,null的hash值爲0,而後按照正常的putVal()
插入。
從源碼中(下文構造函數)咱們能夠看到,new HashMap()開銷很是少,僅僅確認裝載因子。真正的建立table的操做盡量的日後延遲,這使得HashMap有很多操做都須要檢查table是否初始化。這種設計有一種好處,就是可以沒必要擔憂HashMap的開銷,能夠一次性大量的建立HashMap。
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable { //用於序列化 private static final long serialVersionUID = 362498820763181265L; //HashMap的默認容量是16 static final int DEFAULT_INITIAL_CAPACITY = 16; //最大容量爲1073741824(2的30次方,即1<<30) static final int MAXIMUM_CAPACITY = 1073741824; //默認裝載因子爲0.75f static final float DEFAULT_LOAD_FACTOR = 0.75F; /* 將鏈表轉化爲紅黑樹的閾值爲8,即當鏈表長度 >= 8時,鏈表轉化爲紅黑樹,也就是樹形化。 爲何要樹形化呢?想一下咱們爲何要用HashMap,是由於經過Hash算法在理想狀況下時間複雜度O(1)就能找到元素,特別快,但僅限於理想狀況下,若是遇到了hash碰撞,且碰撞比較頻繁的話,那麼當咱們get一個元素的時候,定位到了這個數組,還須要在這個數組中遍歷一次鏈表最終才能找到要get的元素,是否是已經失去了hashmap的初心了?(由於須要遍歷鏈表,因此時間複雜度就高上去了)。 因此使用紅黑樹這種數據結構來解決鏈表過長的問題,能夠理解爲紅黑樹遍歷比鏈表遍歷快,時間複雜度低。 */ static final int TREEIFY_THRESHOLD = 8; //將紅黑樹轉化成鏈表的閾值爲6(<6時),這個是在resize()的過程當中調用TreeNode.split()實現 static final int UNTREEIFY_THRESHOLD = 6; /* 最小樹形化閾值。要樹化並不只僅是要超過TREEIFY_THRESHOLD,同時容量要超過MIN_TREEIFY_CAPACITY,若是隻是超過TREEIFY_THRESHOLD,則會進行擴容(調用resize())。爲何這個時候是擴容而不是樹形化呢? 緣由就在於,形成鏈表過長也多是數組(桶)過短了也就是容量過小了。舉個例子,若是數組長度爲1,那麼全部的元素都擠在了數組的第0個位置上,這個時候就算樹形化只是治標不治本,由於引發鏈表過長的根本緣由是數組太短。 因此在執行樹形化以前(鏈表長度>=8),會檢查數組長度,若是長度小於64,則對數組進行擴容,而不是樹形化。 */ static final int MIN_TREEIFY_CAPACITY = 64; /* 哈希表的數組主體定義,初始化時,在構造函數中並不會初始化,因此在各類操做中老是要先檢查table是否爲null。 */ transient HashMap.Node<K, V>[] table; /* 做爲一個entrySet緩存,使用entrySet首先檢查其是否爲null,不爲null則使用這個緩存,不然生成一個entrySet並緩存至此。 */ transient Set<Entry<K, V>> entrySet; //HashMap中的Entry的數量 transient int size; /* 記錄修改內部結構化修改次數,用於實現fail-fast,ConcurrentModificationException就是經過檢測這個拋出。 */ transient int modCount; //其值=capacity*loadFactor,當size超過threshold的時候便進行一次擴容 int threshold; //裝載因子 final float loadFactor; …… }
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) { throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); } else { if (initialCapacity > 1073741824) { initialCapacity = 1073741824; } if (loadFactor > 0.0F && !Float.isNaN(loadFactor)) { this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } else { throw new IllegalArgumentException("Illegal load factor: " + loadFactor); } } }
public HashMap(int initialCapacity) { this(initialCapacity, 0.75F); }
public HashMap() { this.loadFactor = 0.75F; }
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = 0.75F; this.putMapEntries(m, false); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //獲取該map實際長度 int s = m.size(); if (s > 0) { //判斷table是否初始化,若是沒有初始化 if (this.table == null) { /**求出須要的容量,由於實際使用的長度=容量*0.75得來的,+1是由於小數相除,基本都不會是整數,容量大小 不能爲小數的,後面轉化爲int,多餘的小數就要被丟掉,因此+1,例如,map實際長度爲29.3,則所須要的容量爲30. */ float ft = (float)s / this.loadFactor + 1.0F; //判斷該容量大小是否超出上限 int t = ft < 1.07374182E9F ? (int)ft : 1073741824; //對臨界值進行初始化,tableSizeFor(t)這個方法會返回大於t值的,且離其最近的2次冪,例如t爲29,則返回的值是32 if (t > this.threshold) { this.threshold = tableSizeFor(t); } } else if (s > this.threshold) { //若是table已經初始化,則進行擴容操做,resize就是擴容 this.resize(); } Iterator var8 = m.entrySet().iterator(); //遍歷,把map中的數據轉移到hashmap中 while(var8.hasNext()) { Entry<? extends K, ? extends V> e = (Entry)var8.next(); K key = e.getKey(); V value = e.getValue(); this.putVal(hash(key), key, value, false, evict); } } }
該構造函數,傳入一個Map,而後把該Map轉爲hashMap,resize方法在下面添加元素的時候會詳細講解,在上面entrySet方法會返回一個Set<Map.Entry<K,V>>,泛型爲Map的內部類Entry,它是一個存放key-value的實例,也就是Map中的每個key-value就是一個Entry實例,爲何使用這個方式進行遍歷,由於效率高,putVal方法把取出來的每一個key-value存入到hashMap中。
hash函數負責產生hashcode,計算方法爲若爲空則返回0,不然返回對key的高16位和底16位的異或的結果。
static final int hash(Object key) { int h; return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16; }
這個方法是判斷傳入的Object對象x是否實現了Comparable接口,若是傳入的是String對象,天然實現了Comparable接口,直接返回就行。可是對於其餘的類,比方說咱們本身寫了一個類對象,而後存在HashMap中,可是就HashMap來講它並不知道咱們有沒有實現Comparable接口,甚至都不知道咱們Comparable接口中有沒有用泛型,泛型具體用的是哪一個類。
static Class<?> comparableClassFor(Object x) { if (x instanceof Comparable) { Class c; if ((c = x.getClass()) == String.class) { return c; } ype[] ts; if ((ts = c.getGenericInterfaces()) != null) { Type[] var5 = ts; int var6 = ts.length; for(int var7 = 0; var7 < var6; ++var7) { Type t = var5[var7]; Type[] as; ParameterizedType p; if (t instanceof ParameterizedType && (p = (ParameterizedType)t).getRawType() == Comparable.class && (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) { return c; } } } } return null; }
若是x爲空,返回0;若是x的類型爲kc,則返回compareTo(x)。
static int compareComparables(Class<?> kc, Object k, Object x) { return x != null && x.getClass() == kc ? ((Comparable)k).compareTo(x) : 0; }
該函數用於計算大於等於cap的的最小的2的整數冪,用於作table的長度。numberOfLeadingZeros()方法的做用是返回無符號整形i的最高非零位前面的0的個數,包括符號位在內。
static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return n < 0 ? 1 : (n >= 1073741824 ? 1073741824 : n + 1); }
public V put(K key, V value) { //四個參數,第一個hash值,第四個參數表示若是該key存在值,若是爲null的話,則插入新的value,最後一個參數,在hashMap中沒有用,能夠不用管。 return this.putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab哈希數組,p爲該哈希桶的首節點,n爲hashMap的長度,i爲計算出的數組下標 HashMap.Node[] tab; int n; //獲取長度並擴容,使用的是懶加載,table一開始是沒有加載的,等puthou纔開始加載 if ((tab = this.table) == null || (n = tab.length) == 0) { n = (tab = this.resize()).length; } Object p; int i; //若是計算出的該哈希桶的位置沒有值,則把新插入的key-value放到此處,此處就算沒有插入成功,也就是發生哈希衝突時也會把哈希桶的首節點賦予p if ((p = tab[i = n - 1 & hash]) == null) { tab[i] = this.newNode(hash, key, value, (HashMap.Node)null); } else { //發生哈希衝突的幾種狀況 //e臨時節點的做用,k存放當前節點的key值 Object e; Object k; //第一種,插入的key-value的hash值,key都與當前節點相等,e=p,則表示爲首節點 if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) { e = p; } //第二種,hash值不等於首節點,判斷該p是否屬於紅黑樹的節點 else if (p instanceof HashMap.TreeNode) { /* 爲紅黑樹的節點,則在紅黑樹中進行添加,若是該節點已經存在,則返回該節點(不爲null), 該值很重要,用來判斷put操做是否成功,若是添加成功返回null */ e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value); } //第三種,hash值不等於首節點,不爲紅黑樹節點,則爲鏈表的節點 else { //遍歷該鏈表 int binCount = 0; while(true) { //若是找到尾部,則代表添加的key-value沒有重複,在尾部進行添加。 if ((e = ((HashMap.Node)p).next) == null) { ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null); //判斷是否要轉化爲紅黑樹結構 if (binCount >= 7) { this.treeifyBin(tab, hash); } break; } //若是鏈表有重複的key,e爲當前重複的節點,結束循環。 if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) { break; } p = e; ++binCount; } } //e不爲null,則說明有重複的key,則用待插入值進行覆蓋,返回舊值。 if (e != null) { V oldValue = ((HashMap.Node)e).value; if (!onlyIfAbsent || oldValue == null) { ((HashMap.Node)e).value = value; } this.afterNodeAccess((HashMap.Node)e); return oldValue; } } /* 到了這步,說明待插入的key-value是沒有key的重複,由於插入成功的e節點的值爲null。 修改次數+1 */ ++this.modCount; //實際長度+1,並判斷是否大於臨界值,大於則擴容 if (++this.size > this.threshold) { this.resize(); } this.afterNodeInsertion(evict); //添加成功 return null; }
擴容方法resize()
final HashMap.Node<K, V>[] resize() { //把沒有插入以前的哈希數組叫作oldTab HashMap.Node<K, V>[] oldTab = this.table; //oldTab的長度 int oldCap = oldTab == null ? 0 : oldTab.length; //oldTab的臨界值 int oldThr = this.threshold; //初始化new的長度和臨界值 int newThr = 0; int newCap; //oldCap>0也就說明不是首次加載,由於hashMap用的是懶加載 if (oldCap > 0) { //若是大於最大值 if (oldCap >= 1073741824) { //將臨界值設置爲整數的最大值 this.threshold = 2147483647; return oldTab; } //位置*。其餘狀況,擴容兩倍,而且擴容後的長度要小於最大值,old的長度也要大於16 if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) { //臨界值也要擴容爲old的2倍 newThr = oldThr << 1; } } /* 若是oldCap<0,可是已經初始化了,像把元素刪除完以後的狀況,那麼它的臨界值確定還存在, 若是是首次初始化,它的臨界值則爲0. */ else if (oldThr > 0) { newCap = oldThr; } //首次初始化,給默認值 else { newCap = 16; newThr = 12; //臨界值等於容量*0.75 } //位置*的補充,也就是初始化時容量小於默認值16的,此時newThr沒有賦值 if (newThr == 0) { //new的臨界值 float ft = (float)newCap * this.loadFactor; //判斷new容量是否大於最大值,臨界值是否大於最大值 newThr = newCap < 1073741824 && ft < 1.07374182E9F ? (int)ft : 2147483647; } //把上面各類狀況分析出的臨界值,在此處進行真正的改變,也就是容量和臨界值都改變了 this.threshold = newThr; //初始化 HashMap.Node<K, V>[] newTab = new HashMap.Node[newCap]; //賦予當前的table this.table = newTab; //此處天然是把old中的元素,遍歷到new中 if (oldTab != null) { for(int j = 0; j < oldCap; ++j) { //臨時變量 HashMap.Node e; //當前哈希桶的位置值不爲null,也就是數組下標處有值,由於有值表示可能會發生衝突 if ((e = oldTab[j]) != null) { //把已經賦值以後的變量置位null,固然是爲了好回收,釋放內存 oldTab[j] = null; //若是下標處的節點沒有下一個元素 if (e.next == null) { //把該變量的值存入newTab中,e.hash & new Cap-1並不等於j newTab[e.hash & newCap - 1] = e; } //若是該節點爲紅黑樹結構,也就是存在hash衝突,該hash桶中有多個元素 else if (e instanceof HashMap.TreeNode) { //把此樹轉移到newTab中 ((HashMap.TreeNode)e).split(this, newTab, j, oldCap); } /* 此處表示爲鏈表結構,一樣把鏈表轉移到newTab中,就是把鏈表遍歷後,把值轉過去,再置位null */ else { HashMap.Node<K, V> loHead = null; HashMap.Node<K, V> loTail = null; HashMap.Node<K, V> hiHead = null; HashMap.Node hiTail = null; HashMap.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; } e = next; } while(next != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } //返回擴容後的hashmap return newTab; }
刪除元素
public V remove(Object key) { //臨時變量 HashMap.Node e; /* 調用removeNode,第三個value表示,把key的節點直接都刪除了,不須要用到值, 若是設爲值,則還須要去進行查找操做。 */ return (e = this.removeNode(hash(key), key, (Object)null, false, true)) == null ? null : e.value; } /* 第一參數爲哈希值,第二個爲key,第三個爲value,第四個爲true的話,則表示刪除它 key對應的value,不刪除key,第四個若是爲false,則表示刪除後,不移動節點。 */ final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { //tab哈希數組,p數組下標節點,n長度,index當前數組下標 HashMap.Node[] tab; HashMap.Node p; int n; int index; //哈希數組不爲null,且長度大於0,而後得到要刪除key的節點的數組下標位置 if ((tab = this.table) != null && (n = tab.length) > 0 && (p = tab[index = n - 1 & hash]) != null) { //node存儲要刪除的節點,e臨時變量,k當前節點的key,v當前節點的value HashMap.Node<K, V> node = null; Object k; //若是數組下標的節點正好是要刪除的節點,把值賦給臨時變量 if (p.hash == hash && ((k = p.key) == key || key != null && key.equals(k))) { node = p; } //也就是要刪除的節點,在鏈表或者紅黑樹上,先判斷是否爲紅黑樹的節點 else { HashMap.Node e; if ((e = p.next) != null) { if (p instanceof HashMap.TreeNode) { //遍歷紅黑樹,找到該節點並返回 node = ((HashMap.TreeNode)p).getTreeNode(hash, key); } //若是是鏈表節點,遍歷找到該節點 else { label88: { while(e.hash != hash || (k = e.key) != key && (key == null || !key.equals(k))) { //p爲要刪除節點的上一個節點 p = e; if ((e = e.next) == null) { break label88; } } //node爲要刪除的節點 node = e; } } } } Object v; /* 找到要刪除的節點後,判斷!matchValue,咱們正常的remove刪除,!matchValue都爲true */ if (node != null && (!matchValue || (v = ((HashMap.Node)node).value) == value || value != null && value.equals(v))) { //若是刪除的節點是紅黑樹節點,則從紅黑樹中刪除 if (node instanceof HashMap.TreeNode) { ((HashMap.TreeNode)node).removeTreeNode(this, tab, movable); } //若是是鏈表節點,且刪除的節點爲數組下標節點,也就是頭節點,直接讓下一個做爲頭。 else if (node == p) { tab[index] = ((HashMap.Node)node).next; } //爲鏈表結構,刪除的節點在鏈表中,要把刪除的下一個節點設爲上一個節點的下一個節點。 else { p.next = ((HashMap.Node)node).next; } //修改計數器 ++this.modCount; //長度減1 --this.size; this.afterNodeRemoval((HashMap.Node)node); //返回刪除的節點 return (HashMap.Node)node; } } return null; }
刪除還有clear方法,把全部的數組下標元素都置位null。
下面在看較爲簡單的獲取元素。
public V get(Object key) { HashMap.Node e; //也是調用getNode方法來完成 return (e = this.getNode(hash(key), key)) == null ? null : e.value; } final HashMap.Node<K, V> getNode(int hash, Object key) { //tab哈希數組,first頭節點,n長度,k爲key HashMap.Node[] tab; HashMap.Node first; int n; //若是哈希數組不爲null,且長度大於0,獲取key值所在的鏈表頭賦值給first if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) { Object k; //若是是頭節點,則直接返回頭節點。 if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) { return first; } HashMap.Node e; //結果不是頭節點 if ((e = first.next) != null) { //判斷是不是紅黑樹結構 if (first instanceof HashMap.TreeNode) { //去紅黑樹中找,而後返回 return ((HashMap.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; }
hashMap源碼暫時分析到這裏,能力有限,若是內容出現錯誤,歡迎指出。