厲害了!粉絲把 HashMap 剖析的只剩渣了!

你知道的越多,不知道的就越多,業餘的像一棵小草!

你來,咱們一塊兒精進!你不來,我和你的競爭對手一塊兒精進!
web

編輯:業餘草

推薦:https://www.xttblog.com/?p=5167


前言面試

HashMap是一個很是重要的集合,平常使用也很是的頻繁,同時也是面試重點。本文並不打算講解基礎的使用api,而是深刻HashMap的底層,講解關於HashMap的重點知識。須要讀者對散列表和HashMap有必定的認識。
HashMap本質上是一個散列表,那麼就離不開散列表的三大問題:散列函數、哈希衝突、擴容方案;同時做爲一個數據結構,必須考慮多線程併發訪問的問題,也就是線程安全。這四大重點則爲學習HashMap的重點,也是HashMap設計的重點。
HashMap屬於Map集合體系的一部分,同時繼承了Serializable接口能夠被序列化,繼承了Cloneable接口能夠被複制。他的的繼承結構以下:

HashMap並非全能的,對於一些特殊的情景下的需求官方拓展了一些其餘的類來知足,如線程安全的ConcurrentHashMap、記錄插入順序的LinkHashMap、給key排序的TreeMap等。
文章內容主要講解四大重點:散列函數、哈希衝突、擴容方案、線程安全,再補充關鍵的源碼分析和相關的問題。
本文全部內容如若未特殊說明,均爲JDK1.8版本。
哈希函數
哈希函數的目標是計算key在數組中的下標。判斷一個哈希函數的標準是:散列是否均勻、計算是否簡單。
HashMap哈希函數的步驟:
  1. 對key對象的hashcode進行擾動
  2. 經過取模求得數組下標
擾動是爲了讓hashcode的隨機性更高,第二步取模就不會讓因此的key都彙集在一塊兒,提升散列均勻度。擾動能夠看到hash()方法:

static final int hash(Object key) {
    int h;
    // 獲取到key的hashcode,在高低位異或運算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
算法

也就是低16位是和高16位進行異或,高16位保持不變。通常的數組長度都會比較短,取模運算中只有低位參與散列;高位與低位進行異或,讓高位也得以參與散列運算,使得散列更加均勻。具體運算以下圖(圖中爲了方便採用8位進行演示,32位同理):

對hashcode擾動以後須要對結果進行取模。HashMap在jdk1.8並非簡單使用%進行取模,而是採用了另一種更加高性能的方法。HashMap控制數組長度爲2的整數次冪,好處是對hashcode進行求餘運算和讓hashcode與數組長度-1進行位與運算是相同的效果。以下圖:

但位與運算的效率卻比求餘高得多,從而提高了性能。在擴容運算中也利用到了此特性,後面會講。取模運算的源碼看到putVal()方法,該方法在put()方法中被調用:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    ...
    // 與數組長度-1進行位與運算,獲得下標
    if ((p = tab[i = (n - 1) & hash]) == null)
        ...
}
api

完整的hash計算過程能夠參考下圖:

上面咱們提到HashMap的數組長度爲2的整數次冪,那麼HashMap是如何控制數組的長度爲2的整數次冪的?修改數組長度有兩種狀況:
  1. 初始化時指定的長度
  2. 擴容時的長度增量
先看第一種狀況。默認狀況下,如未在HashMap構造器中指定長度,則初始長度爲16。16是一個較爲合適的經驗值,他是2的整數次冪,同時過小會頻繁觸發擴容、太大會浪費空間。若是指定一個非2的整數次冪,會自動轉化成大於該指定數的最小2的整數次冪。如指定6則轉化爲8,指定11則轉化爲16。結合源碼來分析,當咱們初始化指定一個非2的整數次冪長度時,HashMap會調用tableSizeFor()方法:

public HashMap(int initialCapacity, float loadFactor) {
    ...
    this.loadFactor = loadFactor;
    // 這裏調用了tableSizeFor方法
    this.threshold = tableSizeFor(initialCapacity);
}

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;
}
數組

tableSizeFor()方法的看起來很複雜,做用是使得最高位1後續的全部位都變爲1,最後再+1則獲得恰好大於initialCapacity的最小2的整數次冪數。以下圖(這裏使用了8位進行模擬,32位也是同理):

那爲何必需要對cap進行-1以後再進行運算呢?若是指定的數恰好是2的整數次冪,若是沒有-1結果會變成比他大兩倍的數,以下:

00100 --高位1以後全變1--> 00111 --加1---> 01000
安全

第二種改變數組長度的狀況是擴容。HashMap每次擴容的大小都是原來的兩倍,控制了數組大小必定是2的整數次冪,相關源碼以下:

final Node<K,V>[] resize() {
    ...
    if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 設置爲原來的兩倍
            newThr = oldThr << 1;
    ...
}
微信

小結
  1. HashMap經過高16位與低16位進行異或運算來讓高位參與散列,提升散列效果;
  2. HashMap控制數組的長度爲2的整數次冪來簡化取模運算,提升性能;
  3. HashMap經過控制初始化的數組長度爲2的整數次冪、擴容爲原來的2倍來控制數組長度必定爲2的整數次冪。
哈希衝突解決方案 
再優秀的hash算法永遠沒法避免出現hash衝突。hash衝突指的是兩個不一樣的key通過hash計算以後獲得的數組下標是相同的。解決hash衝突的方式不少,如開放定址法、再哈希法、公共溢出表法、鏈地址法。HashMap採用的是鏈地址法,jdk1.8以後還增長了紅黑樹的優化,以下圖:

出現衝突後會在當前節點造成鏈表,而當鏈表過長以後,會自動轉化成紅黑樹提升查找效率。紅黑樹是一個查找效率很高的數據結構,時間複雜度爲O(logN),但紅黑樹只有在數據量較大時才能發揮它的優點。關於紅黑樹的轉化,HashMap作了如下限制。
  • 當鏈表的長度>=8且數組長度>=64時,會把鏈表轉化成紅黑樹。
  • 當鏈表長度>=8,但數組長度<64時,會優先進行擴容,而不是轉化成紅黑樹。
  • 當紅黑樹節點數<=6,自動轉化成鏈表。
那就有了如下問題:
爲何須要數組長度到64纔會轉化紅黑樹?
當數組長度較短時,如16,鏈表長度達到8已是佔用了最大限度的50%,意味着負載已經快要達到上限,此時若是轉化成紅黑樹,以後的擴容又會再一次把紅黑樹拆分平均到新的數組中,這樣非但沒有帶來性能的好處,反而會下降性能。因此在數組長度低於64時,優先進行擴容。
爲何要大於等於8轉化爲紅黑樹,而不是7或9?
樹節點的比普通節點更大,在鏈表較短時紅黑樹並未能明顯體現性能優點,反而會浪費空間,在鏈表較短是採用鏈表而不是紅黑樹。在理論數學計算中(裝載因子=0.75),鏈表的長度到達8的機率是百萬分之一;把7做爲分水嶺,大於7轉化爲紅黑樹,小於7轉化爲鏈表。紅黑樹的出現是爲了在某些極端的狀況下,抗住大量的hash衝突,正常狀況下使用鏈表是更加合適的。
注意,紅黑樹在jdk1.8以後出現的,jdk1.7採用的是數組+鏈表模式。
小結
  1. HashMap採用鏈地址法,當發生衝突時會轉化爲鏈表,當鏈表過長會轉化爲紅黑樹提升效率。
  2. HashMap對紅黑樹進行了限制,讓紅黑樹只有在極少數極端狀況下進行抗壓。
擴容方案 
當HashMap中的數據愈來愈多,那麼發生hash衝突的機率也就會愈來愈高,經過數組擴容能夠利用空間換時間,保持查找效率在常數時間複雜度。那何時進行擴容?由HashMap的一個關鍵參數控制:裝載因子。
裝載因子=HashMap中節點數/數組長度,他是一個比例值。當HashMap中節點數到達裝載因子這個比例時,就會觸發擴容;也就是說,裝載因子控制了當前數組可以承載的節點數的閾值。如數組長度是16,裝載因子是0.75,那麼可容納的節點數是16*0.75=12。裝載因子的數值大小須要仔細權衡。裝載因子越大,數組利用率越高,同時發生哈希衝突的機率也就越高;裝載因子越小,數組利用率下降,但發生哈希衝突的機率也下降了。因此裝載因子的大小須要權衡空間與時間之間的關係。在理論計算中,0.75是一個比較合適的數值,大於0.75哈希衝突的機率呈指數級別上升,而小於0.75衝突減小並不明顯。HashMap中的裝載因子的默認大小是0.75,沒有特殊要求的狀況下,不建議修改他的值。
那麼在到達閾值以後,HashMap是如何進行擴容的呢?HashMap會把數組長度擴展爲原來的兩倍,再把舊數組的數據遷移到新的數組,而HashMap針對遷移作了優化:使用HashMap數組長度是2的整數次冪的特色,以一種更高效率的方式完成數據遷移。
JDK1.7以前的數據遷移比較簡單,就是遍歷全部的節點,把全部的節點依次經過hash函數計算新的下標,再插入到新數組的鏈表中。這樣會有兩個缺點:一、每一個節點都須要進行一次求餘計算;二、插入到新的數組時候採用的是頭插入法,在多線程環境下會造成鏈表環。jdk1.8以後進行了優化,緣由在於他控制數組的長度始終是2的整數次冪,每次擴展數組都是原來的2倍,帶來的好處是key在新的數組的hash結果只有兩種:在原來的位置,或者在原來位置+原數組長度。具體爲何咱們能夠看下圖:

從圖中咱們能夠看到,在新數組中的hash結果,僅僅取決於高一位的數值。若是高一位是0,那麼計算結果就是在原位置,而若是是1,則加上原數組的長度便可。這樣咱們只須要判斷一個節點的高一位是1 or 0就能夠獲得他在新數組的位置,而不須要重複hash計算。HashMap把每一個鏈表拆分紅兩個鏈表,對應原位置或原位置+原數組長度,再分別插入到新的數組中,保留原來的節點順序,以下:

小結
  1. 裝載因子決定了HashMap擴容的閾值,須要權衡時間與空間,通常狀況下保持0.75不做改動;
  2. HashMap擴容機制結合了數組長度爲2的整數次冪的特色,以一種更高的效率完成數據遷移,同時避免頭插法形成鏈表環。
線程安全
HashMap做爲一個集合,主要功能則爲CRUD,也就是增刪查改數據,那麼就確定涉及到多線程併發訪問數據的狀況。併發產生的問題,須要咱們特別關注。
HashMap並非線程安全的,在多線程的狀況下沒法保證數據的一致性。舉個例子:HashMap下標2的位置爲null,線程A須要將節點X插入下標2的位置,在判斷是否爲null以後,線程被掛起;此時線程B把新的節點Y插入到下標2的位置;恢復線程A,節點X會直接插入到下標2,覆蓋節點Y,致使數據丟失,以下圖:
jdk1.7及之前擴容時採用的是頭插法,這種方式插入速度快,但在多線程環境下會形成鏈表環,而鏈表環會在下一次插入時找不到鏈表尾而發生死循環。
那若是結果數據一致性問題呢?解決這個問題有三個方案:
  • 採用Hashtable
  • 調用Collections.synchronizeMap()方法來讓HashMap具備多線程能力
  • 採用ConcurrentHashMap
前兩個方案的思路是類似的,均是每一個方法中,對整個對象進行上鎖。Hashtable是老一代的集合框架,不少的設計均以及落後,他在每個方法中均加上了synchronize關鍵字保證線程安全。

// Hashtable
public synchronized V get(Object key) {...}
public synchronized V put(K key, V value) {...}
public synchronized V remove(Object key) {...}
public synchronized V replace(K key, V value) {...}
...
數據結構

第二種方法是返回一個SynchronizedMap對象,這個對象默認每一個方法會鎖住整個對象。以下源碼:
img
這裏的mutex是什麼呢?直接看到構造器:

final Object      mutex;        // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
    this.m = Objects.requireNonNull(m);
    // 默認爲本對象
    mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
    this.m = m;
    this.mutex = mutex;
}
多線程

能夠看到默認鎖的就是自己,效果和Hashtable實際上是同樣的。這種簡單粗暴鎖整個對象的方式形成的後果是:
  • 鎖是很是重量級的,會嚴重影響性能。
  • 同一時間只能有一個線程進行讀寫,限制了併發效率。
ConcurrentHashMap的設計就是爲了解決此問題。他經過下降鎖粒度+CAS的方式來提升效率。簡單來講,ConcurrentHashMap鎖的並非整個對象,而是一個數組的一個節點,那麼其餘線程訪問數組其餘節點是不會互相影響,極大提升了併發效率;同時ConcurrentHashMap讀操做並不須要獲取鎖,以下圖:

關於ConcurrentHashMap和Hashtable的更多內容,限於篇幅,我會在另外一篇文章講解。
那麼,使用了上述的三種解決方案是否是絕對線程安全?先觀察下面的代碼:

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("abc","123");

Thread1:
if (map.containsKey("abc")){
    String s = map.get("abc");
}

Thread2:
map.remove("abc");
併發

當Thread1調用containsKey以後釋放鎖,Thread2得到鎖並把「abc」移除再釋放鎖,這個時候Thread1讀取到的s就是一個null了,也就出現了問題了。因此ConcurrentHashMap類或者Collections.synchronizeMap()方法或者Hashtable都只能在必定的限度上保證線程安全,而沒法保證絕對線程安全。
關於線程安全,還有一個fast-fail問題,即快速失敗。當使用HashMap的迭代器遍歷HashMap時,若是此時HashMap發生告終構性改變,如插入新數據、移除數據、擴容等,那麼Iteractor會拋出fast-fail異常,防止出現併發異常,在必定限度上保證了線程安全。以下源碼:

final Node<K,V> nextNode() {
    ...
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
   ...
}

建立Iteractor對象時會記錄HashMap的modCount變量,每當HashMap發生結構性改變時,modCount會加1。在迭代時判斷HashMap的modCount和本身保存的expectedModCount是否一致便可判斷是否發生告終構性改變。
fast-fail異常只能當作遍歷時的一種安全保證,而不能當作多線程併發訪問HashMap的手段。如有併發需求,仍是須要使用上述的三種方法。
小結
  1. HashMap並不能保證線程安全,在多線程併發訪問下會出現意想不到的問題,如數據丟失等
  2. HashMap1.8採用尾插法進行擴容,防止出現鏈表環致使的死循環問題
  3. 解決併發問題的的方案有Hashtable、Collections.synchronizeMap()、ConcurrentHashMap。其中最佳解決方案是ConcurrentHashMap
  4. 上述解決方案並不能徹底保證線程安全
  5. 快速失敗是HashMap迭代機制中的一種併發安全保證
源碼解析

關鍵變量的理解

HashMap源碼中有不少的內部變量,這些變量會在下面源碼分析中常常出現,首先須要理解這些變量的意義。

// 存放數據的數組
transient Node<K,V>[] table;
// 存儲的鍵值對數目
transient int size;
// HashMap結構修改的次數,主要用於判斷fast-fail
transient int modCount;
// 最大限度存儲鍵值對的數目(threshodl=table.length*loadFactor),也稱爲閾值
int threshold;
// 裝載因子,表示可最大容納數據數量的比例
final float loadFactor;
// 靜態內部類,HashMap存儲的節點類型;可存儲鍵值對,自己是個鏈表結構。
static class Node<K,V> implements Map.Entry<K,V> {...}

擴容

HashMap源碼中把初始化操做也放到了擴容方法中,於是擴容方法源碼主要分爲兩部分:肯定新的數組大小、遷移數據。詳細的源碼分析以下,我打了很是詳細的註釋,可選擇查看。擴容的步驟在上述已經講過了,讀者能夠自行結合源碼,分析HashMap是如何實現上述的設計。

final Node<K,V>[] resize() {
    // 變量分別是原數組、原數組大小、原閾值;新數組大小、新閾值
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 若是原數組長度大於0
    if (oldCap > 0) {
        // 若是已經超過了設置的最大長度(1<<30,也就是最大整型正數)
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 直接把閾值設置爲最大正數
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 設置爲原來的兩倍
            newThr = oldThr << 1; 
    }

    // 原數組長度爲0,但最大限度不是0,把長度設置爲閾值
    // 對應的狀況就是新建HashMap的時候指定了數組長度
    else if (oldThr > 0) 
        newCap = oldThr;
    // 第一次初始化,默認16和0.75
    // 對應使用默認構造器新建HashMap對象
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 若是原數組長度小於16或者翻倍以後超過了最大限制長度,則從新計算閾值
    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);
                // 新的位置只有兩種可能:原位置,原位置+老數組長度
                // 把原鏈表拆成兩個鏈表,而後再分別插入到新數組的兩個位置上
                // 不用屢次調用put方法
                else { 
                    // 分別是原位置不變的鏈表和原位置+原數組長度位置的鏈表
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍歷老鏈表,判斷新增斷定位是1or0進行分類
                    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;
}

添加數值

調用put()方法添加鍵值對,最終會調用putVal()來真正實現添加邏輯。代碼解析以下:

public V put(K key, V value) {
    // 獲取hash值,再調用putVal方法插入數據
    return putVal(hash(key), key, value, falsetrue);
}

// onlyIfAbsent表示是否覆蓋舊值,true表示不覆蓋,false表示覆蓋,默認爲false
// evict和LinkHashMap的回調方法有關,不在本文討論範圍
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {

    // tab是HashMap內部數組,n是數組的長度,i是要插入的下標,p是該下標對應的節點
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 判斷數組是不是null或者是不是空,如果,則調用resize()方法進行擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // 使用位與運算代替取模獲得下標
    // 判斷當前下標是不是null,如果則建立節點直接插入,若不是,進入下面else邏輯
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {

        // e表示和當前key相同的節點,若不存在該節點則爲null
        // k是當前數組下標節點的key
        Node<K,V> e; K k;

        // 判斷當前節點與要插入的key是否相同,是則表示找到了已經存在的key
        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);
                    // 長度大於等於8時轉化爲紅黑樹
                    // 注意,treeifyBin方法中會進行數組長度判斷,
                    // 若小於64,則優先進行數組擴容而不是轉化爲樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到相同的直接跳出循環
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }

        // 若是找到相同的key節點,則判斷onlyIfAbsent和舊值是否爲null
        // 執行更新或者不操做,最後返回舊值
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }

    // 若是不是更新舊值,說明HashMap中鍵值對數量發生變化
    // modCount數值+1表示結構改變
    ++modCount;
    // 判斷長度是否達到最大限度,若是是則進行擴容
    if (++size > threshold)
        resize();
    // 最後返回null(afterNodeInsertion是LinkHashMap的回調)
    afterNodeInsertion(evict);
    return null;
}

代碼中關於每一個步驟有了詳細的解釋,這裏來總結一下:
  1. 整體上分爲兩種狀況:找到相同的key和找不到相同的key。找了須要判斷是否更新並返回舊value,沒找到須要插入新的Node、更新節點數並判斷是否須要擴容。
  2. 查找分爲三種狀況:數組、鏈表、紅黑樹。數組下標i位置不爲空且不等於key,那麼就須要判斷是否樹節點仍是鏈表節點並進行查找。
  3. 鏈表到達必定長度後須要擴展爲紅黑樹,當且僅當鏈表長度>=8且數組長度>=64。
最後畫一張圖整體再加深一下整個流程的印象:

其餘問題

爲何jdk1.7之前控制數組的長度爲素數,而jdk1.8以後卻採用的是2的整數次冪?

答:素數長度能夠有效減小哈希衝突;JDK1.8以後採用2的整數次冪是爲了提升求餘和擴容的效率,同時結合高低位異或的方法使得哈希散列更加均勻。
爲什麼素數能夠減小哈希衝突?若能保證key的hashcode在每一個數字之間都是均勻分佈,那麼不管是素數仍是合數都是相同的效果。例如hashcode在1~20均勻分佈,那麼不管長度是合數4,仍是素數5,分佈都是均勻的。而若是hashcode之間的間隔都是2,如1,3,5...,那麼長度爲4的數組,位置2和位置4兩個下標沒法放入數據,而長度爲5的數組則沒有這個問題。長度爲合數的數組會使間隔爲其因子的hashcode彙集出現,從而使得散列效果下降。

爲何插入HashMap的數據須要實現hashcode和equals方法?對這兩個方法有什麼要求?

答:經過hashcode來肯定插入下標,經過equals比較來尋找數據;兩個相等的key的hashcode必須相等,但擁有相同的hashcode的對象不必定相等。
這裏須要區分好他們之間的區別:hashcode就像一我的的名,相同的人名字確定相等,可是相同的名字不必定是同我的;equals比較內容是否相同,通常由對象覆蓋重寫,默認狀況下比較的是引用地址;「==」引用隊形比較的是引用地址是否相同,值對象比較的是值是否相同。
HashMap中須要使用hashcode來獲取key的下標,若是兩個相同的對象的hashcode不一樣,那麼會形成HashMap中存在相同的key;因此equals返回相同的key他們的hashcode必定要相同。HashMap比較兩個元素是否相同採用了三種比較方法結合:p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))) 。
最後
關於HashMap的內容很難在一篇文章講完,他的設計到的內容很是多,如線程安全的設計能夠延伸到ConcurrentHashMap與Hashtable,這兩個類與HashMap的區別以及內部設計均很是重要,這些內容我將在另外的文章作補充。
最後,但願文章對你有幫助。

本文分享自微信公衆號 - 業餘草(yyucao)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索