1. 程式人生 > >Java集合原始碼解析:HashMap

Java集合原始碼解析:HashMap

本文概要

  1. HashMap概述
  2. HashMap資料結構
  3. HashMap的原始碼解析

HashMap概述

在官方文件中是這樣描述的:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

我們可以總結一下:

  1. 基於Map實現,也就是key-value形式去儲存
  2. 允許key為null,允許value為null
  3. 非同步,它不是執行緒安全的
  4. 沒有順序

HashMap資料結構

JDK7以及之前HashMap使用的是陣列+連結串列 在這裡插入圖片描述

JDK8以後HashMap使用的是陣列+連結串列+紅黑樹(我們這篇文章主要講的是JDK8)

連結串列大於一定長度會轉換為紅黑樹,主要是為了提高操作效率

在這裡插入圖片描述

HashMap的原始碼解析

原始碼解析主要為以下幾個方面去分析

  1. HashMap主要的成員變數
  2. HashMap的建構函式
  3. get()方法
  4. put()方法
  5. resize()擴容方法

HashMap主要的成員變數

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
 	/* ---------------- Fields -------------- */
	// 預設陣列初始化容量
    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; // 當樹的長度小於這個值,把樹轉為連結串列 static final int UNTREEIFY_THRESHOLD = 6; // 桶中結構轉化為紅黑樹對應的陣列的最小長度,如果當前陣列的長度(即table的長度)小於它,就不會將連結串列轉化為紅黑樹,而是用resize()代替 static final int MIN_TREEIFY_CAPACITY = 64; // 儲存元素的陣列 transient Node<K,V>[] table; // 儲存元素的集 transient Set<Map.Entry<K,V>> entrySet; // 存放元素的總個數 transient int size; // 更改結構的計數器(比如put()、remove()等對hashmap結構有改動的操作,那麼該數值都會+1) transient int modCount; // 擴容臨界值,當size > threshold。就會進入擴容 int threshold; // 擴容因子 final float loadFactor; }

HashMap建構函式

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

這裡總結兩點

  1. 建立HashMap物件,呼叫建構函式時,沒有初始化table陣列
  2. 如果呼叫了第三個建構函式(即傳入了initialCapacity初始化容量和擴容因子),會呼叫tableSizeFor(),雖然這時候會把值賦給擴容臨界值threshold,但是第一次put(),會進行resize(),然後初始化table陣列,這時候會把threshold當成table陣列的長度,所以暫時我們可以理解這個threshold就是容量,但是實際上它還是擴容臨界值,只不過第一次比較特殊。這裡會把initialCapacity轉換成大於initialCapacity的最靠近2次冪的那個數,比如說initialCapacity = 10,經過tableSizeFor(10)後,threshold = 16。因為16是2次冪的數,也是最靠近10的。

這裡需要說下tableSizeFor(initialCapacity)方法

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

下面用一個圖解釋下tableSizeFor(10) : 在這裡插入圖片描述

get()方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    // 對key進行hash
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這裡沒有直接使用key的hashCode,而是使key的hashCode高16位不變,低16位與高16位異或作為最終hash值。 原因就是:如果直接使用key的hashCode作為hash很容易發生碰撞。比如n-1為15(0x1111)時,雜湊值真正生效的只是低4位,當新增的值hashCode為2、18、34這些以16位倍數的等差數列,就產生大量碰撞

    // get()實際上就是呼叫getNode()
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        /*	   
	     * 1、首先判斷table是否為空、table的長度是否大於0
	     * 2、hash & (n - 1),取的這個hash在這個陣列的下標,類似於(hash % (n-1)),但是&效率更高
	     * 3、tab[hash & (n - 1)],獲取該陣列在該索引的的頭元素,
	     */
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            /*
             * 判斷該key是否為頭元素
             */             
            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;
    }

整個get()方法,還是比較簡單,可以總結為幾點

  • hash = hash(key),獲取key的hashcode,用於獲取在陣列中的下標
  • (n - 1) & hash,通過hashcode與陣列的長度進行&運算,獲取該hashcode在陣列中的下標。
  • first = tab[(n - 1) & hash],獲取該下標的頭元素
  • first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))),判斷該key是否為頭元素
  • 因為可能會出現hash碰撞【即為不同的hashcode可能定位到同一個下標】,所以判斷該頭元素是為紅黑樹節點,還是連結串列,然後在節點中進行迴圈判斷。

put()方法

    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) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /*
         * 我們說過建立HashMap物件,是不初始化table陣列的。
         * 所以第一次呼叫put()的時候。table陣列為空。
        */ 
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 那麼呼叫resize()進行初始化table陣列
            n = (tab = resize()).length;
        //  通過hash定位在table陣列下的索引,判斷該索引是否存在元素
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 不存在元素,直接往該索引下插入一個Node元素
            tab[i] = newNode(hash, key, value, null);
        else {
        	// 說明該索引下存在頭元素
            Node<K,V> e; K k;
            // 判斷插入的key與頭元素的key是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 說明插入的key與頭元素的key相等,賦值給e變數
                e = p;
            else if (p instanceof TreeNode)
            	// 說明頭元素是紅黑樹節點
            	// 判斷樹中是否存在一個節點的key與插入的key相等,存在賦值給e,不存在,往紅黑樹節點插入節點,並且返回null
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	// 說明頭元素是連結串列中的一個節點
            	// 迴圈遍歷該連結串列
                for (int binCount = 0; ; ++binCount) {               	
                    if ((e = p.next) == null) {
                    	// 如果直到連結串列的尾節點,都沒有找到與該key相等的節點
                    	// 往該連結串列插入一個新的節點
                        p.next = newNode(hash, key, value, null);
                        // 判斷該連結串列的長度是否大於等於TREEIFY_THRESHOLD
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	// 呼叫treeifyBin(),判斷是否需要把連結串列轉為紅黑樹
                            treeifyBin(tab, hash);
                        break;
                    }                    
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 找到了和該key相等的節點e,直接break跳出迴圈
                        break;
                    p = e;
                }
            }
            // 如果e不為空,說明該連結串列或者紅黑樹中存在與該key相等的節點
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                	// 把該節點的值替換成新的值
                    e.value = value;
                // 回撥方法,HashMap沒有實現,忽略
                afterNodeAccess(e);
                // 返回舊的值
                return oldValue;
            }
        }        
        ++modCount;
        // size+1,並且判斷大小是否大於擴容閾值
        if (++size > threshold)
        	// 進行擴容
            resize();
        // 回撥方法,HashMap沒有實現,忽略
        afterNodeInsertion(evict);
        return null;
    }

我們看下TreeNode.putTreeVa(),往紅黑樹裡新增節點

	final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            // 獲取樹的根節點
            TreeNode<K,V> root = (parent != null) ? root() : this;
            // 遍歷樹
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                // 如果p的hash大於傳入的hash
                if ((ph = p.hash) > h)
                	// 把-1賦值給dir,代表左邊查詢樹
                    dir = -1;
                // 如果p的hash小於傳入的hash
                else if (ph < h)
                	// 把-1傳遞給dir,代表右邊查詢樹
                    dir = 1;
                // 如果傳入的hash和p.hash相等,而且p.key 等於傳進來的key,那麼直接返回p
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                // 如果k所屬的類沒有實現Comparable介面 或者 k和p節點的key相等
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    // 如果k所屬的類沒有實現Comparable介面 或者 k和p節點的key相等
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                     // 從p節點的左節點和右節點分別呼叫find方法進行查詢, 如果查詢到目標節點則返回
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                // 根據dir的值,獲取p的左節點或者右節點,判斷獲取的節點是否為空
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                	// 如果獲取的節點為空,那麼則需要往樹裡插入一個新節點
                    Node<K,V> xpn = xp.next;
                    // 建立一個新Node節點
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    // 如果dir小於0
                    if (dir <= 0)
                    	// 插入左節點
                        xp.left = x;
                    else
                    	// 如果dir大於0
                    	// 插入右節點
                        xp.right = x;
                    // 這裡進行調整指標
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    // 插入新節點後可能會破壞紅黑樹結構,所以需要呼叫balanceInsertion(root, x)進行修復紅黑樹結構
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

treeifyBin(),判斷是否需要把連結串列轉為紅黑樹

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // MIN_TREEIFY_CAPACITY = 64
        // 判斷table的長度是否小於MIN_TREEIFY_CAPACITY 
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
       		// 呼叫 resize()進行擴容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {        	
            TreeNode<K,V> hd = null, tl = null;
            // 建立一條以TreeNode為節點的連結串列,方便以後紅黑樹轉為連結串列
            do {                        	
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
            	// 把TreeNode的連結串列轉為紅黑樹
                hd.treeify(tab);
        }
    }

treeify(),把TreeNode的連結串列轉為紅黑樹,原理很簡單,就不解釋,不懂的可以看這篇文章講紅黑樹

        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V