文章目錄
-
- 1.HashMap簡述
- 2.HashMap資料結構
-
- 2.1.JDK1.7中HashMap資料結構
- 2.2.JDK1.8中HashMap資料結構
- 3.HashMap存取值分析
-
- 3.1. HashMap存值(put)分析【JDK1.7】
-
- 3.1.1.HashMap存值源碼走讀【JDK1.7】
- 3.1.2.HashMap數組初始化【JDK1.7】
- 3.1.3.HashMap元素存放位置計算【JDK1.7】
- 3.1.4.HashMap添加元素到連結清單【JDK1.7】
- 3.1.5.HashMap數組擴容【JDK1.7】
- 3.3.JDK1.7取值(get)分析
- 3.4.HashMap存值(put)分析【JDK1.8】
-
- 3.4.1.HashMap存值(put)源碼走讀【JDK1.8】
- 3.4.2.HashMap數組擴容【JDK1.8】
- 3.5.JDK1.8取值(get)分析
1.HashMap簡述
HashMap是工作中最常用的集合工具之一,在整個集合架構中也是很重要的一部分,是以本篇文章主要講述它的底層實作原理,因為jdk1.8中對HashMap的資料結構有了修改,是以本篇将會分别講解jdk1.7和jdk1.8中HashMap的差別,通過對比學習來加深對HashMap的了解
jdk1.8之前HashMap采用【數組+連結清單】實作,使用連結清單處理hash沖突,同一個hash值都存在一個連結清單裡。但是當存儲的元素較多時,hash值相等的元素也會增多,通過key值依次查找的效率就降低了許多。
jdk1.8中,HashMap采用【數組+連結清單+紅黑樹】實作,當連結清單長度超過8時,将連結清單轉換為紅黑樹,這樣就大大提高的查找的時間
2.HashMap資料結構
2.1.JDK1.7中HashMap資料結構
HashMap中的數組即為嵌套類Entry,數組中的每個元素是一個單項連結清單,每個Entry包含四個屬性:key, value, hash 值和用于單向連結清單的 next。
capacity:目前數組容量,始終保持 2^n,可以擴容,擴容後數組大小為目前的 2 倍。
loadFactor:負載因子,預設為 0.75。
threshold:擴容的門檻值,等于 capacity * loadFactor
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
.....................................
}
2.2.JDK1.8中HashMap資料結構
Java8 對 HashMap 做了一些修改,最大的不同就是利用了紅黑樹,是以其由 【數組+連結清單+紅黑樹】 組成。
我們知道,Java7 HashMap查找的時候,根據 hash 值我們能夠快速定位到數組的具體下标,但是之後,需要順着連結清單一個個比較下去才能找到我們需要的,時間複雜度取決于連結清單的長度,為 O(n)。
為了降低這部分的開銷,在 Java8 中,當連結清單中的元素超過了 8 個以後,會将連結清單轉換為紅黑樹,在進行查找的時候可以降低時間複雜度為 O(logN)。
Java7 中使用 Entry 來代表每個 HashMap 中的資料元素,Java8 中使用 Node,都是 key,value,hash 和 next 這四個屬性,不過,Node 隻能用于連結清單的情況,紅黑樹的情況需要使用 TreeNode
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;
}
...............................................
}
3.HashMap存取值分析
3.1. HashMap存值(put)分析【JDK1.7】
3.1.1.HashMap存值源碼走讀【JDK1.7】
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
public V put(K key, V value) {
//先判斷數組是否為空,添加第一個元素時,需要先初始化數組大小
if (table == EMPTY_TABLE) {
//數組初始化
inflateTable(threshold);
}
//如果key 為 null,會将這個 entry 放在數組的第一個元素位置 table[0]
if (key == null)
return putForNullKey(value);
//1)計算key的hash值
int hash = hash(key);
//2)根據 key 哈希值和數組長度計算存放位置的下标
int i = indexFor(hash, table.length);
//3)周遊對應下标處的連結清單,判斷是否有重複的 key ,如果有直接覆寫并傳回舊值
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//4)如果不存在重複的key,則将新的entry添加到連結清單中
addEntry(hash, key, value, i);
return null;
}
3.1.2.HashMap數組初始化【JDK1.7】
當第一個元素插入時,會初始化數組,計算數組的初始化大小及擴容門檻值
private void inflateTable(int toSize) {
// 確定數組大小是2的n次方
//比如toSize=3,計算結果為4;toSize=10,計算結果為16
int capacity = roundUpToPowerOf2(toSize);
//計算擴容門檻值 數組大小 * 負載因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化數組
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
3.1.3.HashMap元素存放位置計算【JDK1.7】
使用key的hash值與數組長度大小減一取模
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
3.1.4.HashMap添加元素到連結清單【JDK1.7】
先判斷key值是否重複,如果不重複添加新元素到連結清單,添加之前先判斷是否需要擴容,如果需要則先擴容,重新計算hash,然後再将新元素存入對應連結清單的表頭
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果 HashMap 大小已經達到了門檻值,并且新元素要插入的數組位置已經有元素,那麼此時要先擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容,數組大小的2倍
resize(2 * table.length);
// 擴容以後,重新計算 hash 值
hash = (null != key) ? hash(key) : 0;
// 重新計算擴容後元素存儲的新下标
bucketIndex = indexFor(hash, table.length);
}
// 添加元素到擴容後的連結清單
createEntry(hash, key, value, bucketIndex);
}
// 新值存放到連結清單的表頭
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
3.1.5.HashMap數組擴容【JDK1.7】
在插入新值的時候,如果目前的 size 已經達到了門檻值,并且要插入的數組位置已經有元素,就會觸發擴容,擴容後,數組大小為原來的 2 倍。
擴容就是用一個新的大數組替換原來的小數組,并将原來數組中的值遷移到新的數組中
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果原數組大小等于最大容量值,則設定容量門檻值為整形(Integer)最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//建立新數組
Entry[] newTable = new Entry[newCapacity];
//将原數組中的值遷移到擴容後的新數組中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
3.3.JDK1.7取值(get)分析
相對于存值(put),HashMap取值相對簡單,大緻邏輯為:
1)根據key計算hash
2)使用key的hash值與數組長度取模(length-1)得到元素存儲位置下标
3)周遊數組中在該下标處的連結清單,直到找到相等的key
public V get(Object key) {
//如果key為null,擷取對應值
if (key == null)
return getForNullKey();
//如果key不為null,計算hash值、計算位置下标、周遊連結清單
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//擷取key值為null的對應值
private V getForNullKey() {
if (size == 0) {
return null;
}
//周遊數組第一個位置的連結清單,找到key為null的值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//計算key的hash
int hash = (key == null) ? 0 : hash(key);
//計算存儲位置下标并周遊對應位置連結清單,直到找到key相等的值
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
3.4.HashMap存值(put)分析【JDK1.8】
3.4.1.HashMap存值(put)源碼走讀【JDK1.8】
相對于jdk1.7中HashMap存值,jdk1.8的邏輯相對複雜,因為需要判斷資料節點類型是連結清單還是紅黑樹,然後使用對應的方法進行查找
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// onlyIfAbsent 如果是 true,隻有在不存在該 key 時才會進行 put 操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 要初始化數組長度
//第一次 resize 和後續的擴容有些不同,
//首次擴容觸發條件是,數組為null或數組長度為0
//首次擴容使用數組初始化預設值,容量為16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//首先計算數組下标,然後判斷該位置是否為null,如果為null,執行個體化一個新node并存入該位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 數組該位置有資料
Node<K,V> e; K k;
// 判斷該位置的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) {
// 插入到連結清單的最後面(Java7 是插入到連結清單的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 為 8,是以,如果新插入的值是連結清單中的第 9 個
// 會觸發 treeifyBin,将連結清單轉換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在該連結清單中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 結束循環,e 為連結清單中與要插入的新值的 key 相等的 node
break;
p = e;
}
}
// 存在舊值的key與要插入的key相等,進行值覆寫
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由于新插入的值導緻 size 已經超過了門檻值,需要進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.4.2.HashMap數組擴容【JDK1.8】
resize() 方法用于初始化數組或數組擴容,初始化時使用預設容量(16)和預設門檻值(0.74*16),後來擴容時,容量為原來的 2 倍,并進行資料遷移
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;
}
// 将數組大小擴大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将門檻值擴大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
// 使用 new HashMap(int initialCapacity) 初始化後,首次插入值時
newCap = oldThr;
else {
// 首次插入值,數組初始化大小
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 建立新數組
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 如果是初始化數組,到這裡就結束了,傳回 newTab 即可
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);
else {
// 如果是連結清單,要将此連結清單一拆為二,放到新的數組中,并且保持先後順序
// loHead、loTail 對應一條連結清單,hiHead、hiTail 對應另一條連結清單
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
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;
// 第二條連結清單的新的位置是 j + oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3.5.JDK1.8取值(get)分析
取值邏輯如下:
1)計算 key 的 hash 值,根據 hash 值計算對應數組下标: hash & (length-1)
2)判斷數組該位置處的key是否相等,如果不等,走第(3)步,否則結束
3)判斷該元素類型是否是 TreeNode,如果是,用紅黑樹的方法取資料,如果不是,走第(4)步
4)周遊連結清單,直到找到相等(==或equals)的 key
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//計算元素存儲位置下标
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果key相等,傳回該node節點
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果節點node是紅黑樹,按照紅黑樹查找法查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是連結清單,周遊連結清單,直到找到key相等的節點
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}