HashMap源碼閱讀筆記(基於jdk1.8)


1、HashMap概述:  

  HashMap是基於Map接口的一個非同步實現,此實現提供key-value形式的數據映射,支持null值。

  HashMap的常量和重要變量如下:

 
DEFAULT_INITIAL_CAPACITY = 16
 
Node數組的默認長度
 
MAXIMUM_CAPACITY = 1073741824
 
Node數組的最大長度
 
DEFAULT_LOAD_FACTOR = 0.75F
 
負載因子,調控控件與沖突率的因數
 
TREEIFY_THRESHOLD = 8
 
鏈表轉換為樹的閾值,超過這個長度的鏈表會被轉換為紅黑樹
 
UNTREEIFY_THRESHOLD = 6
 
當進行resize操作時,小於這個長度的樹會被轉換為鏈表
 
MIN_TREEIFY_CAPACITY = 64
 
鏈表被轉換成樹形的最小容量,如果沒有達到這個容量只會執行resize進行擴容
 
Node<K, V>[] table
 
儲存元素的數組
 
Set<Map.Entry<K, V>> entrySet
 
set數組,用於迭代元素
 
int size
 
存放元素的個數,但不等於數組的長度
 
int modCount
 
每次擴容和更改map結構的計數器
 
int threshold
 
臨界值,當實際大小(容量*負載因子)超過臨界值的時候,會進行擴容
 
float loadFactor
 
負載因子,默認為0.75F

2、HashMap實現原理:

  在jdk1.8中,HashMap是采用數組+鏈表+紅黑樹的形式實現。如下圖:  

  其中鏈表的實現如下:

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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

  可以看到,node中包含一個next變量,這個就是鏈表的關鍵點,hash結果相同的元素就是通過這個next進行關聯的。

  接下來看看紅黑樹的實現:

  static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); }
   ......
}

  紅黑樹比鏈表多了四個變量,parent父節點、left左節點、right右節點、prev上一個同級節點,紅黑樹內容較多,有興趣的可以自行百度,不在贅述。

  在來說說hash算法,HashMap中使用的算法如下

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

  這個hash先將key右移了16位,然后與key進行異或,這里還涉及到后面put方法中的另一次&操作,

tab[i = (n - 1) & hash]

  tab既是table,n是map集合的容量大小,hash是上面方法的返回值。因為通常聲明map集合時不會指定大小,或者初始化的時候就創建一個容量很大的map對象,所以這個通過容量大小與key值進行hash的算法在開始的時候只會對低位進行計算,雖然容量的2進制高位一開始都是0,但是key的2進制高位通常是有值的,因此先在hash方法中將key的hashCode右移16位在與自身異或,使得高位也可以參與hash,更大程度上減少了碰撞率。

  構造方法如下:

public HashMap() //默認構造方法
public HashMap(int initialCapacity)//參數為初始大小
public HashMap(int initialCapacity, float loadFactor)//參數為初始大小,負載因子

  這里又涉及到另一個算法:

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;
    }

  這個算法很有意思,通過給定的大小cap,計算大於等於cap的最小的2的冪數。連續5次右移運算乍一看沒有什么意思,但仔細一想2進制都是0和1啊,這就有問題了,第一次右移一位,就表示但凡是1的位置右邊的一位都變成了1,第二次右移兩位,上次已經把有1的位置都變成連續兩個1了,是不是感覺很神奇,如此下來5次運算正好將int的32位都轉了個遍,以最高的一個1的位置為基准將后面所有位數都變為1,然后在進行n+1,不就變成了2的冪數。這里還有一點要注意的是第一行的cap-1,這是因為如果cap本身就是2的冪數,會出現結果是cap的2倍的情況,會浪費空間。

 3、HashMap的存取:

  3.1、存儲:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {    //put方法的實際執行者
        //hash,key的hash值;onlyIfAbsent,是否改變原有值;evict,LinkedHashMap回調時才會用到。
            Node<K,V>[] tab; 
        Node<K,V> p;       int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //table為空或長度為0時,對table進行初始化,分配內存   if ((p = tab[i = (n - 1) & hash]) == null)   tab[i] = newNode(hash, key, value, null); //當put的key在map中不存在時,直接new一個Node存入table。    else { //當key在map中存在時,進入此分支。   Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //此處的p是table中的第n-1個元素,每一次必檢查此元素的key是否與put的key相同,如果相同則替換value e = p; else if (p instanceof TreeNode) //檢查p是否是TreeNode類型。   e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //如果第n-1個元素不符合,則一次遍歷table中其余元素,直到找到相應key值。     for (int binCount = 0; ; ++binCount) {       if ((e = p.next) == null) { //此處主要為了防止hash出現重復,只有上邊tab[i = (n - 1) & hash]中存在元素且不是put的元素時才會進入這個分支。       p.next = newNode(hash, key, value, null);     if (binCount >= TREEIFY_THRESHOLD - 1) // 此處判斷的意義是當鏈表長度超過8時,轉換為紅黑樹,在1.8以前是沒有的,鏈表查詢的復雜度是O(n),而紅黑樹是O(log(n)),但是如果hash結果不均勻會極大的影響性能          treeifyBin(tab, hash);              break;       }   if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //進入此分支標志已找到與put的key值相同的元素,元素變量為e。   break;   p = e; } } if (e != null) { // 將對應key的value替換為put的value,同時返回舊value,只有存在key時才會返回! V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); //對table進行擴容  afterNodeInsertion(evict); return null; }

  從上面的源碼可以看出,首先會判斷key的hash與map容量-1的與計算值,如果數組中這個位置沒有元素則直接插入,反之則在遍歷此位置的元素鏈表,直到在最后插入。這個地方有一個鏈表的閾值(默認是8),如果一個鏈表的長度達到了閾值,則調用treefyBin()將鏈表轉換成紅黑樹。

  3.2、擴容:

final Node<K,V>[] resize() {    //擴容方法
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;        //map現在的容量
        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; // 如果現在的容量擴大為2倍依然沒有超過最大容量,並且現在的容量大於等於數組的默認長度,將map的容量擴大為2倍  } else if (oldThr > 0) //hashMap有一個構造方法會指定初始大小,這時候就會用到這個分支,對map進行初始化 newCap = oldThr; else { //這個分支是默認的初始化參數分支,比如用new hashMap()生成一個對象,就會進入這個分支初始化 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) { //擴容操作,將oldTab的元素依次添加到newTab 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; }

  在這個方法里有幾個比較有意思的地方,首先是最大容量MAXIMUN_CAPACITY,這個值實際就是int的最大值,個人推測應該是為了配合put時的hash算法,因為計算key值hash的算法返回值是int型,如果容量超過int的閾值,在進行與運算時碰撞率會增大很多倍。然后是擴容的方式,這里是new了一個數組!這也是HashMap不安全的關鍵之一。當超過一個線程對一個HashMap對象進行put的時候如果觸發了resize方法,后執行的那一個會把之前執行的結果覆蓋掉!也就是先put的值不會真的存入map中。

  3.3、鏈表轉換為紅黑樹

final void treeifyBin(Node<K,V>[] tab, int hash) {    //將鏈表轉換為紅黑樹
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)    //如果map的容量小於64(默認值),會調用resize擴容,不會轉換為紅黑樹
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);    //Node轉換為TreeNode
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)    
                hd.treeify(tab);        //調用TreeNode的樹排序方法
        }
    }

  這里需要注意的是當map容量小於64時,就算鏈表超過了8也不會轉換為紅黑樹。

  3.4、Map克隆

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {  //主要用於clone方法和putAll方法和一個構造方法HashMap(Map<? extends K, ? extends V> m)。
        int s = m.size();
        if (s > 0) {    
            if (table == null) {     // 如果是一個new的map,即table變量還沒有初始化,先通過參數的map.size和負載因子計算所需的map大小。
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)    //如果計算出的大小大於map的實際存儲容量,則重新計算存儲容量
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)    //如果參數map大小大於實際存儲容量,則將map容量變為2倍
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {    //遍歷賦值
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

 

  3.5、讀取:

final Node<K,V> getNode(int hash, Object key) {    //get的實際實現方法
        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) { //通過hash定位到一個桶,並且有元素存在 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) //總是檢查桶中的第一個元素 return first; if ((e = first.next) != null) { //如果第一個元素不符合,則繼續搜索 if (first instanceof TreeNode) //如果桶中是紅黑樹,進入此分支,實際執行的是TreeNode中的find方法,從根節點開始向下搜尋 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; }

  看過put方法后再來看get就容易很多了,首先用put中的hash算法定位到數組中的位置,如果有元素且key值相同直接返回,如果有元素且key值不同那就要向下遍歷了,如果是鏈表就一直遍歷next,如果是紅黑樹則調用getTreeNode方法查找節點。

  3.6、迭代器

abstract class HashIterator {    //KeySet和EntrySet的迭代器的父類
        Node<K,V> next;        // 下一個元素
        Node<K,V> current;     // 刪除方法會調用
        int expectedModCount;  // 就是hashMap中的modCount
        int index;             // 元素序號

        HashIterator() {    //為上面四個變量賦初始值
            expectedModCount = modCount;
            Node<K,V>[] 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);
            }
        }
  .......
}

  HashIterator是KerSet和EntrySet的父類,這個迭代器中有一個屬性是expectedModCount要特別注意,這個變量等價於HashMap對象中的modCount,如果在迭代器沒有執行完的期間對map進行增刪,會拋出異常阻止繼續迭代,代碼如下:

 final Node<K,V> nextNode() {    //獲取下一個元素
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)    //在迭代器執行期間只能用下面的remove刪除元素,否則會拋出異常,不能增加元素
                throw new ConcurrentModificationException();
            if (e == null)    //如果沒有下一個會拋出異常
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {    //如果next沒有取到值,通過index繼續向下遍歷,直到取到元素為止
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

  不過迭代器雖然不允許添加元素,但是給了一個刪除的方法:

 public final void remove() {    //刪除方法
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;    //關鍵語句,這里做了一個同步,否則與正常的刪除沒有區別,這個同步避免了上面的異常拋出
        }

與普通的刪除方法就差在最后這一行上,這里同步了迭代器和HashMap對象的操作數,便不會拋出異常

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM