Java 8 中HashMap源碼分析

HashMap的系統介紹:

HashMap實現了Map接口(注意:map類容器都沒有實現Collection接口,只有set,list這類的容器才實現Collection),其對一般的基本操作(put,get,contains)能夠保證常數時間,當然前提是hash function能讓各個key分佈的均勻。然而HashMap不能維護其內<key, value>對的順序,也不保證其中的順序是一直不變的。

有兩個參數能夠影響HashMap的性能: initial capacity 與 load factor,前者指創建HashMap時指定的bucket(抽象list)數量,即底層數組的length,默認爲16;後者指裝填因子,即當 NUMS(Entry) > load factor * capacity 時,自動擴充數組rehash,默認爲0.75。

此外,HashMap is not synchronized。可以使用工具類Collections中的方法:Map m = Collections.synchronizedMap(new HashMap(...));來獲取一個併發的hashmap。當遍歷HashMap時,有另一個Thread試圖修改hashmap,會立即終止迭代並拋出 ConcurrentModificationException ,即所謂的fail-fast策略。


實現原理的概述:

在hashmap的實際實現時,其底層爲bucket的數組(bucket=bin)。爲讓Node分佈更均勻,不至於扎堆集中到同一個bin中,通過key.hashCode()經位運算得一個h值,在利用此h值來計算數組下標index。據此index,在數組中定位到bucket,然後在bucket中進行查找或插入。bucket有list和tree兩種形式:list式的node更小,但是可能導致bucket很深,遍歷list時更耗時;treenode更大,但其能有效降低bucket的深度,能夠更加快速的遍歷bucket。在實際使用時,只有當map比較大時,纔會採用tree式的bucket,以空間換時間。


具體實現源碼分析:

以下分析均基於list數組形式,不考慮tree node(一個TreeNode大概是普通Node的兩倍大小)。且主要考慮get,put,contains三個方法

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //默認bucket數量(數組大小)爲16
static final int MAXIMUM_CAPACITY = 1 << 30;         //默認最大capacity爲2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;      //默認裝填因子爲0.75

static final int TREEIFY_THRESHOLD = 8;        //當一個bucket中多餘8的元素時,這個bin(bucket)就會轉換爲tree實現
static final int UNTREEIFY_THRESHOLD = 6;      //當bin中元素小於6時,就會轉換爲list形式實現bucket的功能
static final int MIN_TREEIFY_CAPACITY = 64;    //當大於64個bin時,纔會考慮向tree轉化
//幾個重要屬性
transient Node<K,V>[] table;     //bucket的數組,即底層list的數組
transient int size;              //<key, value>對的數量,調用size()方法返回的就是這個量
transient int modCount;          //記錄在迭代時,map被修改的次數,據此在併發環境下報告異常


list式bucket的node:
//利用靜態內部類Node來封裝<key-value>對數據,同時用next屬性來構成list結構的bucket
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;                                               //指向下一個Node,構成list,將index相同的Node,都裝到同一個bucket中

        Node(int hash, K key, V value, Node<K,V> next) {              //constructor
            ... ...
        }

        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) {
            ... ...
        }

        public final boolean equals(Object o) {                      //判斷兩個<key, value>是否完全相等
            ... ...
        }
    }

構造方法:

//在構造時注意,table size(即數組長度)永遠是2的n次方。例如按HashMap(15),table size應爲2^4=16
public HashMap(int initialCapacity, float loadFactor) {   //用戶指定初始數組大小及裝填因子
    ... ...
}
public HashMap(int initialCapacity) { ... ... }
public HashMap() { ... ... }

//靜態工具方法:
    static final int hash(Object key) {                  //根據key的hashCode來計算出一個值h
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//分析:在查找或插入Node時,不直接使用key.hashCode(),而將key.hashCode經過位運算求得一個h值,再根據h值確定數組下標index。
//原因:爲了讓Node能更加均勻的分佈到數組中各個bucket中,儘量避免扎堆

get() 與 containsKey()方法的實現:
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;                //使用h值來定位bucket
    }
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }


    final Node<K,V> getNode(int hash, Object key) {            //被get與contains調用的工具方法,輸入的hash不是key.hashCode,而是上面提到的h值
        Node<K,V>[] tab; 
        Node<K,V> first, e; 
        int n; K k;
                                                               //index = (table.length-1) & h ,定位到bucket
        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))))  //若這個bin中第一個元素即爲所找就直接返回
                return first;

            if ((e = first.next) != null) {                                   //否則,就得遍歷這個bucket來查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);     //bin爲tree式的

                do {                                                          //bin爲list式的
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;                                             //在遍歷bucket時得不斷調用equals()方法
                } while ((e = e.next) != null);                               
            }

        }
        return null;
    }

put(key, value)方法的實現:

    public V put(K key, V value) {                              //key已有時,就更新其對應的value,否則新建一個Node放入value
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {   //被put()調用的工具方法
        Node<K,V>[] tab; 
        Node<K,V> p; 
        int n, i;

        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;                     //resize()方法將底層bucket的數組table擴充爲2倍

        if ((p = tab[i = (n - 1) & hash]) == null)           //index = (table.length-1) & h ,定位到bucket
            tab[i] = newNode(hash, key, value, null);        //當這個bucket爲null時,就新建一個Node,這個Node就是此bin的第一個節點
        else {                                               //當bucket不爲null,有同key的就替換其value,否在就插入一個新Node
            Node<K,V> e; K k;                                //Node e 爲需要放入value的Node,要麼是bucket中已有的,要麼是新插入的

            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))   //bucket的第一個node與欲put的同key
                e = p;                                                                      //記錄下這個Node,在後面統一替換
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);             //tree式bucket
            else {                                                                          //list式bucket
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {                                //最初p是bucket中第一個Node,且上面已經判斷p不是所找的Node
                        p.next = newNode(hash, key, value, null);              //在循環中,不斷變更p,且利用binCount來記錄此bin中已有多少Node了
                        if (binCount >= TREEIFY_THRESHOLD - 1)                 //根據binCount判斷是否需要tree化bucket
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))  //在bucket中找到了同key的node,就停下,後面更新其value
                        break;                                                                     //若沒找到同key的就說明bin中沒有,就e = newNode
                    p = e;
                }
            }
            if (e != null) {                  //此時e要麼爲bucket中新插入的Node,要麼爲同key需要更新value的
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;                           //記錄修改次數,以使當併發錯誤時能拋出異常
        if (++size > threshold)               //更新size,並判斷是否需要擴容table
            resize();
        afterNodeInsertion(evict);
        return null;
    }
//此兩個方法在源碼中沒有具體實現,意思應該是當訪問或插入之後,回調此方法,完成一些額外的功能
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }

//再看一下map中的一個foreach方法,在進行迭代時,使用modCount來保證併發出錯時能終止迭代,並拋出異常
        public final void forEach(Consumer<? super 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.value);
                }
                if (modCount != mc)                      //在迭代時,若map被其他的線程修改了,就拋出異常
                    throw new ConcurrentModificationException();
            }
        }


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章