1. 程式人生 > >HashMap中的資料結構與get,put原始碼解析

HashMap中的資料結構與get,put原始碼解析

HashMap 執行流程:

首先構造方法:

public HashMap() {

        this.loadFactor =DEFAULT_LOAD_FACTOR;// all otherfields defaulted

    }

public HashMap(intinitialCapacity) {

        this(initialCapacity,DEFAULT_LOAD_FACTOR);

}

public HashMap(intinitialCapacity,floatloadFactor) {

}

public HashMap(Map<?extends K, ?extends

V>m) {

    }

通過過載方法HashMap傳入兩個引數,1.初始化容量,2.裝載因子

那麼就介紹下幾個名詞:

1.      capacity,表示的是hashmap中桶的數量,初始化容量initCapacity為16,第一次擴容會擴到64,之後每次擴容都是之前容量的2倍,所以容量每次都是2的次冪,

(為什麼HashMap的容量是2的次冪呢?

因為在原始碼中我們發現在通過hash值尋找putindex時進行的是一個位運算(n-1)&hash,位運算是基於二進位制的,所以是2的次冪;

通過位運算可以很快的找到putindex位置,所以hashMap的插入效率很高。

比如初始容量為16,一個待插入元素hash值為6,那麼我們一般要插入的位置就是index=6,也就是6%16我們進行的是取餘操作。那麼試試位運算(16-1&6

&位與運算:有00

0111   15

0110   6

_______

0110   6

通過取餘和位與運算我們都得到了想要的結果,那麼位運算的高效率肯定會被採納。

當容量為2n次冪時,減1後與任何數進行與運算都可以快速的得到取餘結果,也就是index的值。

)

//預設初始化容量為16

static final int DEFAULT_INITAL_CAPACITY=1<<4;  //aka 16

//中間會進行擴容操作,但是最大容量為2的30次方

static final int MAXIMUM_CAPACITY = 1 << 30;

2.  loadFactor,裝載因子,衡量hashmap一個滿的程度,初始化為0.75

//預設的裝載因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

   實時裝載因子是size/capacity;

3.  threshold,hashmap擴容的一個標準,每當size大於這個標準時就會進行擴容操作,threeshold等於capacity*loadfacfactor

/**

 The next sizevalue at which to resize (capacity * load factor).

**/

int threshold;

4.  size,表示HashMap中存放Node的數量,就是所有的鍵值對數量。

                String str="abc";
		String str1=new String("abc");
		Map<String, String> map=new HashMap<>();
		map.put("11", "22");
		map.put("11", "22");
		map.put(null, "22");	
		map.put(str,"1");
		map.put(str1,"1");
	
		System.out.println(map.size());

這個程式執行結果是3,從這裡我們可以看出鍵已經有了的話是不會在新增而是覆蓋的,而且可以允許null做值,null做鍵,當然值得注意的是str,str1,在這裡他size不考慮地址,只考慮內容,儘管str和str1指向的地址不同,但是put進去仍然是覆蓋不是新增。馬上我們會根據原始碼進行分析。

 /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

在初始化過程中,如果初始化容量和裝載因子都是使用者自己設定的,那麼會判斷初始化容量,如果小於0會丟擲異常,大於了2的30次方也就是最大容量時,會定為最大容量,判斷裝載因子,如果小於等於0或者他不是一個數字(通過Float.isNaN(loadFactor)),丟擲異常,在最後也初始化了threshold的值

當然如果通過傳Map進行初始化

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 /**
     * Implements Map.putAll and Map constructor
     *
     * @param m the map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion).
     */
    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);
            }
            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);
            }
        }
    }

會呼叫putMapEntries(Map<?extends K, ? extends V> m, boolean evict)方法

在這裡面首先通過獲取map的size,來判斷是否需要擴容,之後迴圈便利每一個元素放入hashmap中,使用的方法是putVal(hash(key), key, value, false, evict);,這個方法也是我們平時呼叫map.put(Kkey,V value)的核心。

public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

我們也會看到,putAll方法的原理也是這個函式。

在第一個引數我們傳的是hash(Key),

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


不同的key有著不同的hashCode(),只要hashCode()相同,hash一定相同,但是反之不成立,不同物件的hashCode()的hash是可能相同的,這就是所謂的hash衝突

那麼為什麼會出現相同的hash呢?

那就是(h=key.hashCode())^(h>>>16)

雖然每個元素的hashCode()是唯一的,但是他的二進位制右移(>>>是帶符號,>>不帶符號)16位就會出現重複的。h>>>16這樣只有超過2的16次方hash(key)才會有作用,也就是說在2的16次方內都為0。之後和hashCode進行異或。hash()就是為了讓均勻分佈,他會讓1111 0000 0000 0000變得1111 1110 1110 1111

讓”1”變得均勻點

下面我們看下hashmap的結構示意圖

 

我們可以看到 每一個元素就是一個Node<K,V>,這個Node<K,V>實現了Map.Entry<K,V> 介面。在jdk1.7中,它是一個HashMapEntry<K,V>

在JDK1.8之前,HashMap採用桶+連結串列實現,本質就是採用陣列+單向連結串列組合型的資料結構。它之所以有相當 快的查詢速度主要是因為它是通過計算雜湊碼來決定儲存的位置。HashMap通過key的hashCode來計算hash值,不同的hash值就存在陣列中不同的位置,當多個元素的hash值相同時(所謂hash衝突),就採用連結串列將它們串聯起來(連結串列解決衝突),放置在該hash值所對應的陣列位置上。在JDK1.8中,HashMap的儲存結構已經發生變化,它採用陣列+連結串列+紅黑樹這種組合型資料結構。當hash值發生衝突時,會採用連結串列或者紅黑樹解決衝突。當同一hash值的結點數小於8時,則採用連結串列,否則,採用紅黑樹。

我們現在對hashmap的儲存結構現在應該有了一個初步瞭解了吧,那麼我們就來看下,我們每次進行put(key)時到底是hashMap是如何處理的。

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

put中呼叫了 

final VputVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

 /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @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
     */
    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;
            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;
                    }
                    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;
    }

    該方法中有Node<K,V>[]tab; Node<K,V> p;

tab就是陣列,而p是每個桶

如果tab剛開始是null或者大小為0,則進行擴容操作resize(),返回值為Node<K,V>[],直接賦值給tab,初始化tab。

初始化之後通過位與運算(求餘)找到put的index,如果該位置沒有元素也就是tab[index]==null,那麼tab[i] =newNode(hash, key, value, null);即put成功

當然我們知道hash衝突是有的,所以當tab[index]!=null時,也就發生了hash衝突

 if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

 if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

第一個if其實考慮的是重複鍵,第二個if我們可以看到綠色的註釋說的是在map中已經存在key了,所以這兩步是對於已有key情況下的節點put的一個處理。

如果不是重複的,那麼就看p是否是樹節點,因為jdk1.8中採用的是紅黑樹,所以要考慮樹節點,如果是樹節點就進行樹節點的put,e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,value);對於樹節點的插入我們這裡就不多做解釋了

如果上述情況都不是,那就是hash衝突並且使用連結串列處理了;。

 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;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }


通過e=p.next進行一個連結串列遍歷,,

如果等於null也就是說遍歷到了末尾也沒發現重複的key,那麼就是就執行一個插入操作,是一個尾插法,jdk1.8之前是頭插法,jdk1.8是尾插法,

那麼為什麼jdk1.8是頭插,之前為頭插法呢

1.8為什麼尾插我覺得大家通過上面這段話應該都可以在知道原因吧,因為我已經遍歷到了連結串列尾部了,尾插不就更省事嗎?

可是有些人問了1.7是單獨的陣列加連結串列,那不應該也尾插嗎?這就有一個效率問題了,因為jdk1.8每當節點>8時就會變為樹,而樹的遍歷會更加快速,

而連結串列遍歷最多也就是7次,效率還是很高的,可是1.7就不是這樣了,如果你有10000個節點,那你如果尾插的話就需要遍歷10000次,這是非常耗時的,所以1.7採用的是頭插法。                                                                                                                   

         再插入過程中,如果桶中節點個數大於樹的閾值TREEIFY_THRESHOLD-1,就會進行樹化。從連結串列變為紅黑樹

 ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

最後進行一個判斷,看size是否到達了擴容標準,如果達到了進行擴容resize();

resize():

如果為空,則按threshold分配空間,(預設是陣列=16,裝載因子=0.75f,閾值=16*0.75),否則,加倍後,每個容器中的元素在新table中要麼呆在原索引處,

要麼有一個2的次冪的位移(這也是保證了hashmap中的元素分配均勻)

                                                                                                                                                                                                                                                                                       

到這裡我們平常所用的put方法就結束了

下面我們看下get方法

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

    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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 && // 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方法比較簡單

主要是getNode(int hash,Object key)

直接判斷hashmap中的桶是否為空,並且看tab[index]是否為空,如果為空則返回null

否則檢查tab[index]處Node的屬性,看key是否相等,相等返回改Node,不是則遍歷該桶中的節點。

利用first.next遍歷,如果是樹節點則getTreeNode(hash,key),是連結串列節點的話遍歷連結串列尋找。