天天看點

hashmap底層原理_Java集合 - HashMap原理(一) 概念和底層架構1. table變量2. entrySet變量3. capacity4. size變量5. threshold變量和loadFactor變量總結

HashMap在Java開發中使用的非常頻繁,可以說僅次于String,可以和ArrayList并駕齊驅,準備用幾個章節來梳理一下HashMap。我們還是從定義一個HashMap開始。

HashMap mapData = new HashMap<>();
           

我們從此處進入源碼,逐漸揭露HashMap

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

我們發現了兩個變量loadFactor和DEFAULT_LOAD_FACTOR,從命名方式來看:因為沒有接收到loadFactor參數,進而将某個預設值指派給了loadFactor。這兩變量到底是什麼意思,還有無其他變量?

其實HashMap中定義的靜态變量和成員變量很多,我們看一下

//靜态變量static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final int MAXIMUM_CAPACITY = 1 << 30;static final float DEFAULT_LOAD_FACTOR = 0.75f;static final int TREEIFY_THRESHOLD = 8;static final int UNTREEIFY_THRESHOLD = 6;static final int MIN_TREEIFY_CAPACITY = 64;
           
//成員變量transient Node[] table;transient Set> entrySet;transient int size;transient int modCount;int threshold;final float loadFactor;
           

共有6個靜态變量,都設定了初始值,且被final修飾,叫常量更合适,它們的作用其實也能猜出來,就是用于成員變量的預設值設定以及方法中相關的條件判斷等情況。

共有6個成員變量,除這些成員變量外,還有一個重要概念capacity,我們主要說一下table,entrySet,capacity, size,threshold,loadFactor,我們我們簡單解釋一下它們的作用。

1. table變量

table變量為HashMap的底層資料結構,用于存儲添加到HashMap中的Key-value對,是一個Node數組,Node是一個靜态内部類,一種數組和連結清單相結合的複合結構,我們看一下Node類:

static class Node implements Map.Entry {    final int hash;    final K key;    V value;    Node next;    Node(int hash, K key, V value, Node 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;    }}
           

若以乘機做比喻的話,那麼你買票的身份證号就是(key),通過hash算法生成的(hash)值就相當于值機後得到的航班座位号;你自己自然就是(value),你旁邊的座位、人就是下一個Node(next);這樣的一個座位整體(包括所坐人員及其身份證号、座位号)就是一個table,這許多的table的建構的Node[] table,就構成了本次航班任務。

那麼為什麼要用到數組和連結清單結合的資料結構?

我們知道數組和連結清單都有其各自的優點和缺點,數組連續存儲,尋址容易,插入删除操作相對困難;而連結清單離散存儲,尋址相對困難,而插入删除操作容易;而HashMap結合了這兩種資料結構,保留了各自的優點,又彌補了各自的缺點,當然連結清單長度太長的話,在JDK8中會轉化為紅黑樹,紅黑樹在後面的TreeMap章節在講解。

HashMap的結構圖如下:

hashmap底層原理_Java集合 - HashMap原理(一) 概念和底層架構1. table變量2. entrySet變量3. capacity4. size變量5. threshold變量和loadFactor變量總結

怎麼解釋這種結構呢?

還是以乘機為例來說明,假如購票系統比較人性化并取消了值機操作,購票按照年齡段進行了區分,友善大家旅途溝通交流,于是20歲以下共6個人的分為了一組在20A20F,2030歲共6個人分為一組在21A21F,3040歲共6個人分為一組在22A22F,4050歲共6個人分為一組在23A~23F。

這時我們如果要找20幾歲的小姐姐,我們很容易知道去21排找,從21A開始往下找,應該就能很快找到。

從資料的角度看,按年齡段分組(通過hash算法得到hash值,不同年齡段hash值不同,相同年齡段hash值相同)後,将各年齡段中第一個坐到座位上的人放到數組table中,下一個人來的時候,将第一個人往裡面挪,自己在數組裡,并将next指向第一個人。

2. entrySet變量

entrySet變量為EntrySet實體,定義為變量可保證不重複多次建立,是一個Map.Entry的集合,Map.Entry是一個接口,Node類就實作了該接口,是以EntrySet中方法需要操作的資料就是HashMap的Node實體。

public Set> entrySet() {    Set> es;    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;}
           

3. capacity

capacity并不是一個成員變量,但HashMap中很多地方都會使用到這個概念,意思是容量,很好了解,在前面的文中提到了兩個常量都與之相關

/** * The default initial capacity - MUST be a power of two(必須為2的幂次). * 預設容量16,舉例:飛機上正常的座位所對應的人員數量, */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(必須為2的幂次,且不能大于最大容量1,073,741,824). * 舉例:緊急情況下,如救災時盡可能快撤離人員,這個時候在保證安全的情況下(允許站立),能運輸的人員數 */static final int MAXIMUM_CAPACITY = 1 << 30;
           

同時HashMap還具有擴容機制,容量的規則為2的幂次,即capacity可以是1,2,4,8,16,32...,怎麼實作這種容量規則呢?

/** * 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;}
           

用該方法即可找到傳遞進來的容量的最近的2的幂次,即

cap = 2, return 2;

cap = 3, return 4;

cap = 9, return 16;

...

大家可以傳遞值進去自己算一下,先cap-1操作,是因為當傳遞的cap本身就是2的幂次情況下,假如為4,不減去一最後得到的結果将是傳遞的cap的2倍。

我們來一行行計算一下:tableSizeFor(11),按規則最後得到的結果應該是16

//第一步:n = 10,轉為二進制為00001010int n = cap - 1;//第二步:n右移1位,高位補0(10進制:5,二進制:00000101),并與n做或運算(有1為1,同0為0),然後指派給n(10進制:15,二進制:00001111)n |= n >>> 1;//第三步:n右移2位,高位補0(10進制:3,二進制:00000011),并與n做或運算(有1為1,同0為0),然後指派給n(10進制:15,二進制:00001111)n |= n >>> 2;//第四步:n右移4位,高位補0(10進制:0,二進制:00000000),并與n做或運算(有1為1,同0為0),然後指派給n(10進制:15,二進制:00001111)n |= n >>> 4;//第五步:n右移8位,高位補0(10進制:0,二進制:00000000),并與n做或運算(有1為1,同0為0),然後指派給n(10進制:15,二進制:00001111)n |= n >>> 8;//第六步:n右移16位,高位補0(10進制:0,二進制:00000000),并與n做或運算(有1為1,同0為0),然後指派給n(10進制:15,二進制:00001111)n |= n >>> 16;//第七步:return 15+1 = 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
           

最終的結果正如預期,算法很牛逼啊,ヽ(ー_ー)ノ,能看懂,但卻設計不出來。

4. size變量

size變量記錄了Map中的key-value對的數量,在調用putValue()方法以及removeNode()方法時,都會對其造成改變,和capacity區分一下即可。

5. threshold變量和loadFactor變量

threshold為臨界值,顧名思義,當過了臨界值就需要做一些操作了,在HashMap中臨界值“threshold = capacity * loadFactor”,當超過臨界值時,HashMap就該擴容了。

loadFactor為裝載因子,就是用來衡量HashMap滿的程度,預設值為DEFAULT_LOAD_FACTOR,即0.75f,可通過構造器傳遞參數調整(0.75f已經很合理了,基本沒人會去調整它),很好了解,舉個例子:

100分的試題,父母隻需要你考75分,就給你買一台你喜歡的電腦,裝載因子就是0.75,75分就是臨界值;如果幾年後,試題的分數變成200分了,這個時候就需要你考到150分才能得到你喜歡的電腦了。

總結

本文主要講解了HashMap中的一些主要概念,同時對其底層資料結構從源碼的角度進行了分析,table是一個資料和連結清單的複合結構,size記錄了key-value對的數量,capacity為HashMap的容量,其容量規則為2的幂次,loadFactor為裝載是以,衡量滿的程度,而threshold為臨界值,當超出臨界值時就會擴容。

----------------------------------------------------

轉載自:https://www.cnblogs.com/LiaHon/p/11142958.html

繼續閱讀