Java集合系列之HashMap源碼分析

HashMap概述

官方文檔中這樣描述HashMap:node

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.git

大體意思爲:HashMap是基於哈希表的Map接口的實現。這個實現提供了map的全部可選操做,而且容許null值(能夠多個)和一個null的key(僅限一個)。HashMap和HashTable十分類似,除了HashMap是非同步的且容許null元素。這個類不保證map裏的順序,特別是,隨着時間的推移它不保證順序一直不變。github

原理

HashMap是一種'「數組+鏈表+紅黑樹」的數據結構,在put操做中,經過內部定義算法對key進行hash計算,算出哈希值,再經過(n - 1) & hash)計算出數組下標,將數據直接放入此數組中,若經過算法獲得的該數組元素已經存在(俗稱哈希衝突,使用鏈表結構便爲了解決哈希衝突問題,即拉鍊法)。將會把這個元素上的鏈表進行遍歷,將新的數據放到鏈表末尾。若鏈表長度爲8時則將鏈表轉爲紅黑樹。 算法


重要參數(變量)

HashMap中重要的參數(變量)有兩個即:loadFactor(負載因子)與threshold(擴容閾值)sql

//負載因子* 
 final float loadFactor;
//默認負載因子爲 0.75 ,這是權衡了時間複雜度與空間複雜度以後的最好取值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 擴容閾值(threshold):當哈希表中數據長度 ≥ 擴容閾值時,就會擴容哈希表(即擴充HashMap的容量) 
//  擴容 = 對哈希表進行resize操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數
//  擴容閾值 = 容量 * 負載因子
int threshold;

複製代碼
// 存儲數據的Node類型 數組,長度 = 2的冪;
//數組的每一個元素 = 1個單鏈表
transient Node<K,V>[] table;  

//默認初始容量爲16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量爲 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//桶的樹化閾值:即 鏈表轉成紅黑樹的閾值,在存儲數據時,當鏈表長度 > 該值時,
//則將鏈表轉換成紅黑樹
static final int TREEIFY_THRESHOLD = 8;

//桶的鏈表還原閾值:即 紅黑樹轉爲鏈表的閾值,當在擴容(resize())時
//(此時HashMap的數據存儲位置會從新計算),在從新計算存儲位置後,
//當原有的紅黑樹內數量 < 6時,則將 紅黑樹轉換成鏈表
static final int UNTREEIFY_THRESHOLD = 6;

// 最小樹形化容量閾值:即 當哈希表中的容量 > 該值時,才容許樹形化鏈表 (即 將鏈表 轉換成紅黑樹)
// 不然,若桶內元素太多時,則直接擴容,而不是樹形化
// 爲了不進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

複製代碼

構造函數

HashMap共有四種構造函數segmentfault

//默認構造方法	負載因子,容量均爲默認值 =0.75,16
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

 // 指定初始容量 負載因子 = 0.75
 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 public HashMap(int initialCapacity, float loadFactor) {
	//指定初始容量必須大於0不然報錯
        if (initialCapacity < 0) 
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
	//hashMap的最大容量只能是MAXIMUM_CAPACITY,即便傳入的值 > 最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
	//填入的負載因子必須爲正
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
	//設置負載因子
        this.loadFactor = loadFactor;
	//設置擴充閾值,此值不是真正的閾值(後面會從新計算),此值爲調用該構造方法的  
        //初始容量 = 傳入的容量大小轉化爲:>傳入容量大小的最小2次冪
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
	//將傳入的子Map中的所有元素逐個添加到HashMap中
        putMapEntries(m, false);
    }
複製代碼
// 做用:將傳入的容量大小轉化爲:>傳入容量大小的最小的2的冪
     // eg: 8 = tableSizeFor(5);
    static final int tableSizeFor(int cap) {
     //這是爲了防止,cap已是2的冪。若是cap已是2的冪,又沒有執行這個減1操做,
     //則執行完後面的幾條無符號右移操做以後,返回的capacity將是這個cap的2倍
     int n = cap - 1;
     //n分別與n無符號右移1,2,4,8,16位後進行或運算
     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;
    }

複製代碼

我將以HashMap<String,Integer> hashMap = new HashMap(5)爲例對HashMap整個存儲,查找流程進行分析。數組

HashMap<String, Integer> hashMap = new HashMap<>(5);

        hashMap.put("Java", 1);
        hashMap.put("Kotlin", 2);
        hashMap.put("Android", 3);
        hashMap.put("Flutter", 4);
        hashMap.put("Python", 5);
        hashMap.put("C", 6);
        hashMap.put("C++", 7);
        hashMap.put("PHP", 8);
        hashMap.put("Objective-C", 9);
        hashMap.put("JavaScript", 10);
        hashMap.put("Mysql", 11);
        hashMap.put("Swift", 12);
        hashMap.put("Go", 13);

        for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
            Log.d(TAG, "HashMap = [" + entry.getKey() + " -> " + entry.getValue() + "]");
        }
複製代碼

輸出結果爲:bash

HashMap = [Java -> 1] HashMap = [C++ -> 7] HashMap = [C -> 6] HashMap = [Go -> 13] HashMap = [Kotlin -> 2] HashMap = [Android -> 3] HashMap = [JavaScript -> 10] HashMap = [Mysql -> 11] HashMap = [PHP -> 8] HashMap = [Objective-C -> 9] HashMap = [Flutter -> 4] HashMap = [Swift -> 12] HashMap = [Python -> 5]數據結構

其執行過程地址變換圖 app

執行過程爲圖中所示,第一次擴容閾值threshold 爲通過tableSizeFor(5)計算得出爲8,也就是HashMap的實際容量初始值,後續threshold的值爲 = 容量*負載因子, 當HashMap中數據長度大於擴容閾值threshold時,纔會對HashMap進行擴容,capacity左移一位(capacity << 1) 變爲原來容量的2倍。

put函數

HashMap調用put方法對數據進行存儲,該方法源碼爲

// 輸入的Key -Value	鍵值對
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

  //根據對Key值進行hash計算
  static final int hash(Object key) {
        int h;
	//此處說明key容許爲空,若key不爲空 則對key的HashCode的高16位與低16位進行異或操做
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	//建立Node<K,V>數組tab 存放數據
        Node<K,V>[] tab; Node<K,V> p; int n, i;
	//若哈希表的table爲空或者table長度爲0則進行resize()操做新建table
	//因此初始化哈希表的時機是第一次調用put函數時,即調用resize()方法初始化建立	
        if ((tab = table) == null || (n = tab.length) == 0)
	    //table表長度即table容量capacity
            n = (tab = resize()).length; 
	//table不爲空,計算插入存儲的數組索引i,
        // 下標 i 計算方式  = (n-1)& hash 即 capacity -1和hash值進行按位與運算 獲得下標 i,
        //再判斷tab[i]是否爲空,爲空則建立Node<K,V>節點 賦值給tab[i]
        //不爲空則表明存在hash衝突,及當前存儲位置已存在節點
        if ((p = tab[i = (n - 1) & hash]) == null)
	    //建立Node 並賦值		
            tab[i] = new Node(hash, key, value, null);
        else {
	    //tab[i]不爲空,說明該座標下已經存在值
            Node<K,V> e; K k;
	    //若是tab[i]元素對應的Key與要插入的Key值相等,則直接把tab[i]賦值給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;
                    }
		    //若當前鏈表包含要插入的key ,則跳出循環
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
		    //把鏈表的下一位賦值給p
                    p = e;
                }
            }
	     //e不爲空 即對應key已經存在則把舊value更新爲新value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
		//put方法進來 onlyIfAbsent默認爲false ,該方法則一直執行	
                if (!onlyIfAbsent || oldValue == null)
		    //新value 覆蓋 舊value	
                    e.value = value;
                //該方法在LinkedHashMap中調用在HashMap中爲空接口
                afterNodeAccess(e);
                return oldValue;
            }
        }
	//修改次數統計
        ++modCount;
	//判斷實際存在的鍵值對是否大於擴充閾值,大於則進行resize()方法進行擴容
        if (++size > threshold)
            resize();
        //該方法在LinkedHashMap中調用在HashMap中爲空接口
        afterNodeInsertion(evict);
        return null;
    }
複製代碼

分析完put方法源碼後,能夠知道其大體流程爲

  1. 先判斷table 是否爲空 ,爲null 則resize()進行初始化
  2. 經過 ((capacity-1) & hash )計算出索引下標 i
  3. 判斷 i節點是否爲null,爲null添加節點 ,不然表明hash衝突
  4. 若哈希衝突,則轉爲鏈表形式存在
  5. 若鏈表長度超過樹形閾值8 則轉爲紅黑樹
  6. 若key已經存在則新value 覆蓋舊value

其流程圖爲

HashMap的put方法流程圖

resize函數

從put方法中能夠知道,建立HashMap對象的並無進行初始化,只是在put第一個鍵值對的時候執行resize()方法初始化哈希表。 在此方法中設置capacity以及threshold = capacity * loadfactor ,並對Node<K,V>[] table進行初始化 。resize()方法源碼爲:

//調用該方法 一、初始化哈希表 ,二、擴容
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;
            }
	    //	若無超過容量最大值,就擴充爲原來的2倍            
	    //判斷新容量是否小於最大容量,大於默認容量16 ,爲true則 新擴充閾值 =原來的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
		//左移一位值變爲原來的2倍
                newThr = oldThr << 1; // double threshold
        }
	//初始化時執行該語句,擴容前閾值 > 0 則擴容閾值 = 新容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
	    // 容量和擴容閾值均爲0時,也就是執行默認方法 ,capacity = 16,threshold = capacity * 0.75 =12 ;
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
	//新擴充閾值等於0時會從新計算擴充閾值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
	// 賦值
        threshold = newThr;
	//建立擴容後的table
        @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; //清空oldtab[i] 
		     //判斷節點下一位是否爲空,爲空則從新計算在newTab中對應的下標 並賦值給newTab
                    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;  
			    //這段決定了該結點被分到低位仍是高位,依據算式是e.hash mod oldCap,因爲oldCap是擴展前數組的大小,
			    //因此必定是2的指數次冪,因此bit必定只有一個高位是1其他全是0  
	                    //這個算式實際是判斷e.hash新多出來的有效位是0仍是1,如果0則分去低位鏈表,是1則分去高位鏈表
                            if ((e.hash & oldCap) == 0) { //等於0判斷爲低位(原索引)
				 //判斷低位尾部是否爲空
                                if (loTail == null) 
                                    loHead = e; //賦值給低位頭結點
                                else
                                    loTail.next = e; //賦值給低位尾結點下一節點
                                loTail = e;//賦值給低位尾結點
                            }
                            else { //等於1判斷爲高位(原索引+oldCap)
                                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) {//高位結點放在新表中(原索引+oldCap)位置
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab; 
    }
複製代碼

put方法和resize方法中用到的Node的源碼爲

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 是 HashMap的內部類,實現了Map.Entry接口,本質是 = 一個映射(鍵值對) 實現了getKey()、getValue()、equals(Object o)和hashCode()等方法。

get函數

get方法源碼爲

public V get(Object key) {
        Node<K,V> e;
	//對key進行hash計算 獲取其哈希值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

  final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	//判斷table是否爲空,且經過(n-1) & hash 計算出的索引對應的tab不能爲空 不然返回空值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
	    //判斷第一個結點的key是否與查找的key的值是否相等,相等直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;                        
	    //判斷Node<key,value>[i]的下一節點是否爲空
            if ((e = first.next) != null) { 
                if (first instanceof TreeNode)  //如果紅黑樹直接從樹中查找
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
		//遍歷鏈表節點 查找判斷key是否與要查找的key的值相等(equals()),若存在直接返回
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null); 
            }
        }
        return null;
    }
複製代碼

其get方法流程圖爲:

總結

本文中主要分析了 HashMap的 get,put以及resize方法,後續會繼續分析Java的集合源碼。

相關文章閱讀 Java集合系列之ArrayList源碼分析

Android 源碼解析系列分析

自定義View繪製過程源碼分析
ViewGroup繪製過程源碼分析
ThreadLocal 源碼分析
Handler消息機制源碼分析
Android 事件分發機制源碼分析
Activity啓動過程源碼分析
Activity中View建立到添加在Window窗口上到顯示的過程源碼分析

參考文章

Java HashMap工做原理及實現
Java源碼分析:HashMap 1.8 相對於1.7 到底更新了什麼?
Java集合之HashMap源碼解析

相關文章
相關標籤/搜索