HashMap源碼閱讀(jekyll遷移)

layout: post
title: HashMap源碼閱讀
date: 2020-02-02
author: xiepl1997
tags: 源碼閱讀java

下面是JDK11中HashMap的源碼分析,對代碼的分析將主要以註釋的方式來體現。node

1 概述

1.1 HashMap的主要概念

  1. HashMap是基於Map接口實現的哈希表,實現了Map接口中的全部操做,並且HashMap容許鍵爲空值,也容許值爲空值,與之對應的是Hashtable,Hashtable不能將鍵和值設置爲空。HashMap不能保證元素的順序,特別是,它不能保證隨着時間的推移保持順序不變。算法

  2. HashMap爲基本操做(get和put)提供了恆定的時間性能,假設散列函數在木桶(buckets)中適當地分散了元素。集合(Collection)的迭代須要的時間與HashMap實例的「容量」和它的大小成比例。所以,若是迭代的性能很重要,要求很高,那麼不將初始容量設置得過高(或負載因素太低)是很是重要的。數組

  3. HashMap是線程不安全的集合,即當多線程訪問時,同一時刻若是沒法保證只有一個線程修改HashMap,則會毀壞HashMap,拋出ConcurrentModificationException。緩存

1.2 HashMap的基本實現

HashMap底層使用哈希表(數組 + 單鏈表),當鏈表過長會將鏈表轉成紅黑樹以實現O(logn)時間複雜度內查找。
HashMap的定義爲class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable安全

1.3 HashMap內部類

1.Node
2.KeySet
3.Values
4.EntrySet
5.HashIterator
6.KeyIterator
7.ValueIterator
8.EntryIterator
9.HashMapSpliterator
10.KeySpliterator
11.ValueSpliterator
12.EntrySpliterator
13.TreeNode 表明紅黑樹節點,HashMap中對紅黑樹的操做的方法都在此類中數據結構

1.4 擴容原理

HashMap採用的擴容策略是,每次加倍的方式。這樣,原來位置的Entry在新擴展的數組中要麼依然在原來的位置,要麼在原來的位置+原來的容量的位置。多線程

1.5 hash計算

HashMap經過hash()函數(也叫「擾動函數」)來計算hash值,方法爲key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;,計算出來的hash值存放在Node.hash中。函數

hash的值計算至關於將高16位與底16位進行異或,結果是高16位不變,底16位變成異或的新結果。爲何這樣作呢,緣由是HashMap擴容以前的數組大小才爲16,散列值是不能直接拿來用的。在進行長度取模運算時採用的只是取二進制中的最右端的幾位,並無用到高位二進制的信息,作帶來的結果就是hash結果分佈不太均勻。而將高16位和底16位異或後就可讓低位附帶高位的信息,加大低位的隨機性。源碼分析

在對散列值作完高低位的異或操做後,在對異或結果進行對長度的取模獲得最終的結果。具體參考JDK源碼中HashMap的hash方法原理是什麼?-胖君的回答-知乎

1.6 插入null原理

在hash計算中,null的hash值爲0,而後按照正常的putVal()插入。

1.7 new HashMap()

從源碼中(下文構造函數)咱們能夠看到,new HashMap()開銷很是少,僅僅確認裝載因子。真正的建立table的操做盡量的日後延遲,這使得HashMap有很多操做都須要檢查table是否初始化。這種設計有一種好處,就是可以沒必要擔憂HashMap的開銷,能夠一次性大量的建立HashMap。

2 HashMap的成員變量

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
	//用於序列化
	private static final long serialVersionUID = 362498820763181265L;
	//HashMap的默認容量是16
	static final int DEFAULT_INITIAL_CAPACITY = 16;
	//最大容量爲1073741824(2的30次方,即1<<30)
	static final int MAXIMUM_CAPACITY = 1073741824;
	//默認裝載因子爲0.75f
	static final float DEFAULT_LOAD_FACTOR = 0.75F;
	/*
	將鏈表轉化爲紅黑樹的閾值爲8,即當鏈表長度 >= 8時,鏈表轉化爲紅黑樹,也就是樹形化。
	爲何要樹形化呢?想一下咱們爲何要用HashMap,是由於經過Hash算法在理想狀況下時間複雜度O(1)就能找到元素,特別快,但僅限於理想狀況下,若是遇到了hash碰撞,且碰撞比較頻繁的話,那麼當咱們get一個元素的時候,定位到了這個數組,還須要在這個數組中遍歷一次鏈表最終才能找到要get的元素,是否是已經失去了hashmap的初心了?(由於須要遍歷鏈表,因此時間複雜度就高上去了)。
	因此使用紅黑樹這種數據結構來解決鏈表過長的問題,能夠理解爲紅黑樹遍歷比鏈表遍歷快,時間複雜度低。
	*/
	static final int TREEIFY_THRESHOLD = 8;
	//將紅黑樹轉化成鏈表的閾值爲6(<6時),這個是在resize()的過程當中調用TreeNode.split()實現
	static final int UNTREEIFY_THRESHOLD = 6;
	/*
	最小樹形化閾值。要樹化並不只僅是要超過TREEIFY_THRESHOLD,同時容量要超過MIN_TREEIFY_CAPACITY,若是隻是超過TREEIFY_THRESHOLD,則會進行擴容(調用resize())。爲何這個時候是擴容而不是樹形化呢?
	緣由就在於,形成鏈表過長也多是數組(桶)過短了也就是容量過小了。舉個例子,若是數組長度爲1,那麼全部的元素都擠在了數組的第0個位置上,這個時候就算樹形化只是治標不治本,由於引發鏈表過長的根本緣由是數組太短。
	因此在執行樹形化以前(鏈表長度>=8),會檢查數組長度,若是長度小於64,則對數組進行擴容,而不是樹形化。
	*/
	static final int MIN_TREEIFY_CAPACITY = 64;
	/*
	哈希表的數組主體定義,初始化時,在構造函數中並不會初始化,因此在各類操做中老是要先檢查table是否爲null。
	*/
	transient HashMap.Node<K, V>[] table;
	/*
	做爲一個entrySet緩存,使用entrySet首先檢查其是否爲null,不爲null則使用這個緩存,不然生成一個entrySet並緩存至此。
	*/
	transient Set<Entry<K, V>> entrySet;
	//HashMap中的Entry的數量
	transient int size;
	/*
	記錄修改內部結構化修改次數,用於實現fail-fast,ConcurrentModificationException就是經過檢測這個拋出。
	*/
	transient int modCount;
	//其值=capacity*loadFactor,當size超過threshold的時候便進行一次擴容
	int threshold;
	//裝載因子
	final float loadFactor;

	……
}

3 HashMap的方法

3.1 構造函數

  1. 該構造函數進行對容量和裝載因子的合法性驗證,然而沒有對容量進行存儲,只是用來肯定擴容閾值threshold。
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0) {
		throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
	} else {
		if (initialCapacity > 1073741824) {
			initialCapacity = 1073741824;
		}

		if (loadFactor > 0.0F && !Float.isNaN(loadFactor)) {
			this.loadFactor = loadFactor;
			this.threshold = tableSizeFor(initialCapacity);
		} else {
			throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
		}
	}
}
  1. 只傳入初始容量
public HashMap(int initialCapacity) {
	this(initialCapacity, 0.75F);
}
  1. 無參構造函數僅僅確認裝載因子
public HashMap() {
	this.loadFactor = 0.75F;
}
  1. 經過Map構造HashMap時,使用默認裝載因子,並調用putMapEntries將Map裝入HashMap
public HashMap(Map<? extends K, ? extends V> m) {
		this.loadFactor = 0.75F;
		this.putMapEntries(m, false);
	}

	final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
		//獲取該map實際長度
	    int s = m.size();
	    if (s > 0) {
	    	//判斷table是否初始化,若是沒有初始化
	        if (this.table == null) {
	        	/**求出須要的容量,由於實際使用的長度=容量*0.75得來的,+1是由於小數相除,基本都不會是整數,容量大小
				不能爲小數的,後面轉化爲int,多餘的小數就要被丟掉,因此+1,例如,map實際長度爲29.3,則所須要的容量爲30.
	        	*/
	            float ft = (float)s / this.loadFactor + 1.0F;
	            //判斷該容量大小是否超出上限
	            int t = ft < 1.07374182E9F ? (int)ft : 1073741824;
	            //對臨界值進行初始化,tableSizeFor(t)這個方法會返回大於t值的,且離其最近的2次冪,例如t爲29,則返回的值是32
	            if (t > this.threshold) {
	                this.threshold = tableSizeFor(t);
	            }
	        } else if (s > this.threshold) {  //若是table已經初始化,則進行擴容操做,resize就是擴容
	            this.resize();
	        }

	        Iterator var8 = m.entrySet().iterator();

	        //遍歷,把map中的數據轉移到hashmap中
	        while(var8.hasNext()) {
	            Entry<? extends K, ? extends V> e = (Entry)var8.next();
	            K key = e.getKey();
	            V value = e.getValue();
	            this.putVal(hash(key), key, value, false, evict);
	        }
	    }

	}

該構造函數,傳入一個Map,而後把該Map轉爲hashMap,resize方法在下面添加元素的時候會詳細講解,在上面entrySet方法會返回一個Set<Map.Entry<K,V>>,泛型爲Map的內部類Entry,它是一個存放key-value的實例,也就是Map中的每個key-value就是一個Entry實例,爲何使用這個方式進行遍歷,由於效率高,putVal方法把取出來的每一個key-value存入到hashMap中。

3.2 hash(Object key)

hash函數負責產生hashcode,計算方法爲若爲空則返回0,不然返回對key的高16位和底16位的異或的結果。

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

3.3 comparableClassFor(Object x)

這個方法是判斷傳入的Object對象x是否實現了Comparable接口,若是傳入的是String對象,天然實現了Comparable接口,直接返回就行。可是對於其餘的類,比方說咱們本身寫了一個類對象,而後存在HashMap中,可是就HashMap來講它並不知道咱們有沒有實現Comparable接口,甚至都不知道咱們Comparable接口中有沒有用泛型,泛型具體用的是哪一個類。

static Class<?> comparableClassFor(Object x) {
	if (x instanceof Comparable) {
		Class c;
		if ((c = x.getClass()) == String.class) {
			return c;
		}

		ype[] ts;
		if ((ts = c.getGenericInterfaces()) != null) {
			Type[] var5 = ts;
			int var6 = ts.length;

			for(int var7 = 0; var7 < var6; ++var7) {
				Type t = var5[var7];
				Type[] as;
				ParameterizedType p;
				if (t instanceof ParameterizedType && (p = (ParameterizedType)t).getRawType() == Comparable.class && (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) {
					return c;
				}
			}
		}
	}
	return null;
}

3.4 compareComparables(Class<?> kc, Object k, Object x)

若是x爲空,返回0;若是x的類型爲kc,則返回compareTo(x)。

static int compareComparables(Class<?> kc, Object k, Object x) {
	return x != null && x.getClass() == kc ? ((Comparable)k).compareTo(x) : 0;
}

3.5 tableSizeFor(int cap)

該函數用於計算大於等於cap的的最小的2的整數冪,用於作table的長度。numberOfLeadingZeros()方法的做用是返回無符號整形i的最高非零位前面的0的個數,包括符號位在內。

static final int tableSizeFor(int cap) {
	int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
	return n < 0 ? 1 : (n >= 1073741824 ? 1073741824 : n + 1);
}

3.6 put(K key, V value)

public V put(K key, V value) {
		//四個參數,第一個hash值,第四個參數表示若是該key存在值,若是爲null的話,則插入新的value,最後一個參數,在hashMap中沒有用,能夠不用管。
        return this.putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    	//tab哈希數組,p爲該哈希桶的首節點,n爲hashMap的長度,i爲計算出的數組下標
        HashMap.Node[] tab;
        int n;
        //獲取長度並擴容,使用的是懶加載,table一開始是沒有加載的,等puthou纔開始加載
        if ((tab = this.table) == null || (n = tab.length) == 0) {
            n = (tab = this.resize()).length;
        }

        Object p;
        int i;
        //若是計算出的該哈希桶的位置沒有值,則把新插入的key-value放到此處,此處就算沒有插入成功,也就是發生哈希衝突時也會把哈希桶的首節點賦予p
        if ((p = tab[i = n - 1 & hash]) == null) {
            tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
        } else {  //發生哈希衝突的幾種狀況
        	//e臨時節點的做用,k存放當前節點的key值
            Object e;
            Object k;
            //第一種,插入的key-value的hash值,key都與當前節點相等,e=p,則表示爲首節點
            if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
                e = p;
            }
            //第二種,hash值不等於首節點,判斷該p是否屬於紅黑樹的節點
            else if (p instanceof HashMap.TreeNode) {
            	/*
            	爲紅黑樹的節點,則在紅黑樹中進行添加,若是該節點已經存在,則返回該節點(不爲null),
            	該值很重要,用來判斷put操做是否成功,若是添加成功返回null
            	*/
                e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
            }
            //第三種,hash值不等於首節點,不爲紅黑樹節點,則爲鏈表的節點
            else {
            	//遍歷該鏈表
                int binCount = 0;
                while(true) {
                	//若是找到尾部,則代表添加的key-value沒有重複,在尾部進行添加。
                    if ((e = ((HashMap.Node)p).next) == null) {
                        ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
                        //判斷是否要轉化爲紅黑樹結構
                        if (binCount >= 7) {
                            this.treeifyBin(tab, hash);
                        }
                        break;
                    }

                    //若是鏈表有重複的key,e爲當前重複的節點,結束循環。
                    if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
                        break;
                    }

                    p = e;
                    ++binCount;
                }
            }
            //e不爲null,則說明有重複的key,則用待插入值進行覆蓋,返回舊值。
            if (e != null) {
                V oldValue = ((HashMap.Node)e).value;
                if (!onlyIfAbsent || oldValue == null) {
                    ((HashMap.Node)e).value = value;
                }

                this.afterNodeAccess((HashMap.Node)e);
                return oldValue;
            }
        }

        /*
        到了這步,說明待插入的key-value是沒有key的重複,由於插入成功的e節點的值爲null。
        修改次數+1
        */
        ++this.modCount;
        //實際長度+1,並判斷是否大於臨界值,大於則擴容
        if (++this.size > this.threshold) {
            this.resize();
        }

        this.afterNodeInsertion(evict);
        //添加成功
        return null;
    }

3.7 resize()

擴容方法resize()

final HashMap.Node<K, V>[] resize() {
    	//把沒有插入以前的哈希數組叫作oldTab
        HashMap.Node<K, V>[] oldTab = this.table;
        //oldTab的長度
        int oldCap = oldTab == null ? 0 : oldTab.length;
        //oldTab的臨界值
        int oldThr = this.threshold;
        //初始化new的長度和臨界值
        int newThr = 0;
        int newCap;
        //oldCap>0也就說明不是首次加載,由於hashMap用的是懶加載
        if (oldCap > 0) {
        	//若是大於最大值
            if (oldCap >= 1073741824) {
            	//將臨界值設置爲整數的最大值
                this.threshold = 2147483647;
                return oldTab;
            }
            //位置*。其餘狀況,擴容兩倍,而且擴容後的長度要小於最大值,old的長度也要大於16
            if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) {
            	//臨界值也要擴容爲old的2倍
                newThr = oldThr << 1;
            }
        }
        /*
        若是oldCap<0,可是已經初始化了,像把元素刪除完以後的狀況,那麼它的臨界值確定還存在,
        若是是首次初始化,它的臨界值則爲0.
        */
        else if (oldThr > 0) {
            newCap = oldThr;
        }
        //首次初始化,給默認值
        else {
            newCap = 16;
            newThr = 12;  //臨界值等於容量*0.75
        }
        //位置*的補充,也就是初始化時容量小於默認值16的,此時newThr沒有賦值
        if (newThr == 0) {
        	//new的臨界值
            float ft = (float)newCap * this.loadFactor;
            //判斷new容量是否大於最大值,臨界值是否大於最大值
            newThr = newCap < 1073741824 && ft < 1.07374182E9F ? (int)ft : 2147483647;
        }

        //把上面各類狀況分析出的臨界值,在此處進行真正的改變,也就是容量和臨界值都改變了
        this.threshold = newThr;
        //初始化
        HashMap.Node<K, V>[] newTab = new HashMap.Node[newCap];
        //賦予當前的table
        this.table = newTab;
        //此處天然是把old中的元素,遍歷到new中
        if (oldTab != null) {
            for(int j = 0; j < oldCap; ++j) {
            	//臨時變量
                HashMap.Node e;
                //當前哈希桶的位置值不爲null,也就是數組下標處有值,由於有值表示可能會發生衝突
                if ((e = oldTab[j]) != null) {
                	//把已經賦值以後的變量置位null,固然是爲了好回收,釋放內存
                    oldTab[j] = null;
                    //若是下標處的節點沒有下一個元素
                    if (e.next == null) {
                    	//把該變量的值存入newTab中,e.hash & new Cap-1並不等於j
                        newTab[e.hash & newCap - 1] = e;
                    }
                    //若是該節點爲紅黑樹結構,也就是存在hash衝突,該hash桶中有多個元素
                    else if (e instanceof HashMap.TreeNode) {
                    	//把此樹轉移到newTab中
                        ((HashMap.TreeNode)e).split(this, newTab, j, oldCap);
                    }
                    /*
                    此處表示爲鏈表結構,一樣把鏈表轉移到newTab中,就是把鏈表遍歷後,把值轉過去,再置位null
                    */
                    else {
                        HashMap.Node<K, V> loHead = null;
                        HashMap.Node<K, V> loTail = null;
                        HashMap.Node<K, V> hiHead = null;
                        HashMap.Node hiTail = null;

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

                            e = next;
                        } while(next != null);

                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }

                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回擴容後的hashmap
        return newTab;
    }

3.8 remove(Object key)

刪除元素

public V remove(Object key) {
    	//臨時變量
        HashMap.Node e;
        /*
        調用removeNode,第三個value表示,把key的節點直接都刪除了,不須要用到值,
        若是設爲值,則還須要去進行查找操做。
        */
        return (e = this.removeNode(hash(key), key, (Object)null, false, true)) == null ? null : e.value;
    }

    /*
    第一參數爲哈希值,第二個爲key,第三個爲value,第四個爲true的話,則表示刪除它
    key對應的value,不刪除key,第四個若是爲false,則表示刪除後,不移動節點。
    */
    final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    	//tab哈希數組,p數組下標節點,n長度,index當前數組下標
        HashMap.Node[] tab;
        HashMap.Node p;
        int n;
        int index;
        //哈希數組不爲null,且長度大於0,而後得到要刪除key的節點的數組下標位置
        if ((tab = this.table) != null && (n = tab.length) > 0 && (p = tab[index = n - 1 & hash]) != null) {
        	//node存儲要刪除的節點,e臨時變量,k當前節點的key,v當前節點的value
            HashMap.Node<K, V> node = null;
            Object k;
            //若是數組下標的節點正好是要刪除的節點,把值賦給臨時變量
            if (p.hash == hash && ((k = p.key) == key || key != null && key.equals(k))) {
                node = p;
            }
            //也就是要刪除的節點,在鏈表或者紅黑樹上,先判斷是否爲紅黑樹的節點
            else {
                HashMap.Node e;
                if ((e = p.next) != null) {
                    if (p instanceof HashMap.TreeNode) {
                    	//遍歷紅黑樹,找到該節點並返回
                        node = ((HashMap.TreeNode)p).getTreeNode(hash, key);
                    }
                    //若是是鏈表節點,遍歷找到該節點
                    else {
                        label88: {
                            while(e.hash != hash || (k = e.key) != key && (key == null || !key.equals(k))) {
                            	//p爲要刪除節點的上一個節點
                                p = e;
                                if ((e = e.next) == null) {
                                    break label88;
                                }
                            }

                            //node爲要刪除的節點
                            node = e;
                        }
                    }
                }
            }

            Object v;
            /*
            找到要刪除的節點後,判斷!matchValue,咱們正常的remove刪除,!matchValue都爲true
            */
            if (node != null && (!matchValue || (v = ((HashMap.Node)node).value) == value || value != null && value.equals(v))) {
                //若是刪除的節點是紅黑樹節點,則從紅黑樹中刪除
                if (node instanceof HashMap.TreeNode) {
                    ((HashMap.TreeNode)node).removeTreeNode(this, tab, movable);
                }
                //若是是鏈表節點,且刪除的節點爲數組下標節點,也就是頭節點,直接讓下一個做爲頭。
                else if (node == p) {
                    tab[index] = ((HashMap.Node)node).next;
                }
                //爲鏈表結構,刪除的節點在鏈表中,要把刪除的下一個節點設爲上一個節點的下一個節點。
                else {
                    p.next = ((HashMap.Node)node).next;
                }

                //修改計數器
                ++this.modCount;
                //長度減1
                --this.size;
                this.afterNodeRemoval((HashMap.Node)node);
                //返回刪除的節點
                return (HashMap.Node)node;
            }
        }

        return null;
    }

刪除還有clear方法,把全部的數組下標元素都置位null。

3.9 get()

下面在看較爲簡單的獲取元素。

public V get(Object key) {
        HashMap.Node e;
        //也是調用getNode方法來完成
        return (e = this.getNode(hash(key), key)) == null ? null : e.value;
    }

    final HashMap.Node<K, V> getNode(int hash, Object key) {
    	//tab哈希數組,first頭節點,n長度,k爲key
        HashMap.Node[] tab;
        HashMap.Node first;
        int n;
        //若是哈希數組不爲null,且長度大於0,獲取key值所在的鏈表頭賦值給first
        if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) {
            Object k;
            //若是是頭節點,則直接返回頭節點。
            if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) {
                return first;
            }

            HashMap.Node e;
            //結果不是頭節點
            if ((e = first.next) != null) {
            	//判斷是不是紅黑樹結構
                if (first instanceof HashMap.TreeNode) {
                	//去紅黑樹中找,而後返回
                    return ((HashMap.TreeNode)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;
    }

hashMap源碼暫時分析到這裏,能力有限,若是內容出現錯誤,歡迎指出。

相關文章
相關標籤/搜索