JDK1.8 Hashmap源碼解析

一、基礎知識

1、註釋

註釋中對hashmap進行了一些簡單介紹

  • 允許空值和空鍵
  • 無序:不保證map中的順序,不保證順序一直不變;
  • 兩個重要因素:初始大小和負載因子(初始大小默認16,負載因子默認爲0.75);當已存儲的數量 > 容量 * 負載因子,hashmap自動增大爲原來大小的兩倍,重新散列(rehash,消耗大)。負載因子越高,空間消耗越小,查詢map中元素消耗時間越多。當需要一個較大空間時,最好給一個大的初始容量,避免rehash。
  • 基本操作 (get and put) 恆定時間性能:O(log n);
  • 使用相同的鍵存儲值會直接降低hashtable的效率。爲了減輕這樣的情況,會對key進行比較,確保key的重複性低,但是這樣也會降低性能。
  • hashmap不同步,需要自行在外部同步。一般是將map封裝在一個對象中,然後對這個對象進行同步;也可以如下確保同步:
	Map m = Collections.synchronizedMap(new HashMap(...));

大致相當於 hashtable, 只是hashmap不同步, 並允許空值;

  • 當hashmap過大時(鏈表長度大於8)會轉爲紅黑樹,支持更快的查詢,樹節點的大小是常規節點的兩倍。

2、內部結構

我們先來看一下hashmap的內部結構,大多數人多少都知道一些,畫成圖更加直觀的表現:

這是hashmap的內部結構,用數組加鏈表的形式,先使用散列,把節點分佈到數組的每個位置,發生衝突時,使用鏈表解決

這裏散列的大小爲2^n,事實上這並不是一個很好的選擇,碰撞概率會增大。一般情況下,散列的大小最好取2的n次方-1(素數)。hashmap這樣做是爲了之後運算(位運算)方便,同時在hash時選擇更好的hash函數,以抵消2的n次方帶來的不便。

這是每一個node

當鏈表長度大於8時,每一個node都會變成treeNode,形成紅黑樹。

3、補充

  • 要求map中存儲的對象有hashcode()equals()方法,且有不變性,所以使用IntegerString更好,它們都是final,不會變,而且有hashcode()和equals()方法。
  • fail-fast機制:map中有一個modcount,用於存儲版本號,每次對map進行結構上的修改,modcount就會+1;修改時,檢查版本號,如果期待的版本號和當前版本號不同,則直接拋出異常,不再進行後續步驟。問題在於fail-fast並不保證每次都能檢查出異常,所以並不能依賴它,hashmap依舊是線程不安全的。
  • 序列化的時候,先寫入大小,負載因子等參數,再寫入每一個節點,讀取時按相同順序。

二、常用方法

1、字段

hashmap中的字段如下,可以在初始化時進行設置,如果不設置,則按照默認的處理

	//大小爲2^n,首次使用時初始化,有時長度可以爲0
	transient Node<K,V>[] table;
	//緩存節點,AbstractMap字段在keySet() and values()中使用
	transient Set<Entry<K,V>> entrySet;
	//map中存儲節點數量
	transient int size;
	//版本號,結構修改時增加,fail-fast機制
	transient int modCount;
	//大於該值,rehash
	int threshold;
	//負載因子
	final float loadFactor;

默認配置

	//默認初始化容量
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//最大容量
	static final int MAXIMUM_CAPACITY = 1 << 30;
	//負載因子
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	//鏈表長度大於該值,轉爲紅黑樹
	static final int TREEIFY_THRESHOLD = 8;
	//最多可存儲數量:CAPACITY * LOAD_FACTOR。大於該值,rehash。
	static final int UNTREEIFY_THRESHOLD = 6;
	//變成樹的最小容量
	static final int MIN_TREEIFY_CAPACITY = 64;

2、計算hash

先得到key的hashcode,然後讓高16位和低16位異或,結果就是hash,
index = (n - 1) & hash,也就是hash對錶大小取餘。

	/*計算hash
	由於map的大小爲2^n,更容易出現碰撞,所以需要高位與低位異或,減少碰撞
	*/
	static final int hash(Object key) {
	    int h;
	    // >>>:無符號右移16位
	    //高位與低位異或
	    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	}

3、存入值:

afterNodeInsertion,afterNodeAccess這些是linkhashmap會做的事情,此處不討論

	/** 存入值
	* @param onlyIfAbsent if true, don't change existing value
	* @param evict if false, the table is in creation mode.
	* @return previous value, or null if none
	*
	*/
	//todo
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
	              boolean evict) {
	   Node<K,V>[] tab; Node<K,V> p; int n, i;
	   //table爲空,resize
	   if ((tab = table) == null || (n = tab.length) == 0)
	       n = (tab = resize()).length;//table的長度
	   //該節點應該存入的位置爲空,新建節點,存入
	   if ((p = tab[i = (n - 1) & hash]) == null)
	       tab[i] = newNode(hash, key, value, null);
	   //不爲空,p指向鏈表或紅黑樹
	   else {
	       Node<K,V> e; K k;
	       //判斷第一個節點,如果第一個節點就是要存儲的節點,將p的值給e
	       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;
	               }
	               //如果e是要存儲的節點,停止
	               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;
	}

jdk1.7加入節點用的是頭插法,所以tab[index] = 最新加入的節點,因爲認爲最新加入的節點用到的可能性會更大。
jdk1.8採用尾插

4、查找node:

	final Node<K,V> getNode(int hash, Object key) {
	   Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	   
	   //table爲null,table長度爲0,index = (n - 1) & hash,對應位置爲null
	   if ((tab = table) != null && (n = tab.length) > 0 &&
	           (first = tab[(n - 1) & hash]) != null) {
	       //first = table[index],此位置存的是一串鏈表
	       //first存的是第一個節點
	       //先判斷第一個節點(first)是不是
	       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<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;
	}

經過這兩個方法其他方法基本都大同小異,先得到index對應的鏈表/樹,根據不同的情況進行處理。是樹,交給樹處理,是鏈表遍歷,自行處理。
在這裏插入圖片描述

三、擴容

	//擴容
	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; // 兩倍
	    }
	    else if (oldThr > 0) // initial capacity was placed in threshold
	        newCap = oldThr;
	    else { //初始化
	        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;
	}

jdk1.7:擴容時,鏈表會逆置
在這裏插入圖片描述
鏈表逆置可以避免尾部遍歷,但存在一個很嚴重的問題:多線程的時候會導致死循環!!!
jdk1.8:不會
在這裏插入圖片描述
除此之外,jdk1.8有一個很好的改進,在原來擴容時,需要重新rehash,但是看看這部分代碼,我們並沒有發現rehash。
我們來看一下處理鏈表這一部分:

	if ((e.hash & oldCap) == 0){
		loTail...
	} else {
		hiTail...
	}
	
	if (loTail != null) {
	    loTail.next = null;
	    newTab[j] = loHead;
	}
	if (hiTail != null) {
	    hiTail.next = null;
	    newTab[j + oldCap] = hiHead;
	}

擴容,就是給原來的容量乘2,也就是把原來容量oldCap左移一位,這時2^n的好處就表現出來了。
index = (n - 1) & hash,如果這一位是0,則index = index,否則,index = index + oldCap。
這樣很快就能得到新的index而且避免了rehash(rehash是一個消耗比較大的方法,避免它,可以提高性能)。

四、 jdk1.8新加的方法

	/** 
	 * 添加一個節點,若該節點已存在,不改變原來的值
	 * */
	@Override
	public V putIfAbsent(K key, V value) {
	    return putVal(hash(key), key, value, true, true);
	}
	
	/**
	 * 有值,設置新值,返回新值
	 * 無值,頭插
	 * */
	//TODO afterNodeInsertion mappingFunction.apply treeifyBin afterNodeInsertion
	@Override
	public V computeIfAbsent(K key,
	                         Function<? super K, ? extends V> mappingFunction) {
	    if (mappingFunction == null)
	        throw new NullPointerException();
	    int hash = hash(key);
	    Node<K,V>[] tab; Node<K,V> first; int n, i;
	    int binCount = 0;
	    TreeNode<K,V> t = null;
	    Node<K,V> old = null;
	    //設置大小resize
	    if (size > threshold || (tab = table) == null ||
	            (n = tab.length) == 0)
	        n = (tab = resize()).length;
	    //獲取舊節點
	    if ((first = tab[i = (n - 1) & hash]) != null) {
	        if (first instanceof TreeNode)
	            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
	        else {
	            Node<K,V> e = first; K k;
	            do {
	                if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k)))) {
	                    old = e;
	                    break;
	                }
	                ++binCount;
	            } while ((e = e.next) != null);
	        }
	        //舊的值和節點都存在,返回舊的值
	        V oldValue;
	        if (old != null && (oldValue = old.value) != null) {
	            afterNodeAccess(old);
	            return oldValue;
	        }
	    }
	    V v = mappingFunction.apply(key);
	    //新值不存在
	    if (v == null) {
	        return null;
	    } else if (old != null) {//新值存在且舊節點存在
	        old.value = v;//設置新值
	        afterNodeAccess(old);
	        return v;
	    }
	    //按照紅黑樹處理
	    else if (t != null)
	        t.putTreeVal(this, tab, hash, key, v);
	    else {//頭插法給鏈表加入一個節點
	        tab[i] = newNode(hash, key, v, first);
	        if (binCount >= TREEIFY_THRESHOLD - 1)
	            treeifyBin(tab, hash);
	    }
	    ++modCount;
	    ++size;
	    afterNodeInsertion(true);
	    return v;
	}
	
	/**
	 * 新值存在則設置新值,不存在 則刪除節點
	 * */
	public V computeIfPresent(K key,
	                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
	    if (remappingFunction == null)
	        throw new NullPointerException();
	    Node<K,V> e; V oldValue;
	    int hash = hash(key);
	    if ((e = getNode(hash, key)) != null &&
	            (oldValue = e.value) != null) {
	        V v = remappingFunction.apply(key, oldValue);
	        if (v != null) {
	            e.value = v;
	            afterNodeAccess(e);
	            return v;
	        }
	        else
	            removeNode(hash, key, null, false, true);
	    }
	    return null;
	}
	
	@Override
	public V compute(K key,
	                 BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
	    if (remappingFunction == null)
	        throw new NullPointerException();
	    int hash = hash(key);
	    Node<K,V>[] tab; Node<K,V> first; int n, i;
	    int binCount = 0;
	    TreeNode<K,V> t = null;
	    Node<K,V> old = null;
	    if (size > threshold || (tab = table) == null ||
	            (n = tab.length) == 0)
	        n = (tab = resize()).length;
	    //獲取舊節點
	    if ((first = tab[i = (n - 1) & hash]) != null) {
	        if (first instanceof TreeNode)
	            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
	        else {
	            Node<K,V> e = first; K k;
	            do {
	                if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k)))) {
	                    old = e;
	                    break;
	                }
	                ++binCount;
	            } while ((e = e.next) != null);
	        }
	    }
	    V oldValue = (old == null) ? null : old.value;
	    V v = remappingFunction.apply(key, oldValue);
	    if (old != null) {//有節點
	        if (v != null) {//新值存在
	            old.value = v;//設置新值
	            afterNodeAccess(old);
	        }
	        else//無值,刪除節點
	            removeNode(hash, key, null, false, true);
	    }
	    //沒有節點,但新值存在則添加節點
	    else if (v != null) {
	        //紅黑樹添加
	        if (t != null)
	            t.putTreeVal(this, tab, hash, key, v);
	        //鏈表添加
	        else {
	            tab[i] = newNode(hash, key, v, first);
	            if (binCount >= TREEIFY_THRESHOLD - 1)
	                treeifyBin(tab, hash);
	        }
	        ++modCount;
	        ++size;
	        afterNodeInsertion(true);
	    }
	    return v;
	}
	
	@Override
	public V merge(K key, V value,
	               BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
	    if (value == null)
	        throw new NullPointerException();
	    if (remappingFunction == null)
	        throw new NullPointerException();
	    int hash = hash(key);
	    Node<K,V>[] tab; Node<K,V> first; int n, i;
	    int binCount = 0;
	    TreeNode<K,V> t = null;
	    Node<K,V> old = null;
	    //設置大小resize
	    if (size > threshold || (tab = table) == null ||
	            (n = tab.length) == 0)
	        n = (tab = resize()).length;
	    //first爲對應的鏈表/樹
	    //得到需要的節點old
	    if ((first = tab[i = (n - 1) & hash]) != null) {
	        //紅黑樹
	        if (first instanceof TreeNode)
	            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
	        else {//鏈表
	            Node<K,V> e = first; K k;
	            do {
	                if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k)))) {
	                    old = e;
	                    break;
	                }
	                ++binCount;
	            } while ((e = e.next) != null);
	        }
	    }
	    //節點存在
	    if (old != null) {
	        V v;
	        //根據舊值和value,得到設置的新值
	        if (old.value != null)
	            v = remappingFunction.apply(old.value, value);
	        else
	            v = value;
	        //新值存在
	        if (v != null) {
	            old.value = v;//設置新值
	            afterNodeAccess(old);
	        }
	        else//不存在則刪除節點
	            removeNode(hash, key, null, false, true);
	        return v;
	    }
	    //新的值存在則添加節點
	    if (value != null) {
	        if (t != null)
	            t.putTreeVal(this, tab, hash, key, value);
	        else {
	            tab[i] = newNode(hash, key, value, first);
	            if (binCount >= TREEIFY_THRESHOLD - 1)
	                treeifyBin(tab, hash);
	        }
	        ++modCount;
	        ++size;
	        afterNodeInsertion(true);
	    }
	    return value;
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章