HashMap在Java開發中使用的非常頻繁,可以說僅次於String,可以和ArrayList並駕齊驅,準備用幾個章節來梳理一下HashMap。我們還是從定義一個HashMap開始。
HashMap<String, Integer> mapData = new HashMap<>();
我們從此處進入原始碼,逐步揭露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
}
我們發現了兩個變數loadFactor和DEFAULT_LOAD_FACTOR,從命名方式來看:因為沒有接收到loadFactor引數,從而將某個預設值賦值給了loadFactor。這兩變數到底是什麼意思,還有無其他變數?
其實HashMap中定義的靜態變數和成員變數很多,我們看一下
//靜態變數
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static 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<K,V>[] table;
transient Set<Map.Entry<K,V>> 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<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 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的結構圖如下:
怎麼解釋這種結構呢?
還是以乘機為例來說明,假如購票系統比較人性化並取消了值機操作,購票按照年齡段進行了區分,方便大家旅途溝通交流,於是20歲以下共6個人的分為了一組在20A~20F,20~30歲共6個人分為一組在21A~21F,30~40歲共6個人分為一組在22A~22F,40~50歲共6個人分為一組在23A~23F。
這時我們如果要找20幾歲的小姐姐,我們很容易知道去21排找,從21A開始往下找,應該就能很快找到。
從資料的角度看,按年齡段分組(通過hash演算法得到hash值,不同年齡段hash值不同,相同年齡段hash值相同)後,將各年齡段中第一個坐到座位上的人放到陣列table中,下一個人來的時候,將第一個人往裡面挪,自己在陣列裡,並將next指向第一個人。
2. entrySet變數
entrySet變數為EntrySet實體,定義為變數可保證不重複多次建立,是一個Map.Entry的集合,Map.Entry<K,V>是一個介面,Node類就實現了該介面,因此EntrySet中方法需要操作的資料就是HashMap的Node實體。
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> 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,轉為二進位制為00001010
int 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為臨界值,當超出臨界值時就會擴容。