1. 程式人生 > >JDK8 HashMap原始碼解析,一篇文章徹底讀懂HashMap

JDK8 HashMap原始碼解析,一篇文章徹底讀懂HashMap

    在秋招面試準備中博主找過很多關於HashMap的部落格,但是秋招結束後回過頭來看,感覺沒有一篇全面、通俗易懂的講解HashMap文章(可能是博主沒有找到),所以在秋招結束後,寫下了這篇文章,盡最大的努力把HashMap原始碼講解的通俗易懂,並且儘量涵蓋面試中HashMap的考察點。

       就博主的經歷來看,HashMap是求職面試中名副其實的“明星”,基本上博主面試的每一家公司多多少少都有問到HashMap的底層實現原理等一些相關問題。

       這篇文章將會按以下順序來組織:

  1. HashMap原始碼分析(JDK8,通俗易懂)
  2. HashMap面試“明星”問題彙總,以及明星問題答案

下面是JDK8中HashMap的原始碼分析:

注:下文原始碼分析中

註釋多少與考察頻率成正比

註釋多少與考察頻率成正比

註釋多少與考察頻率成正比

  • HashMap的成員屬性原始碼分析
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;
    /**
     * The default initial capacity - MUST be a power of two.
     */

    //HashMap的初始容量為16,HashMap的容量指的是儲存元素的陣列大小,即桶的數量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    
    //HashMap的最大的容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */

   /**
     * DEFAULT_LOAD_FACTOR:HashMap的負載因子,影響HashMap效能的引數之一,是時間和空間之間的權 
   衡,後面會看到HashMap的元素儲存在Node陣列中,這個陣列的大小這裡稱為“桶”的大小.
      另外還有一個引數size指的是我們往HashMap中put了多少個元素。
      當size==桶的數量*DEFAULT_LOAD_FACTOR的時候,這時HashMap要進行擴容操作,也就是桶不能裝 
   滿。DEFAULT_LOAD_FACTOR是衡量桶的利用率:DEFAULT_LOAD_FACTOR較小時(桶的利用率較小),
   這時浪費的空間較多(因為只能儲存桶的數量*DEFAULT_LOAD_FACTOR個元素,超過了就要進行擴容),
   這種情況下往HashMap中put元素時發生衝突的概率也很小,所謂衝突指的是:多個元素被put到了同一個桶 
   中; 衝突小時(可以認為一個桶中只有一個元素)put、get等HashMap的操作代價就很低,可以認為是O(1);桶的利用率較大的時候(DEFAULT_LOAD_FACTOR很大,注意可以大於1,因為衝突的元素是使用連結串列或者紅黑樹連線起來的) 空間利用率較高,這也意味著一個桶中儲存了很多元素,這時HashMap的put、get等操作代價就相對較大,因為每一個put或get操作都變成了對連結串列或者紅黑樹的操作,代價肯定大於O(1),所以說DEFAULT_LOAD_FACTOR是空間和時間的一個平衡點;
     DEFAULT_LOAD_FACTOR較小時,需要的空間較大,但是put和get的代價較小;
     DEFAULT_LOAD_FACTOR較大時,需要的空間較小,但是put和get的代價較大)。

     擴容操作就是把桶的數量*2,即把Node陣列的大小調整為擴容前的2倍,至於為什麼是兩倍,分析擴容函 
 數時會講解,
     這其實是一個trick。Node陣列中每一個桶中儲存的是Node連結串列,當連結串列長度>=8的時候,
 連結串列會變為紅黑樹結構(因為紅黑樹的增刪改查複雜度是logn,連結串列是n,紅黑樹結構比連結串列代價更小)。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    //當某一個桶中連結串列的長度>=8時,連結串列結構會轉換成紅黑樹結構(其實還要求桶的中數量>=64,後面會提到)
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    //當紅黑樹中的節點數量<=6時,紅黑樹結構會轉變為連結串列結構
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    //上面提到的:當Node陣列容量>=64的前提下,如果某一個桶中連結串列長度>=8,則會將連結串列結構轉換成  
    //紅黑樹結構
    static final int MIN_TREEIFY_CAPACITY = 64;

}
  • HashMap內部類——Node原始碼分析

       下面介紹HashMap的內部類Node,HashMap的所有資料都儲存在Node陣列中那麼這個Node到底是個什麼東西呢?

//Node是HashMap的內部類
static class Node<K,V> implements Map.Entry<K,V> {
        //儲存key的hashcode=key的hashcode ^ (key的hashcode>>>16)這樣做主要是為了減少hash衝突
       //當我們往map中put(k,v)時,這個k,v鍵值對會被封裝為Node,那麼這個Node放在Node陣列的哪個
      //位置呢:index=hash&(n-1),n為Node陣列的長度。那為什麼這樣計算hash可以減少衝突呢?如果直接
      //使用hashCode&(n-1)來計算index,此時hashCode的高位隨機特性完全沒有用到,因為n相對於 
      //hashcode的值很小。基於這一點,把hashcode 高16位的值通過異或混合到hashCode的低16位,由此 
      //來增強hashCode低16位的隨機性
        final int hash; 
        final K key;//儲存map中的key
        V value;//儲存map中的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;
        }
  • HashMap  hash函式和tableSizeFor函式原始碼分析(這兩個函式後面會用到)
//HashMap允許key為null,null的hash為0(也意味著HashMap允許key為null的鍵值對),非null的key的hash高16位和低16位分別由由:key的hashCode
//高16位和hashCode的高16位異或hashCode的低16位組成。主要是為了增強hash的隨機性減少hash&(n-1)的
//隨機性,即減小hash衝突,提高HashMap的效能。所以作為HashMap的key的hashCode函式的實現對HashMap的效能影響較大,極端情況下:所有key的hashCode都相同,這是HashMap的效能很糟糕! 
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }


   /**
     在new HashMap的時候,如果我們傳入了大小引數,這是HashMap會對我們傳入的HashMap容量進行傳到
     tableSizeFor函式處理:這個函式主要功能是:返回一個數:這個數是大於等於cap並且是2的整數次冪 
     的所有數中最小的那個,即返回一個最接近cap(>=cap),並且是2的整數次冪的那個數。   
 
     具體邏輯如下:一個數是2的整數次冪,那麼這個數減1的二進位制就是一串掩碼,即二進位制從某位開始是一 
     串連續的1。 
*/
    static final int tableSizeFor(int cap) {
        //舉例而言:n的第三位是1(從高位開始數), 
        int n = cap - 1;

        n |= n >>> 1; 
        n |= n >>> 2; 
        n |= n >>> 4; 
        n |= n >>> 8; 
        n |= n >>> 16; 
        //舉例而言:如果n為: 00010000 00000000 00000000 000
        /*
        n |= n >>> 1;->n:00011000 00000000 00000000 0000
        n |= n >>> 2;->n: 00011110 00000000 00000000 0000
        n |= n >>> 4;->n: 00011111 11100000 00000000 0000
        n |= n >>> 8;->n: 00011111 11111111 11100000 0000
        n |= n >>> 16;->n:00011111 11111111 11111111 1111
        
        返回n+1:00010000 00000000 00000000 000(>=cap並且為2的整數次冪,與cap差值最小的那個)
        最後的n+1一定是2的整數次冪,並且一定是>=cap
        整體的思路就是:如果n二進位制的第k為1,那麼經過上面四個‘|’運算後[0,k]位都變成了1,
        即:一連串連續的二進位制‘1’(掩碼),最後n+1一定是2的整數次冪(如果不溢位)
        */
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • HashMap成員屬性原始碼分析
/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    //我們往map中put的(k,v)都被封裝在Node中,所有的Node都存放在table陣列中
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    //用於返回keySet和values
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    //儲存map當前有多少個元素
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    //failFast機制,在講解ArrayList和LinkedList一文中已經講過了
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)

   //這也是比較重要的一個屬性:
   //建立HashMap時,改變數的值是:初始容量(2的整數次冪)
   //之後threshold的值是HashMap擴容的門限值:即當前Node/table陣列的長度*loadfactor

 //舉個例子而言,如果我們傳給HashMap構造器的容量大小為9,那麼threshold初始值為16,在向HashMap中
   //put第一個元素後,內部會建立長度為16的Node陣列,並且threshold的值更新為16*0.75=12。
   //具體而言,當我們一直往HashMap put元素的時候,,如果put某個元素後,Node陣列中元素個數為13了 
   //,此時會觸發擴容(因為陣列中元素個數>threshold了,即13>threshold=12),具體擴容操作之後會 
  //詳細分析,簡單理解就是,擴容操作將Node陣列長度*2;並且將原來的所有元素遷移到新的Node陣列中。
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    //負載因子,見上面對DEFAULT_LOAD_FACTOR 引數的講解,預設值是0.75
    final float loadFactor;
  • HashMap構造器原始碼分析:
//構造器:指定map的大小,和loadfactor
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;//儲存loadfactor
        
        //注意,前面有講tableSizeFor函式,該函式返回值:>=initialCapacity、返回值是2的整數次冪
        //並且得是滿足上面兩個條件的所有數值中最小的那個數
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    //只指定HashMap容量的構造器,loadfactor使用的是預設的值:0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
     //無參構造器,預設loadfactor:0.75
     public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//其他不常用的構造器就不分析了

  從構造器中我們可以看到:HashMap是“懶載入”,在構造器中值保留了相關保留的值,並沒有初始化table<Node>陣列,當我們向map中put第一個元素的時候,map才會進行初始化!

  • HashMap的get函式原始碼分析
//入口,返回對應的value
public V get(Object key) {
        Node<K,V> e;
        
        //hash:這個函式上面分析過了。返回key混淆後的hashCode
        //注意getNode返回的型別是Node:當返回值為null時表示map中沒有對應的key,注意區分value為
        //null:如果key對應的value為null的話,體現在getNode的返回值e.value為null,此時返回值也是
        //null,也就是HashMap的get函式不能判斷map中是否有對應的key:get返回值為null時,可能不包 
        //含該key,也可能該key的value為null!那麼如何判斷map中是否包含某個key呢?見下面contains            
        //函式分析
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    public boolean containsKey(Object key) {
        //注意與get函式區分,我們往map中put的所有的<key,value>都被封裝在Node中,如果Node都不存在
        //顯然一定不包含對應的key
        return getNode(hash(key), key) != null;
    }



  //下面分析getNode函式
  /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key //下面簡稱:key的hash值
     * @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;
        //(n-1)&hash:當前key可能在的桶索引,put操作時也是將Node存放在index=(n-1)&hash位置
        //主要邏輯:如果table[index]處節點的key就是要找的key則直接返回該節點;
        //否則:如果在table[index]位置進行搜尋,搜尋是否存在目標key的Node:這裡的搜尋又分兩種:
        //連結串列搜尋和紅黑樹搜尋,具體紅黑樹的查詢就不展開了,紅黑樹是一種弱平衡(相對於AVL)BST,
        //紅黑樹查詢、刪除、插入等操作都能夠保證在O(lon(n))時間複雜度內完成,紅黑樹原理不在本文
        //範圍內,但是大家要知道紅黑樹的各種操作是可以實現的,簡單點可以把紅黑樹理解為BST,
        //BST的查詢、插入、刪除等操作的實現在之前的部落格中有java實現程式碼,實際上紅黑樹就是一種平            
        //衡的BST
        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;//沒找到,返回null
    }

   get函式實質就是進行連結串列或者紅黑樹遍歷搜尋指定key的節點的過程;另外需要注意到HashMap的get函式的返回值不能判斷一個key是否包含在map中,get返回null有可能是不包含該key;也有可能該key對應的value為null。HashMap中允許key為null,允許value為null。

  • HashMap的put函式原始碼分析
//put函式入口,兩個引數:key和value
public V put(K key, V value) {
        //下面分析這個函式,注意前3個引數,後面2個引數這裡不太重要,因為所有的put後面2個引數都一樣
        return putVal(hash(key), key, value, false, true);
    }



//下面是put函式的核心處理函式
/**
     * 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
     */

    //hash:key的hashCode
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //上面提到過HashMap是懶載入,所有put的時候要先檢查table陣列是否已經初始化了,
        //沒有初始化得先初始化table陣列,保證table陣列一定初始化了
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//這個函式後面有resize函式分析
    
        //到這裡表示table陣列一定初始化了
        //與上面get函式相同,指定key的Node,put在table陣列的i=(n-1)&hash下標位置,get的時候
        //也是從table陣列的該位置搜尋
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果i位置還沒有儲存元素,則把當前的key,value封裝為Node,儲存在table[i]位置
            tab[i] = newNode(hash, key, value, null);
        else {
       //如果table[i]位置已經有元素了,則接下來執行的是:
       //首先判斷連結串列或者二叉樹中時候已經存在key的鍵值對,存在的話就更新它的value
       //不存在的話把當前的key,value插入到連結串列的末尾或者插入到紅黑樹中
       //如果連結串列或者紅黑樹中已經存在Node.key等於key,則e指向該Node,即
       //e指向一個Node:該Node的key屬性與put時傳入的key引數相等的那個Node,後面會更新e.value
            Node<K,V> e; K k;

       //為什麼get和put先判斷p.hash==hash,下面的if條件中去掉hash的比較也可以邏輯也正確?
       //因為hash的比較是兩個整數的比較,比較的代價相對較小,key是泛型,物件的比較比整數比較
        //代價大,所以先比較hash,hash相等在比較key
            if (p.hash == hash &&//
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//e指向一個Node:該Node的key屬性與put時傳入的key引數相等的那個Node
            else if (p instanceof TreeNode)
               //紅黑樹的插入操作,如果已經存在該key的TreeNode,則返回該TreeNode,否則返回null
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //table[i]處存放的是連結串列,接下來和TreeNode類似
                //在遍歷連結串列過程中先判斷是key先前是否存在,如果存在則e指向該Node
                //否則將該Node插入到連結串列末尾,插入後判斷連結串列長度是否>=8,是的話要進行額外操作
                
                //binCountt最後的值是連結串列的長度
                for (int binCount = 0; ; ++binCount) {
                    
                    if ((e = p.next) == null) {
                   //遍歷到了連結串列最後一個元素,接下來執行連結串列的插入操作,先封裝為Node再插入
                   //p指向的是連結串列最後一個節點,將待插入的Node置為p.next,就完成了單鏈表的插入
                        p.next = newNode(hash, key, value, null);

                       
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //TREEIFY_THRESHOLD值是8, binCount>=7,然後又插入了一個新節點,            
                            //連結串列長度>=8,這時要麼進行擴容操作,要麼把連結串列結構轉為紅黑樹結構
                            //我們接下會分析treeifyBin的原始碼實現
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    //當p不是指向連結串列末尾的時候:先判斷p.key是否等於key,等於的話表示當前key
                    //已經存在了,令e指向p,停止遍歷,最後會更新e的value;
                    //不等的話準備下次遍歷,令p=p.next,即p=e
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            
            if (e != null) { // existing mapping for key
                //表示當前的key之前已經存在了,並且上面的邏輯保證:e.key一定等於key
                //這是更新e.value就好
                V oldValue = e.value;//儲存oldvalue
                
                //onlyIfAbsent默是false,evict為true
                //onlyIfAbsent為true表示如果之前已經存在key這個鍵值對了,那麼後面在put這個key 
                //時,忽略這個操作,不更新先前的value,這裡連線就好 
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//更新e.value
                
                //這個函式的預設實現是“空”,即這個函式預設什麼操作都不執行,那為什麼要有它呢?
                //這是個hook/鉤子函式,主要要在LinkedHashMap中,LinkedHashMap重寫了這個函式
                //後面講解LinkedHashMap時會詳細講解
                afterNodeAccess(e);
                return oldValue;//返回舊的value
            }
        }

        //如果是第一次插入key這個鍵,就會執行到這裡
        ++modCount;//failFast機制
        
        //size儲存的是當前HashMap中儲存了多少個鍵值對,HashMap的size方法就是直接返回size
        //之前說過,threshold儲存的是當前table陣列長度*loadfactor,如果table陣列中儲存的
        //Node數量大於threshold,這時候會進行擴容,即將table陣列的容量翻倍。後面會詳細講解
        //resize方法
        if (++size > threshold)
            resize();
        
        //這也是一個hook函式,作用和afterNodeAccess一樣
        afterNodeInsertion(evict);
        return null;
    }

    
    //將連結串列轉換為紅黑樹結構,在連結串列的插入操作後呼叫
/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        
        //MIN_TREEIFY_CAPACITY值是64,也就是當連結串列長度>8的時候,有兩種情況:
        //如果table陣列的長度<64,此時進行擴容操作
        //如果table陣列的長度>64,此時進行連結串列轉紅黑樹結構的操作
        //具體轉細節在面試中幾乎沒有問的,這裡不細講了,
        //大部同學認為連結串列長度>8一定會轉換成紅黑樹,這是不對的!!!
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
  • HashMap的resize函式原始碼分析

重點中的重點,面試談到HashMap必考resize相關知識

/**
     有兩種情況會呼叫當前函式:
        1.之前說過HashMap是懶載入,第一次hHashMap的put方法的時候table還沒初始化,這個時候會執 
          行resize,進行table陣列的初始化,table陣列的初始容量儲存在threshold中(如果從構造器 
          中傳入的一個初始容量的話),如果建立HashMap的時候沒有指定容量,那麼table陣列的初始容 
          量是預設值:16。即,初始化table陣列的時候會執行resize函式
        2.擴容的時候會執行resize函式,當size的值>threshold的時候會觸發擴容,即執行resize方法, 
          這時table陣列的大小會翻倍。

      注意我們每次擴容之後容量都是翻倍(*2),所以HashMap的容量一定是2的整數次冪,那麼HashMap的 
   容量為什麼一定得是2的整數次冪呢?(面試重點)
      要知道原因,首先回顧我們put key的時候,每一個key會對應到一個桶裡面,桶的索引是這樣計算的:
   index = hash & (n-1),index的計算最為直觀的想法是:hash%n,即通過取餘的方式把當前的key、 
   value鍵值對雜湊到各個桶中;那麼這裡為什麼不用取餘(%)的方式呢?原因是CPU對位運算支援較好,即位運算速度很快。另外一方面, 當n是2的整數次冪時:hash&(n-1)與hash%(n-1)是等價的,但是兩者效率來講是不同的,位運算的效率遠高於%運算。基於這一點,HashMap中使用的是hash&(n-1)。這還帶來了一個好處,就是將舊陣列中的Node遷移到擴容後的新陣列中的時候有一個很方便的特性: HashMap使用table陣列儲存Node節點,所以table陣列擴容的時候(陣列擴容)一定得是先重新開闢一個數組,然後把就陣列中的元素拷貝到新陣列中去。這裡舉一個例子來來說明這個特性:下面以Hash初始容量n=16,預設loadfactor=0.75舉例(其他2的整數次冪的容量也是類似的):
    預設容量:n=16,二進位制:10000;n-1:15,n-1二進位制:01111,即一連串1。某個時刻,map中元素
大於16*0.75=12,即size>12,此時我們新建了一個數組,容量為擴容前的兩倍,newtab len=32;接下來我們需要把table中的Node搬移(rehash)到newtab,從table的i=0位置開始處理,假設我們當前要處理table陣列i索引位置的node,那這個node應該放在newtab的那個位置呢?下面的hash表示node.key對應的hash值,也就等於node.hash屬性值,另外為了簡單,下面的hash只寫出了8位(省略的高位的0),實際上hash是32位:node在newtab中的索引:index=hash%len=hash&(len-1)=hash&(32-1)=hash&31=hash&(0x0001_1111)
;再看node在table陣列中的索引計算:i=hash&(16-1)=hash&15=hash&(0x0000_1111)
    注意觀察兩者的異同:i=hash&(0x0000_1111);index=hash&(0x0001_1111)
    這個表示式有個特點:index=hash&(0x0001_1111)=hash&(0x0000_1111) | hash&(0x0001_0000)
=hash&(0x0000_1111) | hash&n)=i+( hash&n )。什麼意思呢:hash&n要麼等於n要麼等於0;
也就是:inde要麼等於i,要麼等於i+n;再具體一點:當hash&n==0的時候,index=i;當hash&n==n的時候,index=i+n;這有什麼用呢?當我們把table[i]位置的所有Node遷移到newtab中去的時候:這裡面的node要麼在newtab的i位置(不變),要麼在newtab的i+n位置;也就是我們可以這樣處理:把table[i]這個桶中的node拆分為兩個連結串列l1和類:如果hash&n==0,那麼當前這個node被連線到l1連結串列;否則連線到l2連結串列。這樣下來,當遍歷完table[i]處的所有node的時候,我們得到兩個連結串列l1和l2,這時我們令newtab[i]=l1,newtab[i+n]=l2,這就完成了table[i]位置所有node的遷移/rehash,這也是HashMap中容量一定的是2的整數次冪帶來的方便之處。
    
  下面的resize的邏輯就是上面講的那樣。將table[i]處的Node拆分為兩個連結串列,這兩個連結串列再放到newtab[i]和newtab[i+n]位置
    
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    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;
            }
            //正常擴容:newCap = oldCap << 1
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //容量翻倍,擴容後的threshold自然也是*2
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //table陣列初始化的時候會進入到這裡
            newCap = DEFAULT_INITIAL_CAPACITY;//預設容量
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//threshold
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//更新threshold
        @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) {//之後完成oldTab中Node遷移到table中去
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)//j這個桶位置只有一個元素,直接rehash到table陣列
                        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;//第一個連結串列l1
                        Node<K,V> hiHead = null, hiTail = null;//第二個連結串列l2
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//rehash到table[j]位置
                            //將當前node連線到l1上
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                 //將當前node連線到l2上
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
    
                        if (loTail != null) {
                            //l1放到table[j]位置
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            //l1放到table[j+oldCap]位置
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • HashMap面試“明星”問題彙總,及答案 

    你知道HashMap嗎,請你講講HashMap?

           這個問題不單單考察你對HashMap的掌握程度,也考察你的表達、組織問題的能力。個人認為應該從以下幾個角度入手(所有常見HashMap的考點問題總結):

  1. size必須是2的整數次方原因
  2. get和put方法流程
  3. resize方法
  4. 影響HashMap的效能因素(key的hashCode函式實現、loadFactor、初始容量)
  5. HashMap key的hash值計算方法以及原因(見上面hash函式的分析)
  6. HashMap內部儲存結構:Node陣列+連結串列或紅黑樹
  7. table[i]位置的連結串列什麼時候會轉變成紅黑樹(上面原始碼中有講)
  8. HashMap主要成員屬性:threshold、loadFactor、HashMap的懶載入
  9. HashMap的get方法能否判斷某個元素是否在map中
  10. HashMap執行緒安全嗎,哪些環節最有可能出問題,為什麼?
  11. HashMap的value允許為null,但是HashTable和ConcurrentHashMap的valued都不允許為null,試分析原因?

上面問題的答案都可以在上面的原始碼分析中找到,下面在給三點補充:

  • HashMap的初試容量設計怎樣影響HashMap的效能的?

假如你估計你最多往HashMap中儲存64個元素,那麼你在建立HashMap的時候:如果選用無參構造器:預設容量16,在儲存16*loadFactor個元素之後就要進行擴容(陣列擴容涉及到連續空間的分配,Node節點的rehash,代價很高,所以要儘量避免擴容操作);如果給構造器傳入的引數是64,這時HashMap中在儲存64*loadFactor個元素之後就要進行擴容;但是如果你給構造器傳的引數為:(int)(64/0.75)+1,此時就可以保證HashMap不用進行擴容,避免了擴容時的代價。

  • HashMap執行緒安全嗎,哪些環節最有可能出問題,為什麼?

      我們都知道HashMap執行緒不安全,那麼哪些環節最優可能出問題呢,及其原因:沒有參照這個問題有點不好直接回答,但是我們可以找參照啊,參照:ConcurrentHashMap,因為大家都知道HashMap不是執行緒安全的,ConcurrentHashMap是執行緒安全的,對照ConcurrentHashMap,看看ConcurrentHashMap在HashMap的基礎之上增加了哪些安全措施,這個問題就迎刃而解了。後面會有分析ConcurrentHashMap的文章,這裡先簡要回答這個問題:HashMap的put操作是不安全的,因為沒有使用任何鎖;HashMap在多執行緒下最大的安全隱患發生在擴容的時候,想想一個場合:HashMap使用預設容量16,這時100個執行緒同時往HashMap中put元素,會發生什麼?擴容混亂,因為擴容也沒有任何鎖來保證併發安全,另外,後面的博文會講到ConcurrentHashMap的併發擴容操作是ConcurrentHashMap的一個核心方法。

  • HashMap的value允許為null,但是HashTable和ConcurrentHashMap的valued都不允許為null,試分析原因?

      首先要明確ConcurrentHashMap和Hashtable從技術從技術層面講是可以允許value為null;但是它是實際是不允許的,這肯定是為了解決一些問題,為了說明這個問題,我們看下面這個例子(這裡以ConcurrentHashMap為例,HashTable也是類似)

HashMap由於允value為null,get方法返回null時有可能是map中沒有對應的key;也有可能是該key對應的value為null。所以get不能判斷map中是否包含某個key,只能使用contains判斷是否包含某個key。

看下面的程式碼段,要求完成這個一個功能:如果map中包含了某個key則返回對應的value,否則丟擲異常

if (map.containsKey(k)) {
   return map.get(k);
} else {
   throw new KeyNotPresentException();
}
  1.  如果上面的map為HashMap,那麼沒什麼問題,因為HashMap本來就是執行緒不安全的,如果有併發問題應該用ConcurrentHashMap,所以在單執行緒下面可以返回正確的結果
  2. 如果上面的map為ConcurrentHashMap,此時存在併發問題:在map.containsKey(k)和map.get之間有可能其他執行緒把這個key刪除了,這時候map.get就會返回null,而ConcurrentHashMap中不允許value為null,也就是這時候返回了null,一個根本不允許出現的值?

但是因為ConcurrentHashMap不允許value為null,所以可以通過map.get(key)是否為null來判斷該map中是否包含該key,這時就沒有上面的併發問題了。