天天看點

JDK源碼解析集合篇--HashMap無敵全解析

前兩篇寫了Collection體系的List下的ArrayList與LinkedList,另外一部分是Set集合下的集合類,但是Set集合的實作類基本是由Map集合的實作類實作的,是以先分析一下重要的HashMap。

數組存儲區間是連續的,占用記憶體嚴重,故空間複雜的很大。但數組的二分查找時間複雜度小,為O(1);數組的特點是:尋址容易,插入和删除困難;連結清單存儲區間離散,占用記憶體比較寬松,故空間複雜度很小,但時間複雜度很大,達O(N)。連結清單的特點是:尋址困難,插入和删除容易。綜合兩者的特性,做出一種尋址容易,插入删除也容易的資料結構。

由第一篇綜述JDK源碼解析集合篇–綜述 可以看到HashMap實作了Map接口。

類定義為:

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

在上邊的定義,我們可能會很奇怪,為什麼HashMap繼承了AbstractMap還要去實作Map接口呢,而且在HashSet,LinkedHashSet,LinkedHashMap都出現了這種定義,對于這個問題有很多解釋,但集合類的作者Josh Bloch 承認這是個錯誤,具體可以看StackOverFlower上的解釋Why does LinkedHashSet extend HashSet and implement Set 。

對于Map集合,我們常見的也就:HashMap,LinkedHashMap,Hashtable與TreeMap。由于HashMap中知識點很多,是以在面試中經常會考HashMap的實作原理。

HashMap是一種非常常見、友善和有用的集合,是一種鍵值對(K-V)形式(哈希表)的存儲結構,下面将還是用圖示的方式解讀HashMap的實作原理,

下邊關于HashMap的特點可做個總結:

HashMap是否允許空 ———– Key和Value都允許為空

HashMap是否允許重複資料——— Key重複會覆寫、Value允許重複(但hash可能會重複。沖突)

HashMap是否有序 ———無序,特别說明這個無序指的是周遊HashMap的時候,得到的元素的順序基本不可能是put的順序

HashMap是否線程安全———非線程安全(可能會出現死循環)

HashMap源碼

閱讀其實作,我們首先要看它的底層存儲結構是什麼,從結構實作來講,HashMap是數組+連結清單+紅黑樹(JDK1.8增加了紅黑樹部分)實作的,如下如所示。

JDK源碼解析集合篇--HashMap無敵全解析

其實也就是利用哈希表(散清單)這種資料結構來實作的。關于哈希表:散清單(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行通路的資料結構。也就是說,它通過把關鍵碼值映射到表中一個位置來通路記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散清單。這也就是我們的table數組(包括用來解決沖突的連結清單結構)。

下圖給出HashMap的字段:

JDK源碼解析集合篇--HashMap無敵全解析

從HashMap的屬性值,我們可以很好了解上邊的說法。我們可以看一下Node的定義:

//但連結清單結構
 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;
        }
  }
           

要注意:在JDK1.8後,加入了紅黑樹進行優化,存儲紅黑樹的是TreeNode:

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;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
    }
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
           

我們從代碼中可以看到,TreeNode加入相應的紅黑樹的定義屬性,TreeNode是Node的子類,是以table數組就定義的是Node[],這并不會影響後邊轉紅黑樹的操作。

字段分析

HashMap就是使用哈希表來存儲的。哈希表為解決沖突,可以采用開放位址法和鍊位址法等來解決問題,Java中HashMap采用了鍊位址法。鍊位址法,簡單來說,就是數組加連結清單的結合。在每個數組元素上都一個連結清單結構,當資料被Hash後,得到數組下标,把資料放在對應下标元素的連結清單上。

注意:如果哈希桶數組很大,即使較差的Hash算法也會比較分散,如果哈希桶數組數組很小,即使好的Hash算法也會出現較多碰撞,是以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況确定哈希桶數組的大小,并在此基礎上設計好的hash算法減少Hash碰撞。那麼通過什麼方式來控制map使得Hash碰撞的機率又小,哈希桶數組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。

//初始化node數組大小為16    
    //The default initial capacity - MUST be a power of two.  這跟hash算法有關
    static final int DEFAULT_INITIAL_CAPACITY =  << ; // aka 16
    //最大容量
    static final int MAXIMUM_CAPACITY =  << ;
    //預設負載因子
    static final float DEFAULT_LOAD_FACTOR = f;//填充比
    //當add一個元素到某個位桶,其連結清單長度達到8時将連結清單轉換為紅黑樹
    static final int TREEIFY_THRESHOLD = ;
    //由樹轉換成連結清單的門檻值UNTREEIFY_THRESHOLD  當進行删除操作時,轉成連結清單的的門檻值
    static final int UNTREEIFY_THRESHOLD = ;
    //在轉變成樹之前,還會有一次判斷,隻有鍵值對數量(size)大于 64 才會發生轉換。這是為了避免在哈希表建立初期,
    //多個鍵值對恰好被放入了同一個連結清單中而導緻不必要的轉化。這個至少是TREEIFY_THRESHOLD 的4倍
    static final int MIN_TREEIFY_CAPACITY = ;

    transient Node<k,v>[] table;//存儲元素的數組

    transient Set<map.entry<k,v>> entrySet;
    //存放元素node的個數
    transient int size;
    //被修改的次數fast-fail機制  記錄結構變化的次數(主要是删除添加)
    transient int modCount;
    //臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容  也就是。
    //threshold = length * Load factor 當size>threshold 時,進行擴容
    int threshold;
    final float loadFactor;  //負載因子
           

結合負載因子的定義公式可知,threshold就是在此Load factor和length(數組長度)對應下允許的最大元素數目,超過這個數目就重新resize(擴容),擴容後的HashMap容量是之前容量的兩倍。預設的負載因子0.75是對空間和時間效率的一個平衡選擇,建議大家不要修改,除非在時間和空間比較特殊的情況下,如果記憶體空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值;相反,如果記憶體空間緊張而對時間效率要求不高,可以增加負載因子loadFactor的值,這個值可以大于1。

當在建立HashMap時,可以指定數組的初始化大小和負載因子。其構造器源碼為:

/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
           

看上邊的代碼時,不知道大家有沒有想,到底table數組和threshold都在哪初始化的,我在看的時候也是找了好久,由構造器我們可以看到,這時候還沒有進行數組初始化工作,當我們添加元素時:

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還沒初始化時,就進行擴容操作,也就是在這裡進行的初始化
        if ((tab = table) == null || (n = tab.length) == )
            n = (tab = resize()).length;
     }
           

在resize()函數中,你可以看到起初始化工作,在這裡我不詳細解釋其代碼,在後邊擴容講解時,在詳細介紹。其實就是下邊的代碼:

newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
      threshold = newThr;
      @SuppressWarnings({"rawtypes","unchecked"})
      Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
      table = newTab;
           

hash原理

為了減少沖突,設計好的哈希函數是非常必要的。如果完全沒沖突,則HashMap增删該查的時間複雜度将為O(1)。這裡還要強調一下:當使用某個位置的連結清單長度大于8時,轉化為紅黑樹,使查找/删除/添加的時間複雜度是O(logn),而不會是O(n)。 在沖突的那個bin上插入的時間複雜度是O(n),源碼是插入到連結清單最後,因為它要先尋址,因為它先要查找是否有重複的key,再執行插入。

為了減少沖突:在HashMap中,哈希桶數組table的長度length大小必須為2的n次方(一定是合數),這是一種非正常的設計,正常的設計是把桶的大小設計為素數。相對來說素數導緻沖突的機率要小于合數,具體證明可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小為11,就是桶大小設計為素數的應用(Hashtable擴容後不能保證還是素數)。HashMap采用這種非正常設計,主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。

為什麼要把length設定為2的n次方,這在後邊的雜湊演算法中有應用:JDK1.8的哈希函數為:

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

這裡的哈希函數就是3步:1 取key的hashcode值 2 将高位與低位進行運算(通過hashCode()的高16位異或低16位實作的,主要是從速度、功效、品質來考慮的,這麼做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。) 3 取模定位到數組位置

我們會思考,為什麼這裡沒有定位取模這一步呢,這是因為這一步運算又被融合到了put操作中,我們可以單獨取出這一個操作:

tab[i = (n - ) & hash]
           

我們這裡的取模運算非常巧妙地運用了數組長度也就是n隻能是2的n次幂的特點,利用&操作代替mod操作,提高了效率。具體的解釋可以看下圖:

JDK源碼解析集合篇--HashMap無敵全解析

put方法解析

添加元素的流程圖為:

JDK源碼解析集合篇--HashMap無敵全解析

下邊結合代碼來解釋一下此過程:

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

  /**
     * 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;
        //table為null,則說明table數組還沒有進行初始化,在resize中初始化
        if ((tab = table) == null || (n = tab.length) == )
            n = (tab = resize()).length;
        //如果定位到table[i]沒元素,則直接放入到該位置,并符p指派
        if ((p = tab[i = (n - ) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //如果已經有元素,則進行沖突解決
        else {
            Node<K,V> e; K k;
            //p是table[i]的Node值,如果插入的Node的key與此Node值相等(hash和equals相等)
            //則将e指派為p
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
           //判斷是否是TreeNode,是的話執行紅黑樹插入,不是的話執行連結清單插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //連結清單插入
            else {
                for (int binCount = ; ; ++binCount) {
                    //找到連結清單尾部
                    if ((e = p.next) == null) {
                        //插入到連結清單尾部
                        p.next = newNode(hash, key, value, null);
                       //如果達到8,則進行樹化
                        if (binCount >= TREEIFY_THRESHOLD - ) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //若key相等,則e是不為null的
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e不為null,說明有key重複,則直接覆寫e指向的node的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //size+1,看是否需要擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
           

上邊的源碼中,邏輯非常清楚,實作也非常巧妙,看完很有感覺。從源碼中,可以看到,新put的node,如果有沖突,會放到連結清單尾部。

上邊的紅黑樹插入,以及樹化操作時紅黑樹的内容,較為複雜,這裡先不進行解釋。關于紅黑樹可參看:

紅黑樹概念、紅黑樹的插入及旋轉操作詳細解讀 和紅黑樹的移除節點操作 。

下邊我們重點看一下HashMap的擴容過程。

擴容機制

下邊是JDK1.8 hashMap的擴容代碼:在代碼中進行注釋解釋。

在開始之前,因為涉及rehash過程,在解釋代碼之前,必須要搞明白jdk1.8的一個優化點:在進行rehash時,1.7是重新計算hash然後進行&(n-1)定位的,但是jdk1.8進行了優化,解決了重新計算hash進行定位的計算。經過觀測可以發現,我們使用的是2次幂的擴充(指長度擴為原來2倍),是以,元素的位置要麼是在原位置,要麼是在原位置再移動2次幂的位置。看下圖可以明白這句話的意思,相當于多了一個高位1。

JDK源碼解析集合篇--HashMap無敵全解析
JDK源碼解析集合篇--HashMap無敵全解析

是以,我們在擴充HashMap的時候,不需要像JDK1.7的實作那樣重新計算hash,隻需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。這個設計确實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是随機的,是以resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。

其實我覺得這樣也并沒有進行多少優化,因為1.7進行定位時:hash也是知道的,隻是進行hash&(n-1)進行定位的,與hash&n == 1{i+n} 都是位運算,也差不多。

但這也是為什麼長度取2的幂的另一個應用。

/**
     * 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
     */
    //完成擴容(容量變為原來的2倍),且完成rehash
    final Node<K,V>[] resize() {
        //獲得原來的table數組
        Node<K,V>[] oldTab = table;
        //原table數組的容量
        int oldCap = (oldTab == null) ?  : oldTab.length;
        //原擴容門檻值
        int oldThr = threshold;
        //定義新容量與門檻值
        int newCap, newThr = ;
        //如果原容量>0
        if (oldCap > ) {
            //如果原容量已經達到最大了1<<30,則不進行擴容,隻調整門檻值為最大,随其碰撞了
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果沒達到最大,則變為原來容量的2倍
            //其實這句可分解
            //newCap = oldCap << 1
            //如果擴容後的容量小于最大容量才會将門檻值變為原來的2倍
            //else if (newCap < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            //         newThr = oldThr << 1; // double threshold
            else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << ; // double threshold
        }
        //如果newCap = 0,oldThr > 0 這是适用于不同的構造函數的
        else if (oldThr > ) // 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);
        }
        //如果擴容後的容量大于最大容量了1<<30
        if (newThr == ) {
            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;
        //完成rehash
        if (oldTab != null) {
            //對原數組的沒一個位置   這是一個周遊   是以rehash過程的是很耗費時間的
            for (int j = ; j < oldCap; ++j) {
                Node<K,V> e;
                //e = oldTab[j])
                if ((e = oldTab[j]) != null) {
                    //将原位置設為null
                    oldTab[j] = null;
                    //如果沒有碰撞,也就是隻有這一個元素,直接定位設定到新數組的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - )] = e;
                    //如果目前節點是TreeNode類型,說明已經樹化了,紅黑樹的rehash過程
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //表明目前節點沖突是連結清單存儲的,完成rehash   
                    //注意:這是1.8的優化點,這也是容量聲明為2的次幂的另一個應用
                    else { // preserve order
                        //rehash後還是原位置
                        Node<K,V> loHead = null, loTail = null;
                        //rehash後變為j+oldCap位置
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //如果擴大的那一位hash還是0,則說明它還是在原位置
                            if ((e.hash & oldCap) == ) {
                                //如果它是第一個被加入的
                                if (loTail == null)
                                    loHead = e;
                                //進行連結
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //定位到j+oldCap位置
                            else {
                                //如果它是第一個被加入的
                                if (hiTail == null)
                                    hiHead = e;
                               //進行連結     
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //在newTab相應位置設定完成rehash的連結清單
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
           

從我上邊的代碼注釋中,我相信大家已經很容易了解此過程,但是我們沒有涉及紅黑樹的相應操作,單單看其連結清單的操作,我們就可以看到其代碼寫的确實很好。

與JDK1.7的差別

這是基于jdk1.8的,那它與1.7有什麼差別呢?最大的差別當然是關于紅黑樹的那部分操作,另一部分是hash定位的算法,這在上邊已經分析過了。還有一點注意差別,JDK1.7中rehash的時候,舊連結清單遷移新連結清單的時候,如果在新表的數組索引位置相同,則連結清單元素會倒置,但是從1.8的源碼可以看出,JDK1.8不會倒置。

JDK1.8中當元素定位的哈希桶是一個連結清單時,則采用尾插入法。首先從頭周遊連結清單,根據equals和hashCode來比較key是否相同。是以作為hashMap的key必須同時重載equals方法和hashCode方法。

JDK 1.7的連結清單操作采用了頭插入法,即新的元素插入到連結清單頭部。在JDK1.8中采用了尾插入法。插入以後如果連結清單長度大于8,那麼就會将連結清單轉換為紅黑樹。因為如果連結清單長度過長會導緻元素的增删改查效率低下,呈現線性搜尋時間。JDK1.8采用采用紅黑樹進行優化,進而提高HashMap性能。

如果哈希桶是一個紅黑樹,則直接使用紅黑樹插入方式直接插入到紅黑樹中。

下邊是jdk1.7的rehash過程。因為jdk1.7使用的是頭插法,它依次周遊數組每個bin上的連結清單,完成rehash。

我們從代碼中就不難了解,由于1.7采用的是頭插法,是以JDK1.7中rehash的時候,舊連結清單遷移新連結清單的時候,如果在新表的數組索引位置相同,則連結清單元素會倒置。(後在前,前在後)

void transfer(Entry[] newTable) {
     Entry[] src = table;                   //src引用了舊的Entry數組
     int newCapacity = newTable.length;
     for (int j = ; j < src.length; j++) { //周遊舊的Entry數組
         Entry<K,V> e = src[j];             //取得舊Entry數組的每個元素
         if (e != null) {
             src[j] = null;//釋放舊Entry數組的對象引用(for循環後,舊的Entry數組不再引用任何對象)
             do {
                 Entry<K,V> next = e.next;
                 int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置
                 e.next = newTable[i]; //标記[1]
                 newTable[i] = e;      //将元素放在數組上
                 e = next;             //通路下一個Entry鍊上的元素
             } while (e != null);
         }
     }
 }
           

JDK1.8與JDK1.7對比,當hash沖突較多時,顯然是JDK1.8效率更高。

線程安全性

HashMap線程不安全的展現在哪?主要是resize和疊代器的fail-fast上

在多線程使用場景中,應該盡量避免使用線程不安全的HashMap,而使用線程安全的ConcurrentHashMap。那麼為什麼說HashMap是線程不安全的,下面舉例子說明在并發的多線程使用場景中使用HashMap可能造成死循環。resize死循環(會形成環形連結清單)。

jdk1.7出現的resize死循環比較好了解,可以參看這篇文章:談談HashMap線程不安全的展現 。但因為1.8保持了連結清單原來的順序不變,JDK 1.8 是否會出現類似于 JDK 1.7中那樣的死循環呢??這個有點不了解啊啊,求解答。。。。。。。

HashMap與Hashtable的差別

HashMap和Hashtable都實作了Map接口,但決定用哪一個之前先要弄清楚它們之間的分别。主要的差別有:線程安全性,同步(synchronization),以及速度。

第一、繼承不同

第一個不同主要是曆史原因。Hashtable是基于陳舊的Dictionary類的,HashMap是Java 1.2引進的Map接口的一個實作。但後來Hashtable也實作了map接口。

第二、線程安全不一樣

Hashtable 中的方法是同步的(利用synchronized關鍵字實作),而HashMap中的方法在預設情況下是非同步的。在多線程并發的環境下,可以直接使用Hashtable,但是要使用HashMap的話就要自己增加同步處理了。

第三、允不允許null值

從上面的put()方法源碼可以看到,Hashtable中,key和value都不允許出現null值,否則會抛出NullPointerException異常。

而在HashMap中,null可以作為鍵,這樣的鍵隻有一個;可以有一個或多個鍵所對應的值為null。當get()方法傳回null值時,即可以表示 HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。是以,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。

第四、周遊方式的内部實作上不同

Hashtable、HashMap都使用了 Iterator。而由于曆史原因,Hashtable還使用了Enumeration的方式 。

第五、哈希值的使用不同

HashTable直接使用對象的hashCode。而HashMap重新計算hash值。

//hashtable
        int hash = key.hashCode();
        int index = (hash & ) % tab.length;
           

第六、内部實作方式的數組的初始大小和擴容的方式不一樣

HashTable中的hash數組初始大小是11,增加的方式是 old*2+1。HashMap中hash數組的預設大小是16,而且一定是2的指數。

HashMap的周遊方法

HashMap有多種周遊方式,主要是依賴于疊代器(因為for-each也是利用疊代器實作的),這裡就不詳細介紹了,可以參看Java Map周遊方式的選擇 和 HashMap循環周遊方式及其性能對比 。

HashMap内容太多了,但搞懂還是有必要的,另外要想徹底搞定HashMap,ConcurrentHashMap更是并發學習的經典,在後邊并發包的源碼解析中再進行介紹。

繼續閱讀