【JDK原始碼】HashMap原始碼分析
阿新 • • 發佈:2021-01-05
Map 這樣的 Key Value 在軟體開發中是非常經典的結構,常用於在記憶體中存放資料。
總結
JDK1.7
- 陣列加連結串列,"拉鍊法"解決hash衝突
- 底層陣列長度總是為2的冪次方。這是因為在此條件下hash & (length - 1) == hash % length,而且&比%的效率更高,(hash % length總是小於length的,因此可以用來計算元素在桶中的位置)
- 預設長度16,會動態增長為32,64,128…,就算初始化的時候指定為11,其實底層的陣列長度還是16
- 負載因子預設是0.75,是可以修改的,擴容閾值=陣列長度*負載因子
- 假設陣列長度為16,則擴容閾值為16*0.75=12,當實際所放元素大於12時,則觸發擴容操作
- 自動擴容,非常消耗效能
- 當hash嚴重衝突時,連結串列會越來越長嚴重影響效率,時間複雜度最長為O(N)
- 執行緒不安全(需要執行緒安全的請使用ConcurrentHashMap),多執行緒會引發連結串列死迴圈
JDK1.8後的優化
- 當連結串列長度超過8的時候則直接轉換成紅黑樹,查詢效率為O(logN)
- 擴容時會均勻分配元素,而JDK1.7會原封不動的拷貝過來
- 多執行緒會引發連結串列死迴圈的問題已解決
測試
//指定初始容量為11,但是底層陣列的長度還是會初始化為16,具體看tableSizeFor方法
Map<Integer,String> map = new HashMap(11);
//初始化map
map = new HashMap<>();//初始化容量為16的HashMap
map.put(1,"A");//索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map
//元素放在不同的桶中
map = new HashMap<>();//初始化容量為16的HashMap
map.put(1,"A");//索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map
map.put(2,"B");//索引位置 2 % 16 = 2
//hash碰撞,產生連結串列
map = new HashMap<> ();//初始化容量為16的HashMap
map.put(1,"A");//索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map
map.put(17,"B");//索引位置 17 % 16 = 1,索引相同,hash碰撞,產生連結串列
//hash碰撞,產生紅黑樹
map = new HashMap<>();//初始化容量為16的HashMap
map.put(1,"A"); //索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map
map.put(17,"B"); //索引位置 17 % 16 = 1,hash碰撞,連結串列長度2
map.put(33,"D"); //索引位置 33 % 16 = 1,hash碰撞,連結串列長度3
map.put(49,"E"); //索引位置 49 % 16 = 1,hash碰撞,連結串列長度4
map.put(65,"F"); //索引位置 65 % 16 = 1,hash碰撞,連結串列長度5
map.put(81,"G"); //索引位置 81 % 16 = 1,hash碰撞,連結串列長度6
map.put(97,"H"); //索引位置 97 % 16 = 1,hash碰撞,連結串列長度7
map.put(113,"I"); //索引位置 113 % 16 = 1,hash碰撞,連結串列長度8
map.put(129,"J"); //索引位置 129 % 16 = 1,hash碰撞,連結串列長度9,此時產生紅黑樹,呼叫treeifyBin(tab, hash)
JDK1.8
成員變數
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- DEFAULT_INITIAL_CAPACITY表示初始化容量大小為2^4 = 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.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
- 最大容量為2^30
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- 負載因子預設為0.75
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
- 當連結串列長度大於8的時候,連結串列將轉換成紅黑樹
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
- table是真正存放資料的陣列
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
- size表示當前map實際存放元素數量的大小
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
- 結構化修改次數的大小
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
int threshold;
- 需要擴容的閾值,當size和threshold相等時會觸發擴容操作
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
- 負載因子,預設為DEFAULT_LOAD_FACTOR,可構造map的時候傳入
容量capacity©,負載因子loadFactor(L),擴容閾值threshold(T)和實際存放元素大小size(S)的關係
- 注意capacity變數不是成員變數,而是實際存放資料陣列的長度,可以理解成table.length
- T = C * L
- 當S>T時會觸發擴容操作,此時C會變成原來的2倍(當陣列長度C 是2的 n 次時, hash&(length-1) == hash%length,因為&操作比%操作效率更搞,所以陣列長度C總是2的n次方,目的是為了提升效率。)
舉例:
預設大小C為16,此時T=16 * 0.75 = 12,當S=13時,觸發擴容,C=C2=162=32,T=32 * 0.75=24
put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定義tab p n i
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一次往map裡面put時候會呼叫擴容resize方法初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//初始化table並且將長度賦值給n
//i = (n - 1) & hash 會得到當前元素放置在陣列中的位置,
//和hash % n的值相等(前提是table.length為2的冪次方),但是&的操作效率更高
//如果該位置上面沒有元素則直接新建一個元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//下面的操作在該元素位置上有值的時候進行操作
else {
Node<K,V> e; K k;
//如果當前桶有值( Hash 衝突),那麼就要比較當前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就賦值給 e
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);
//如果大於預設閾值,則轉換成紅黑數,接著退出迴圈
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在迴圈中找到了鍵相同的,則直接退出迴圈
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果找到了鍵相同的節點,則替換掉相應的值,並返回該值(不算結構化修改)
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//結構化修改加1
++modCount;
//如果實際所裝的元素大於了閾值,則觸發擴容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get方法
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;
//first = tab[(n - 1) & hash] 通過(n - 1) & hash定位到該鍵所在桶的索引
//如果桶為null則直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶的第一個匹配則直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果桶的第一個節點不匹配
if ((e = first.next) != null) {
//如果節點是紅黑樹,則按照紅黑樹的方式查詢
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是連結串列,則按照連結串列的方式迴圈找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//沒找到或各種意外情況下都返回null
return null;
}
JDK1.7
JDK1.7中的put和get方法就簡單很多,也沒有紅黑樹那些轉換
put
public V put(K key, V value) {
//沒有初始化的時候先初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//空key的時候
if (key == null)
return putForNullKey(value);
//計算hash
int hash = hash(key);
//計算出該hash在當前桶中的定位
int i = indexFor(hash, table.length);
//如果桶是一個連結串列,則迴圈連結串列
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;
}
}
//如果桶是空的或者沒在連結串列中找到一樣的鍵則新增加一個Entry
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果實際的容量達到了閾值,則需要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
//擴容為原來的兩倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//在計算出該key的索引,即在桶中的位置
bucketIndex = indexFor(hash, table.length);
}
//建立Entry
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//把當前位置上的元素拿出來
Entry<K,V> e = table[bucketIndex];
//新建Entry,如果e不是null的話,則形成連結串列結構
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
get
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//計算hashCode
int hash = (key == null) ? 0 : hash(key);
//indexFor(hash, table.length) 計算出在桶中的位置
//如果是連結串列則迴圈找到鍵相同的Entry
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;
}
//如果啥也沒找到,則直接返回null
return null;
}