1. 程式人生 > >【java集合框架原始碼剖析系列】java原始碼剖析之HashMap

【java集合框架原始碼剖析系列】java原始碼剖析之HashMap

前言:之所以打算寫java集合框架原始碼剖析系列部落格是因為自己反思了一下阿里內推一面的失敗(估計沒過,因為寫此部落格已距阿里巴巴一面一個星期),當時面試完之後感覺自己回答的挺好的,而且據面試官最後說的這幾天可能會和你聯絡來看當時以為自己一面應該是通過的,但是事與願違,痛定思痛,仔細回顧了一下面試官問我的整個過程,感興趣的可以參看我的部落格:【阿里內推一面】記我人生的處女面。感覺自己回答的不是很好的地方就是關於java方面的,所以下定決心將java中的核心知識來個大的梳理,java中的核心知識可能很多,但在我看來大的型別就那麼幾個,其它的只是屬於細枝末節而已,主要是java集合框架,javaI/O體系,java中的執行緒,Java類的載入與JVM(包括java記憶體模型)這四大塊。其中最核心的當屬java集合框架,java中的執行緒與JVM相關的知識。

之所以最先講解java中的HashMap是因為HashMap應該是我們在做專案中用到的最多的處理複雜資料的集合,可以說使用率一點都不低於ArrayList與LinkList。

注:本人的原始碼基於JDK1.8.0,JDK的版本可以在命令列模式下通過java -version命令檢視。

一首先我們來看一下HashMap類的定義:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;<span style="font-family: Arial, Helvetica, sans-serif;">}</span>

從上述JDK原始碼可以看到:

1HashMap繼承自AbstractMap類同時實現了Cloneable,Serializable這兩個介面,其中前一個介面Cloneable是為了實現clonet()機制,Serializable介面是為了實現序列化機制,關於這兩種機制的相關知識再此不做贅述。

2HashMap用到了泛型來實現引數化型別,其實java中的全部集合框架都使用到了泛型。

二:HashMap中一些重要的成員屬性:

/**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    
    static final int MAXIMUM_CAPACITY = 1 << 30;//Hash陣列的最大容量,該值必須為2的n次,最大為1^32, MUST be a power of two <= 1<<30.
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//預設裝載因子
   transient Node<K,V>[] table;//用來儲存資料的陣列,每個元素都是Node即連結串列

    static final int TREEIFY_THRESHOLD = 8;//當add一個元素到某個位桶,其連結串列長度達到8時將連結串列轉換為紅黑樹
    
    transient Set<Map.Entry<K,V>> entrySet;
      transient int size;
// The next size value at which to resize (capacity * load factor).   int threshold;//臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴


其中transient關鍵字是用來讓該域在整個類被序列化的時候不包含該內容,即該域不被序列化。至於每個屬性的含義,上述程式碼英文註釋很詳細。重點說一下static final float DEFAULT_LOAD_FACTOR = 0.75f;這個屬性,該屬性即為裝載因子,本質上就是我們學習資料結構中解決Hash衝突的填充因子的意思,它的預設值是0.75,如果實際元素所佔容量佔分配容量的75%時就需要擴容。

三HashMap的內部實現原理:我們來看一下HashMap的構造器

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

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

   
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

   
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
可以看到java設計者們過載了4個HashMap的構造器,重點關注一下tableSizeFor(),putMapEntries這兩個函式,我們來看一下tableSizeFor的原始碼:
/**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

從註釋上就可以清楚的知道該函式的功能就是保證HashMap的容量Capacity屬性總是2的n次方,之所以這麼做原因在於確保Hash雜湊的均勻性,為何這樣做就能保證Hash的均勻性呢?這就需要看HashMap中Hash()函式的原始碼:
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
可以看到hash函式就是通過key.hashCode()得到int型別的h,然後通過 h&(h-1)得到所在陣列位置,注意>>>運算子表示無符號右移運算,所以結果就很清楚了,h為2的整數冪保證了h-1最後一位(當然是二進位制表示)為1,從而保證了取索引操作 h&(length-1)的最後一位同時有為0和為1的可能性,保證了雜湊的均勻性。反過來講,當Hash表長度length為奇數時,length-1最後一位為0,這樣與h按位與的最後一位肯定為0,即索引位置肯定是偶數,這樣陣列的奇數位置全部沒有放置元素,浪費了大量空間。總之:length為2的冪保證了按位與最後一位的有效性,使雜湊表雜湊更均勻。

接著我們來看一下: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);
            }
            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);
            }
        }
    }
可以看到首先獲得傳入的map例項的大小s,然後存在一個將大小s與臨界值比較的過程,如果map例項的大小大於threshold(即零界值的大小),則呼叫resize()方法,即擴容,我們來看一下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;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            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];//27行
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    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;
                            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;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

從上述程式碼可以看到

1首先定義了一個臨時陣列oldTab來儲存table,然後獲取該table的大小oldCap,如果oldCap的值大於MAXIMUM_CAPACITY(即HashMap所允許的最最大容量1>>30),則無法擴容,只能更改 threshold(即擴容的臨界值)的值,否則newCap = oldCap << 1,即令新的容量為原來的2倍,且oldCap >=DEFAULT_INITIAL_CAPACITY(從上上面HashMap中重要成員屬性這塊可以看到值為16),則將新的臨界值也更改為原來的2倍,即newThr = oldThr << 1;,即擴容機制包括兩部分:1HashMap中table陣列的容量的擴容,和成員屬性threshold(即擴容的臨界值)的更改。

2從上述程式碼的第27行可以看到,擴容之後需要建立一個新的Table陣列(該陣列中的每個元素為Node型別,即連結串列型別)   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];然後將原陣列中的類容重新計算hash值放到新的陣列中,分為兩種情況討論,這兩種情況對應兩種不同的資料結構型別,即連結串列和紅黑樹。及對應上述程式碼中的Node與TreeNode,如下所示:

else if (e instanceof TreeNode)
         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//如果為紅黑書,則呼叫split()
else{...}//否則,表示為連結串列節點
看到這就不得不提一下HashMap內部擴容機制所涉及到的資料結構連結串列Node與紅黑樹TreeNode了,這兩個資料結構為HashMap中的內部類,原始碼如下:
 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是一個單向連結串列(因為只存在一個Node<K,V> next屬性)它實現了Map.Entry<K,V>介面。
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;//表示顏色的屬性,紅黑樹是一種自平衡二叉查詢樹,用red與black來標識某個節點,它可以在O(logn)內進行查詢,插入與刪除
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
可以看到TreeNode它實現了LinkedHashMap.Entry<K,V>介面.

看到這裡我們也就明白了HashMap的底層實現原理了,即HashMap是採用陣列 Node<K,V>[]table來儲存<K,V>的,陣列中的每個元素是Node型別(可能會將該Node型別轉換為TreeNode型別),通常稱這種方式為位桶+連結串列/紅黑樹,當某個位桶的連結串列的長度達到TREEIFY_THRESHOLD臨界值的時候,這個連結串列就將轉換成紅黑樹。本質上是一個Hash表,用來解決衝突的(這一點將在HashMap中的put<K,V>方法中看到)。用圖示表示如下:


四HahMap常用的方法:

1put(K,V);

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


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)//1
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//2首先判斷tab[(n - 1) & hash]處是否為空,如果是代表該陣列下標為[(n - 1) & hash]的位置無元素,可直接put
            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))))//如果Hash值相同,則呼叫equals方法來確定是否存在該元素,則執行break語句
                        break;//跳出for迴圈,執行下面的if語句,即<span style="font-family: Arial, Helvetica, sans-serif;">existing mapping for key,則更新value的值,e.value=value。</span>

                    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;
    }
可以看到

1在註釋1處,首先判斷table陣列的長度是否為0或table陣列是否為空,即通常情況下表示剛建立一個空的HashMap時,當你呼叫put(K,V)方法時才會分配記憶體,即tab = resize()

if ((tab = table) == null || (n = tab.length) == 0)//1
            n = (tab = resize()).length;
2在註釋2處,首先判斷tab[(n - 1) & hash]處是否為空,如果是代表該陣列下標為[(n - 1) & hash]的位置無元素,可直接put,否則存在元素,出現衝突,則解決衝突,分為連結串列與紅黑樹這兩種情況。

即put方法主要包括兩大部分:

1根據傳入的key計算hash值得到插入的陣列索引i,如果tab[i]==null,表示此下標處無元素存在,可直接新增元素,否則出現衝突,那麼就要用到連結串列或紅黑樹來解決衝突,可參看上圖幫助理解,

2如果出現衝突,則掃描連結串列或紅黑樹,在此過程中通過equals方法來確定是否存在該元素,如果存在,則直接更新,否則採用連結串列或紅黑樹的方式將元素新增到tab[i]對應的連結串列或紅黑樹中,可參看上圖幫助理解。

即通過hash的值來判斷是否存在該元素,如果hash值不存在(tab[i]==null),則一定不存在該元素,若hash值存在,則可能存在該元素,需要通過equals方法來確定,如果hash值存在且key.equals.(k)則表明存在該元素,直接更新其值,否則表明不存在,則採用連結串列或紅黑樹的方式將元素新增到tab[i]對應的連結串列或紅黑樹中

2 V get(Object key) 

 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) {
        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) {     //通過該hash值與table的長度n-1相與得到陣列的索引first
            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)//代表該HashMap為陣列+紅黑樹結構
                    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某個元素與put某個元素是一一對應的關係,即先通過key得到對應的hash值,然後通過該hash值與table的長度n-1相與得到陣列下標的索引first,然後先判斷傳入的hash是否與陣列索引first節點對應的hash值相等,如果是則直接返回該陣列元素first,否則則通過first.next不斷查詢該陣列元素所對應的連結串列/紅黑樹中是否存在hash與key均和傳入的hash與key相等的節點,如果存在則代表在HashMap集合中找到了該元素,則返回其對應的Value。

五總結:

1HashMap內部是基於Hash表實現的,該Hash表為Node型別陣列+連結串列/紅黑樹,其中連結串列與紅黑樹是用來解決衝突的,即當往HashMap中put某個元素時,相同的hash值的兩個值會被放到陣列中的同一個位置上形成連結串列或紅黑樹。

2HashMap存在擴容機制,是通過resize()方法實現的,即當HashMap中的元素個數超過陣列大小*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,陣列的大小*loadFactor=threshold(即擴容的臨界值),預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍

3另外從put與get的原始碼可以看到HashMap的Key與Value都允許為null,同時可以看到HashMap中的put與get方法均無synchronized關鍵字修飾,即HashMap不是執行緒安全的。

4HashMap中的元素是唯一的(即同一個key只存在唯一的V與之對應),因為在put的過程中如果可能出現相同元素(K相同V不同),則原來的V將會被替換。