源碼解析:HashMap 1.8

本文源碼基於HashMap 1.8,下載地址:Java 8java

另外本文不分析紅黑樹相關的源碼node

前言

在對HashMap進行源碼解析前,咱們頗有必要搞清楚下面這幾個名詞,這對於下文的閱讀有很大的幫助。git

  • 哈希表:這裏指的就是HashMap
  • 哈希桶:HashMap的底層數據結構,即數組
  • 鏈表:哈希桶的下標裝的就是鏈表
  • 節點:鏈表上的節點就是哈希表上的元素
  • 哈希表的容量:元素的總個數
  • 哈希桶的容量:數組的個數,因爲當發生哈希衝突時,採用鏈地址法解決衝突,故哈希桶的容量<=哈希表的容量

注意:必定要區分哈希表的容量和哈希桶的容量,一開始很容易將這兩個定義搞混淆github

1、鏈表節點

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

		//結點的hash值等於key和value哈希值的異或
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

		//設置新的value,同時返回舊的value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        
        //key和value都相等才被認爲是相同的節點
        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;
        }
    }
複製代碼

從上面能夠發現哈希桶的鏈表就是單鏈表結構,而且節點的hash值會等於key和value哈希值的異或。數組

2、成員屬性

HashMap安全

1. 常量

//哈希桶默認容量爲16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

    //默認加載因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //轉化成紅黑樹的閾值,當哈希桶的鏈表結點數量大於等於8時,轉化成紅黑樹
    static final int TREEIFY_THRESHOLD = 8;

    //若是當前是紅黑樹結構,那麼當桶的鏈表結點數量小於6時,會轉換成鏈表
    static final int UNTREEIFY_THRESHOLD = 6;
	
    //當哈希表的容量達到64時,也會轉換爲紅黑樹結構
    static final int MIN_TREEIFY_CAPACITY = 64;
複製代碼

2. 變量

//哈希桶,存放鏈表。transient關鍵字表示該屬性不能被序列化
    transient Node<K,V>[] table;

    //迭代功能
    transient Set<Map.Entry<K,V>> entrySet;

    //哈希表元素數量
    transient int size;

    //統計該map修改的次數
    transient int modCount;

    //閾值,當元素數量,即哈希表的容量達到閾值時,會進行擴容
    int threshold;

    //加載因子,用於計算哈希表的閾值。threshold = 哈希桶的容量*loadFactor
    final float loadFactor;
複製代碼

3、構造器

1. 無參構造器

//默認的構造函數,加載因子爲默認的0.75f
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
複製代碼

咱們在通常狀況下都是用這個無參構造器的,這就證實當咱們平時new了一個HashMap時,底層只是設置了一個加載因子的值爲默認的0.75fmarkdown

2. 指定哈希桶容量的構造函數

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
複製代碼

其實調用的是下面一個構造函數,不過這裏在指定哈希表的容量的同時,也指定了加載因子爲默認值數據結構

3. 指定哈希桶容量和加載因子的構造函數

//指定加載因子的構造函數仍是用的比較少的
    public HashMap(int initialCapacity, float loadFactor) {
    	//初始化容量不能爲負數
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
		//初始化容量不能超過2的30次方
        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);
    }
複製代碼

在上面對容量作了一些邊界處理,而後設置了加載因子的值和哈希表的閾值,這裏在設置閾值時,首先會調用tableSizeFor函數對容量作一些處理函數

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的2的n次方,當擴容時也會調用這個函數,這就保證了哈希桶的容量永遠都是2的n次方,也正是由於這個前提下,接下來的取下標的操做可以經過 hash&(table.length-1)來替換hash%(table.length)

這時候你也許就會有疑問了,我明明傳入的是哈希桶的容量,怎麼最後卻賦值給了閾值呢?這實際上是由於在構造器中,並無對哈希桶table進行初始化,初始化的工做交給了擴容函數。當第一次put時,會調用擴容函數,將閾值賦值給哈希桶的容量,接着對哈希桶table進行初始化,而後根據公式設置從新設置閾值,大概流程是這樣,後續還會提到!

4. 批量添加元素的構造函數

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
複製代碼

這個構造函數比較特殊,其做用就是在構造一個新的哈希表的同時加入指定map全部的元素。這裏也是首先設置了加載因子爲默認值,而後調用putMapEntries方法來進行批量增長元素,注意這裏的第二個參數爲false。

//將map表的全部元素加入到當前表中,當前Map初始化時evict爲false,其它狀況爲true
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
            	//求出須要的容量。由於實際使用的長度=容量*加載因子
            	//+1是由於小數相除,基本都不會是整數,容量大小不能爲小數的,
                //後面轉換爲int,多餘的小數就要被丟掉,
            	//因此+1,例如,map實際長度22,22/0.75=29.3,所須要的容量確定爲30
            	//若是剛恰好除得整數呢,除得整數的話,容量大小多1也沒什麼影響
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
			    //將哈希桶的容量存在閾值中
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
			//當前表尚未初始化,因此不會進行擴容
            else if (s > threshold)
                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);
            }
        }
    }

複製代碼

假設這裏要批量增長的數據不爲0,經過上面分析咱們知道,構造器並無構造哈希桶table,因此這裏的table爲null。接着就是跟上面的構造函數同樣,將要建立的哈希桶的容量暫時存在閾值中。

這裏有個問題值得一提,剛開始我一直覺得ft是閾值,因此一直搞不明白爲何要用s/loadFactor ,由於公式是:threshold = s*loadFactor纔對。後來纔想通,這裏的ft並非閾值,而是哈希桶的容量,由於最後並非設置閾值,而是將容量的值暫存在閾值中。

接着會遍歷m依次將元素添加到當前哈希表中,這裏涉及到了兩個操做:遍歷和添加,這裏不進行展開講,在後文會詳細進行分析。

5. 小結

這裏咱們從新梳理下當new一個HashMap時,內部的實際工做:

  • 無參數構造HashMap,如HashMap<Integer,Integer> map = new HashMap<>(),只設置了加載因子爲默認值
  • 指定哈希桶容量構造HashMap,如HashMap<Integer,Integer> map = new HashMap<>(5),內部的工做就是設置加載因子爲默認值。暫存容量的值到閾值中,其值爲恰好大於容量的2的n次方,這裏的閾值爲8。
  • 同時指定容量和加載因子,這個構造器通常不多使用
  • 在構造的同時批量加載數據。如HashMap<Integer,Integer> newMap = new HashMap<>(map),根據map的size和公式算出哈希桶的容量,而後將容量暫存到閾值中,最後遍歷map,將map中的元素添加到newMap中

能夠發如今構造的時候並無構造哈希桶table的實例,因此將哈希桶的容量都先暫存在閾值threshold中

4、添加

put操做其實包括了哈希表的增、改兩個操做。當添加的元素key存在時,就會修改value的值,不存在則添加這個元素。

1. 添加(改)一個元素

在實際上咱們的操做很簡單,就一行代碼搞定

map.put(key,value);

複製代碼

因此咱們直接看put的操做

HashMap#put

public V put(K key, V value) {
        //先計算key的hash值
        return putVal(hash(key), key, value, false, true);
    }

複製代碼

這裏涉及到了hash函數來求當前key的hash值,咱們來看看

HashMap#hash

static final int hash(Object key) {
        int h;
		//當前key爲null,則hash指爲0,不然返回擾動函數干擾後的hashCode值
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

複製代碼

這個函數被稱之爲擾動函數,能夠發現這個函數並無簡單的返回了key的hashCode的值,而是進行了干擾,干擾的細節不進行分析,其原理就是將key的hashCode值的高位變相的添加到低位去,而後增長key的hash值的隨機性來減小hash衝突。爲何要將高位添加到低位呢?這是由於在HashMap中取桶下標的方式是經過 hash&(桶.size-1)來替代模操做,而位操做的時候hashCode只有低位參與位運算。

講完hash,咱們回到putVal方法上(在批量增長元素的構造函數中遍歷添加也會調用這個函數)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {        
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		//若是當前的哈希桶是空的,則表示當前爲首次初始化
        if ((tab = table) == null || (n = tab.length) == 0)
			//擴容操做
            n = (tab = resize()).length;

		//沒有發生哈希碰撞,直接構建一個新的結點,而後放在指定的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
		//發生哈希衝突,鏈地址法解決哈希衝突
        else {
			
            Node<K,V> e; K k;
			//若是key的哈希值相同,key也相同,則進行覆蓋value操做
            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);
						//若是鏈表結點數>=8,則轉化成紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					//若是找到要覆蓋的結點
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

			//value覆蓋操做,若是e不爲null,則須要覆蓋value
            if (e != null) { 
				//覆蓋結點值,並返回原來結點的value
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
				//空實現的函數,若是是LinkedHashMap會重寫該方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
		//走到這,表示是添加操做

		//修改次數+1
        ++modCount;
		//更新size,判斷是否須要擴容
        if (++size > threshold)
            resize();
		//空實現的函數,若是是LinkedHashMap會重寫該方法
        afterNodeInsertion(evict);
        return null;
    }

複製代碼

這個方法乾的事情不少,其職責就是:

  • 首次初始化須要進行擴容操做
  • 判斷是否發生哈希衝突
    • 沒有,則直接構造一個新的節點,而後放在哈希桶對應下標的位置看成鏈表的頭結點
    • 發生哈希衝突,採用鏈地址法解決衝突
      • 若是頭節點的key等於要添加的key,表示是修改操做,則進行覆蓋value操做
      • 不然,若是是頭結點是紅黑樹,則進行紅黑樹的添加操做
      • 不然,表示當前鏈表節點少於8,則對鏈表進行遍歷,若是遍歷中途出現節點key等於要添加的元素的key時候,表示是修改操做,跳出遍歷;不然遍歷到鏈表末端插入一個新的節點,插入後判斷當前鏈表節點數是否達到8,達到8則進行轉換成紅黑樹的操做
      • 進行覆蓋value操做,覆蓋節點值,返回原來節點的值。
  • 若是是添加操做,則更新當前哈希表的大小,而後判斷是否須要擴容,最後返回null

因此若是是添加操做則返回null,若是是修改操做則返回原來的value。另外擴容操做將在下文進行解析。

2. 批量添加元素

實際使用也很簡單,也是一行代碼搞定,下面的map和oldMap都是HashMap類型,而且兩個key和value類型一致

實際使用

map.putAll(oldMap)

複製代碼

而後咱們看看HashMap中的putAll方法

HashMap#putAll

//批量增長數據
    public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

複製代碼

調用了putMapEntries,emmmm,怎麼感受這個函數似曾相識!沒錯,在批量添加元素的構造函數中,也調用了這個putMapEntries,不一樣的是在構造中傳入的第二個參數是false,而在putAll中傳入的是true.咱們仍是再次看看這個putMapEntries方法。

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
			//若是當前表是空的
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
			//若是當前表已經初始化,而且m的元素數量大於閾值,則進行擴容
            else if (s > threshold)
                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);
            }
        }
    }

複製代碼

因此這個批量添加元素的函數和批量添加元素的構造函數有什麼區別嗎?當調用putAll前,沒有初始化(構造後直接調用putAll)確實沒什麼區別。可是當putAll前,已經調用過put或者putAll時,就不同了,這時候由於已經初始化,因此table並不爲null,故還要判斷添加的元素個數是否須要進行擴容(擴容操做後續解析),而後才遍歷oldMap添加元素。

5、擴容

擴容操做可謂是HashMap的精髓,在瞭解這個操做前,咱們首先須要知道這個神祕的擴容究竟在什麼場合下會出現。

1. 觸發擴容的狀況

從上面的分析中,咱們得知觸發擴容有三種狀況:

1.首次初始化,有多是第一個put操做或者第一個putAll操做,也有多是使用批量添加元素的構造函數

2.已經初始化,putAll批量添加元素,增長元素的總個數大於閾值

3.已經初始化,putVal添加一個節點後,節點個數大於閾值

注:putVal包括了添加一個元素和批量添加元素的狀況,由於批量添加元素也會調用putVal

而後接着看具體擴容方法

2. 擴容方法

因爲擴容方法太長,這裏將分解成兩部分進行講解

final Node<K,V>[] resize() {
    	//當前哈希桶
        Node<K,V>[] oldTab = table;
		//當前桶的容量,若是首次初始化,則當前桶的容量爲0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
		//當前的閾值
        int oldThr = threshold;
		//擴容後新的容量和新的閾值
        int newCap, newThr = 0;
        
        
		//1.構造新的哈希桶
		.....
		
		//2.合併哈希桶
		.....
        return newTab;
    }

複製代碼

2.1 構造新的哈希桶

HashMap#resize

.....
		//1.若是當前桶的容量大於0,則是觸發的狀況2或狀況3
        if (oldCap > 0) {
            //邊界處理
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
			//不然,則設置新的容量爲當前的容量的兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //若是當前的容量達到16的話,新的閾值也等於舊的閾值的2倍 
                newThr = oldThr << 1; // double threshold
        }

		//2.當前的閾值大於0,只能是1狀況而且使用了指定容量的構造函數
        else if (oldThr > 0) 
            newCap = oldThr;
		//3.仍是狀況1,而且使用的是無參數的構造函數
        else {
			//新的容量爲默認容量16
            newCap = DEFAULT_INITIAL_CAPACITY;
			//新的閾值=默認容量*默認加載因子,即新的閾值爲16*0.75=12
            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;
		
		//若是之前的哈希桶有元素,合併哈希桶
		.....


複製代碼

構造新的哈希桶,首先要得獲得新的容量才能構造,而且在構造的同時還得設置閾值。因此須要根據不一樣的狀況設置新的容量和閾值,其總體流程以下:

  • 若是當前桶的容量大於0,表示已經初始化,則應該是觸發擴容的狀況2和狀況3,則設置新的容量爲當前容量的兩倍,若是當前容量大於16,則設置新的閾值爲當前閾值的兩倍(什麼狀況下在已經初始化後,當前容量仍是小於16呢?答案是若是是使用指定了容量的構造函數這種狀況)
  • 不然,當前閾值大於0,代表是初始化而且使用了指定容量的構造器,將暫存在threshold的哈希桶的容量取出來,即新的容量等於當前閾值。
  • 不然,代表是初始化而且沒有指定容量,則設置新的容量爲默認值16,新的閾值爲12(新的閾值=默認容量*默認加載因子)
  • 若是當前的閾值爲0,則代表上述沒有對新的閾值進行賦值,則新的閾值等於新的容量*加載因子
  • 更新全局變量閾值threshold,接着根據新的容量構建新的哈希桶,而且賦值給全局變量table

在這裏也驗證了上面構造函數中提到的設置閾值threshold,其實只是暫存容量的說法。

2.2 合併哈希桶

if (oldTab != null) {
			//遍歷當前哈希桶
            for (int j = 0; j < oldCap; ++j) {
			    //當前的結點
                Node<K,V> e;
			    //當前哈希桶中有元素,賦值給e
                if ((e = oldTab[j]) != null) {
					//將當前哈希桶的結點置爲null,便於gc
                    oldTab[j] = null;
					//若是當前鏈表只有一個元素(沒有發生哈希碰撞)
                    if (e.next == null)
					  //則將這個元素放進新的哈希桶中
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
						//若是發生哈希碰撞且結點數超過8個,轉化成紅黑樹的狀況
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
					
				 //發生哈希碰撞
                    else { 
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
						  //等於0:當前結點的哈希值小於oldCap,故放在low位鏈表中
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
						 //當前結點的哈希值大於oldCap,故放在high位鏈表中
                            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;
                        }
                    }
                }
            }
        }

複製代碼

從上面的一開始的判斷咱們能夠得知,若是在已經初始化而且當前哈希表有元素的狀況下就會進行合併哈希桶的操做,操做的過程以下:

  • 遍歷當前哈希桶
  • 若是當前哈希桶的下標,即鏈表頭節點有元素,則賦值給e
    • 首先爲了便於GC須要將當前鏈表的頭節點置爲null
    • 若是當前鏈表只有一個節點,則表示這個鏈表以前並無發生哈希衝突,因此直接位操做取下標放到新的哈希桶上(這裏爲何不用判斷頭節點的hash值與原哈希桶容量的大小關係呢?由於是容量翻倍,若是當前鏈表只有一個節點,位操做取下標(模運算)後依舊會是一個節點,因此無論大小,最後的鏈表都只會是一個節點)
    • 不然,若是頭結點是紅黑樹,則進行紅黑樹的的合併操做
    • 不然,當前鏈表節點小於8,則須要根據每一個節點的hash值來放入到低位鏈表或高位鏈表
      • 遍歷當前鏈表,利用位運算e.hash & oldCap來判斷當前節點與當前容量的大小關係,若是e.hash & oldCap=0,則表示當前節點的hash值小於當前容量,故放入低位鏈表;不然,放入高位鏈表
      • 遍歷結束,將低位鏈表放回原位置,將高位鏈表放在新位置,新位置 = 原位置+ 當前容量(oldCap)

咱們發現這裏又運用了位操做,這麼作的緣由就是爲了提高擴容的效率。講到這擴容就分析完了,讓咱們接着下一個操做!

6、查詢

在HashMap中提供了幾種查詢操做,get,containsKey,containsValue,還有Java8新增的getOrDefault,接下來咱們一個個進行分析

1. get

HashMap#get

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

複製代碼

get查詢幾乎和put操做如影隨行,從上面咱們也能夠發現當查詢不到的時候返回null,查詢到就返回value。這裏實際上調用了getNode方法來進行查詢

final Node<K,V> getNode(int hash, Object key) {
        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) {
            //鏈表頭爲查找的節點
            if (first.hash == hash && 
                ((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;
    }

複製代碼

getNode的方法也很簡單,流程以下:

  • 根據key來經過位操做取下標,得到當前key的鏈表
  • 若是鏈表頭是查詢的節點,則直接返回該節點
  • 若是頭結點是紅黑樹,則進行紅黑樹查詢操做
  • 不然,遍歷鏈表,返回要查詢的節點
  • 若是查詢不到,返回null

2. getOrDefault

這個方法是Java8新增的,筆者使用過一次以後,對它可謂是愛不釋手,由於有個這個函數,省去了一開始的判空操做。好比有這麼一個需求,咱們須要計算一個int數組各個數字出現的次數。

//常規操做
for(int num:nums){
    if(map.get(num) == null){
        map.put(num,0);
    }
    map.put(num,map.get(num)+1);
}

//getOrDefault
for(int num:nums){
    map.put(num,map.getOrDefault(num,0)+1);
}

複製代碼

是否是頓時愛上了這個getOrDefault,其實其內部實現也是很簡單的,讓咱們看看其操做

public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }

複製代碼

有沒有發現其實就是跟get方法是同樣的,只不過在這裏幫咱們實現了在實際上的判空操做。因此當查詢不到,返回默認值defaultValue,查詢到了就返回value。

3. containsKey

這個方法在平時也是常常會使用到

public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

複製代碼

能夠發現其內部實現其實就是跟get的實現是同樣的,同樣是調用了getNodet,不一樣的是二者的返回類型不同。

4. containsValue

public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

複製代碼

containsValue的實現,須要遍歷哈希桶的每個鏈表,而後與節點上的value匹配,若是找到value,則返回true,不然返回false。

7、刪除

在HashMap提供了兩個刪除操做,都是remove,不過一個只需提供key,一個是須要提供key和value,咱們在實際使用上前者應該用的比較多

1. 根據key刪除

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

複製代碼

從上面發現當刪除成功會返回刪除的value,刪除失敗,則會返回null。經過調用removeNode來進行刪除操做,這裏傳入的matchValue是爲false,若是是true的話,則key和value都相等才能刪除

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;

		//當前哈希表不爲空,而且該key對應的index的鏈表有結點
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node爲待刪除的結點
            Node<K,V> node = null, e; K k; V v;
			//若是鏈表頭就是要刪除的結點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
				//紅黑樹
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
				//鏈表的狀況
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
			//有待刪除的節點,而且matchValue爲false或者刪除的值相等
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {

				//紅黑樹狀況,跳過
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
				//鏈表頭爲待刪除的節點,則更新鏈表頭
                else if (node == p)
                    tab[index] = node.next;

				//待刪除的節點不是鏈表頭,則直接刪除該節點
                else
                    p.next = node.next;
				//操做次數加1
                ++modCount;
				//更新哈希表的size
                --size;
				//LinkedHashMape回調函數
                afterNodeRemoval(node);
				//返回刪除的節點
                return node;
            }
        }
        return null;
    }

複製代碼

這裏的刪除操做與添加增長有點相似,大概流程以下:

  • 首先根據key找到哈希桶上對應下標的鏈表,而後找待刪除的節點
  • 若是待刪除的節點爲鏈表頭,則直接更新鏈表頭
  • 不然,若是鏈表頭爲紅黑樹,則進行紅黑樹的刪除操做
  • 不然,遍歷鏈表,找到待刪除的節點,直接刪除該節點
  • 若是刪除成功,更新操做次數和哈希表的容量,返回刪除的節點
  • 若是刪除失敗,則返回null

2. 根據key和value刪除

public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

複製代碼

這個刪除操做與根據key刪除的操做沒多大區別,不一樣的是這裏調用的removeNode中傳入matchValue爲true,表示只有key和value都匹配才能刪除節點,而且返回類型不一致。

8、遍歷

HashMap的遍歷有不少種,這裏列舉了三種常見的遍歷方法

1. for-each遍歷entrySet

上面咱們在批量添加元素時,在HashMap的內部,也是經過這種方式來遍歷的

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

複製代碼

因此咱們以前看entrySet方法

HashMap#entrySet

public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

複製代碼

因爲是第一次調用該方法,會直接構造這個EntrySet,這個EntrySet是HashMap的內部類

HashMap.EntrySet

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size() { return size; }
        public final void clear() { HashMap.this.clear(); }

		//獲取iterator
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

		//最終調用了getNode
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }

		//最終調用了removeNode方法
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

		//for-each遍歷entrySet
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

複製代碼

在這個類中咱們能夠發現不少操做,不過這些操做不少都是直接調用HashMap的方法來實現的,而後forEach方法就是for-each遍歷的實現,經過這個方法,咱們大概能夠得出for-each在這個類中就是會遍歷哈希桶上的每一個鏈表,而後返回鏈表上的節點。而且其原理實際上是調用了Iterator.next,因此咱們能夠繼續看下一個遍歷方法來了解下EntrySet的Iterator

2. 使用Iterator迭代

實際使用

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
	Map.Entry<Integer, Integer> entry = iterator.next();
	System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}

複製代碼

經過Iterator迭代,實際上是調用了EntrySet的iterator方法

HashMap.EntrySet#iterator

public final Iterator<Map.Entry<K,V>> iterator() {
       return new EntryIterator();
}

複製代碼

能夠發現這裏其實只是返回了一個EntryIterator的對象,因此咱們須要看看這個EntryIterator.

EntryIterator

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

複製代碼

在實際使用中咱們經過iterator.next的方式獲取到當前節點,而在底層其實調用了父類HashIterator的nextNode方法,因此咱們看看父類HashIterator

HashIterator

abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
			//線程不安全,保存modCount
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
			//next初始化時,指向哈希桶上第一個不爲null的鏈表頭
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

 		
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();

			//依次取鏈表的下一個節點
            if ((next = (current = e).next) == null && (t = table) != null) {
				//若是當前鏈表節點遍歷完了,則取哈希桶的下一個不爲null的鏈表頭
                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刪除節點
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

複製代碼

從上面咱們會發現,在調用map.entrySet().iterator()的時候,實際上會構造一個HashIterator對象,並賦值哈希桶上第一個不爲null的鏈表頭給next,iterator.hasNext的操做其實就是判斷當前的next是否爲null,在nextNode中next會被賦值下一個節點,返回的是當前的節點。

3. for-each遍歷keySet和values

在實際中,有時候咱們會遍歷key和values的集合,通常狀況下都是調用map的keySet和values

實際使用

for (Integer key : map.keySet()) {
	System.out.println("Key = " + key);
}
for (Integer value : map.values()) {
	System.out.println("Value = " + value);
}

複製代碼

接着咱們看看其內部實現

HashMap

public Set<K> keySet() {
        Set<K> ks;
        return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
    }

複製代碼
final class KeySet extends AbstractSet<K> {
        public final int size() { return size; }
        public final void clear() { HashMap.this.clear(); }
        public final Iterator<K> iterator() { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

複製代碼

看來上面的代碼是否是跟entrySet的實現很相似,forEach方法也是一致的,不同的是iterator方法返回的是KeyIterator

KeyIterator

final class KeyIterator extends HashIterator implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

複製代碼

結果這個迭代器跟entrySet的迭代器基本是同樣的,只不過這裏返回的是nextNode的key。而values的遍歷其實跟entrySet和keySet基本是同樣的,最終都是HashIterator中的實現。有興趣的同窗能夠自行閱讀相關源碼,這裏再也不進行分析。

4. 小結

從遍歷的方法咱們也能夠發現,HashMap的遍歷是無序的,其順序是哈希桶從左往右,鏈表從上往下依次進行遍歷的

總結

在Java8中HashMap的底層數據結構是數組,稱之爲哈希桶。每一個桶裏放的是鏈表,鏈表中的節點就是哈希表的元素。當添加元素時,採用鏈地址法解決哈希衝突,若是鏈表的節點數超過8個就會將當前鏈表轉換成紅黑樹,來提升插入和查詢效率。因爲哈希桶是數組,因此存在擴容問題。當哈希表的容量達到閾值時或者初始化的時候,就會發生擴容。另外哈希表在實現過程當中用了不少位運算替代常規操做來提升效率。

參考博客:

相關文章
相關標籤/搜索