集合源碼學習之路---hashMap(jdk1.8)

hashMap簡單介紹

hashMap是面試中的高頻考點,或許平常工做中咱們只需把hashMap給new出來,調用put和get方法就完了。可是hashMap給咱們提供了一個絕佳的範例,展現了編程中對數據結構和算法的應用,例如位運算、hash,數組,鏈表、紅黑樹等,學習hashMap絕對是有好處的。
廢話很少說,要想學習hashMap,必先明白其數據結構。在java中,最基礎的數據結構就兩種,一種是數組,另一個就是模擬指針(引用),一塊兒來看下hashMap結構圖:
圖片描述java

類定義

從類定義上看,繼承於AbstractMap,並實現Map接口,其實就是裏面定義了一些經常使用方法好比size(),isEmpty(),containsKey(),get(),put()等等,Cloneable,Serializable 的做用在以前list章節已講述過就再也不重複了,總體來講類定義仍是蠻簡單的node

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

源碼分析

接下來會帶領你們閱讀源碼,有些不重要的,會咔掉一部分。面試

//初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認加載因子,用來計算threshold
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表轉成樹的閾值,當桶中鏈表長度大於8時轉成樹
static final int TREEIFY_THRESHOLD = 8;
//進行resize操做室,若桶中數量少於6則從樹轉成鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//當桶中的bin樹化的時候,最小hashtable容量,最少是TREEIFY_THRESHOLD 的4倍
static final int MIN_TREEIFY_CAPACITY = 64;
//在樹化以前,桶中的單個bin都是node,實現了Entry接口
 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 int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
    }

爲何hashMap容量老是2的冪次方?

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

hashMap中的hash算法,影響hashMap效率的重要因素之一就是hash算法的好壞。hash算法的好壞,咱們能夠簡單的經過兩個因素判斷,1是否高效2是否均勻。
你們都知道key.hashCode調用的是key鍵值類型自帶的哈希函數,返回int散列值。int值得位數有2的32次方,若是直接拿散列值做爲下標訪問hashMap主數組的話,只要hash算法比較均勻,通常是很難出現碰撞的。可是內存裝不下這麼大的數組,因此計算數組下標就採起了一種折中的辦法,就是將hash()獲得的散列值與數組長度作一個與操做。以下函數:算法

//或許你們會發現這個方法是jdk1.7,爲何不用1.8的呢?那是由於1.8裏已經去掉這個函數,直接調用,爲了講解方便,我從1.7中找出此方法方便學習
static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

hashMap的長度必須是2的冪次方,最小爲16.順便說一下這樣設計的好處。由於這樣(length-1)正好是一個高位掩碼,&運算會將高位置0,只保留低位數字。咱們來舉個例子,假設長度爲16,16-1=15,15的2進製表示爲
00000000 00000000 00000000 00001111.隨意定義一個散列值作&運算,結果如所示:編程

10101010 11110011 00111010 01011010
  & 00000000 00000000 00000000 00001111
  -------------------------------------
    00000000 00000000 00000000 00001010

也就是說實際上只截取了最低的四位,也就是咱們計算的索引結果。可是隻取後幾位的話,就算散列值分佈再均勻,hash碰撞也會很嚴重,若是hashcode函數自己很差,分佈上成等差數列的漏洞,使最後幾個低位成規律性重複,這就無比蛋疼了。這時候hash()函數的價值就體現出來了數組

h=key.hashcode()    11111011 01011111 00011100 01011011
        h>>>16    ^ 00000000 00000000 11111011 01011111
                  -------------------------------------
                    11111011 01011111 11100111 00000100
                  & 00000000 00000000 00000000 00001111
                  -------------------------------------
                    00000000 00000000 00000000 00000100

(h = key.hashCode()) ^ (h >>> 16),16正好是32的一半,其目的是爲了將本身的高半區和低半區作異或,混合高低位信息,以此來加大低位的隨機性,並且混合後的低位存在部分高位的特徵,算是變相的保留了高位信息。由此看來jdk1.8對於hash算法和計算索引值的設計就基本暴露在咱們的眼前了,不得不佩服設計之巧妙。數據結構

//返回大於等於cap且距離最近的一個2的冪   
//例子:cap=2 return 4;  cap=9 return 16;
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數組,保留着鏈表的頭結點或者紅黑樹的跟結點。
//當第一次使用的時候,會對其初始化,當須要擴容時,會調用resize方法。長度必定是2的冪
transient Node<K,V>[] table;
//用來遍歷的set集合,速度快於keySet
transient Set<Map.Entry<K,V>> entrySet;

transient int size;
//用來檢測使用iterator期間結構是否發生變化,變化則觸發fail-fast機制
transient int modCount;
//當容器內映射數量達到時,發生resize操做(threshold=capacity * load factor)
int threshold;
//加載因子,默認0.75
final float loadFactor;

get方法解析

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

hash函數以前已經研究過了,直接鎖定getNode()吧。經過hash函數算出hash值&上數組長度從而計算出索引值,而後遍歷比較key,返回對應值。併發

put和resize方法解析

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;
        //若table爲空,則調用resize()進行初始化,並將長度賦值給n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根據(n-1)&hash算出索引,獲得結點p,若p爲null,則生成一個新的結點插入。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //若p不爲null,則將p和插入結點的key與其hash值進行比較,若相同將p的引用同時賦值給e
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //若不一樣,且結點p屬於樹節點,則調用putTreeVal()
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //若不一樣,則將p當作鏈表的頭結點,循環比較,若爲null則新增節點,且循環次數大於等於TREEIFY_THRESHOLD - 1則從鏈表結構轉爲樹結構
                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;
                    }
                    //若鏈表中其中一結點的key與hash與插入結點一致,則跳出循環
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //若是插入的key存在,則替換舊值並返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //若插入的key在map中不存在,則判斷size>thresold
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    //初始化數組和擴容使用
    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];
        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;
    }

entrySet和keySet

hashMap中遍歷的方式是經過entrySet和keySet,爲了保證其效率,建議用entrySet由於他的存儲結構和hashMap一致。hashMap是如何維護entrySet的呢?經過閱讀源碼,發如今put的時候,並無對entrySet進行維護,且源碼中
entrySet方法只是new了個對象,那這個entrySet視圖的數據從哪而來呢?app

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

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        } 
    }
    
    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
    
    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() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == 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) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    }

經過閱讀EntrySet,咱們發現其iterator() 調用了EntryIterator(),而在對其進行實例化的時候會對其父類HashIterator進行實例化,從HashIterator的構造方法和nextNode咱們發現,其返回的視圖就是做用於table的,因此無需從新開闢內存。數據結構和算法

總結

本篇文章主要分析hashMap的存儲結構,分析了hashMap爲何容量始終是2的冪,分析了其hash算法的好壞和影響其效率的因素,同時也瞭解到了在put和get時作了哪些操做和其中數據結構的變化。最後經過hashMap常見的遍歷方式,得出entrySet是便利效率最高的,且hashMap維護entrySet的方式。經過學習,發現hashMap的設計很是優秀,但無奈能力有限,沒法將其精妙之處所有剖析開來。下節預告:分析一下併發下的hashMap有可能形成的閉環問題和concurrentHashMap

相關文章
相關標籤/搜索