HashMap的put操作源碼解析 [toc] 1、官方文檔 1.1、繼承結構 1.2、類型參數: 2、put(key, value) HashMap是一種以鍵——值對的形式來存儲數據的數據結構。HashMap允許使用 null 值和 null 鍵,它並不能保證你存放數據和取出的順序是一致的。 接下 ...
目錄
HashMap的put操作源碼解析
1、官方文檔
1.1、繼承結構
java.lang.Object
java.util.AbstractMap<K,V>
java.util.HashMap<K,V>
1.2、類型參數:
K - 此映射所維護的鍵的類型
V - 所映射值的類型
2、put(key, value)
HashMap
是一種以鍵——值
對的形式來存儲數據的數據結構。HashMap
允許使用 null 值和 null 鍵,它並不能保證你存放數據和取出的順序是一致的。
接下來就以下麵的代碼來看一下put
是怎麼將數據存放到map
中的。
public class HashMapTest {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(null, "map-value");
map.put(map-key", "map-value");
System.out.println(map);
}
}
2.1、重點源碼部分截取
在map.put()
這裡打個斷點F5(我用的eclipse)
跟蹤進去。我們就會進到put
方法中:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
這裡的EMPTY_TABLE
是HashMap
的一個靜態常量,是一個Entry數組,預設值是空數組,table
是HashMap
的一個屬性且其預設值就是EMPTY_TABLE
,這個table也就是我們數據存放的地方,至此為止可以知道,HashMap其實是一個數組,但它又不是一個純粹的數組。下麵會進行解釋。
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
而這個Entry
其實是HashMap的一個內部類,定義如下(僅截取部分代碼),記住這個類,記住這個構造方法:它在new Entry
的時候接收了一個Entry
對象,並將自己的next
指向了傳入的Entry
對象形成一個鏈表,其自身是表頭。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
從上面我們可以看出來這個Entry
其實是一個鏈表,它存放了 key 和 value 並且還有一個指向下一個節點的引用 Entry
, 剩下的這個 hash
就是 key 的哈希值。
現在我們可以捋一捋HashMap的結構了。首先HashMap
是一個Entry
數組,而這個Entry
是一個單向鏈表,我們大致可以將其結構畫成如下圖所示:
2.2、put(key, value)源碼分析
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
- 因為我們在實例化HashMap的時候使用的是無參構造方法,所以第一次
put
數據的時候table
為空
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
上面這段代碼會被執行,inflateTable(threshold)
會將table
初始化為一個長度為16
的Entry
數組。
- 它會對我們的
key
進行空判斷,如果是空就會執行下麵的代碼:
if (key == null)
return putForNullKey(value);
putForNullKey(value)
的實現如下:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
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;
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++;
}
2.2.1 、key
為null
的情況
從上面可以看出來,如果key
為null
的話,它會從table
中取出下標為0也就是第一個元素,沒忘記的話我們應該還知道它一個Entry
,是一個鏈表,如果這個元素不是null
,那麼就會遍歷這個鏈表,並判斷當前這個Entry
節點對象的key
是不是null
。
- 如果是
null
(key
相同了): 使用oldValue
來存放當前這個Entry
節點對象的value
,然後將我們新的值(map-value
)賦給當前節點,再將原值oldValue
返回回去。 - 如果遍歷完鏈表的所有節點都沒有找到
key
為null
的節點就會調用addEntry(0, null, value, 0)
,這個方法前面的if(){***}
這塊代碼是判斷當前table是否要進行擴容。這裡只做簡單講述。
size
是當前table
存放的Entry鏈表的個數,拿我上面畫的那個HapshMap
結構來看就是4。
如果我們實例化HashMap的時候沒有給大小那麼:threshold
=loadFactor
(負載因數預設為0.75f
) *DEFAULT_INITIAL_CAPACITY
(HashMap預設大小也就是table長度為16
),所以threshold
=12
。
如果我們給了大小為initialCapacity
,那麼負載因數還是預設的0.75f
,但是threshold不需要算了,值就是initialCapacity
。如果我們同時給了HashMap
的大小initialCapacity
和負載因數loadFactor
,那麼HashMap
就使用我們給定的負載因數值作為新的負載因數,給定的HashMap
大小作為threshold
。ok第一個條件結束。
null != table[bucketIndex]
就很好理解了,就是我當前這個節點要存放的位置是空的。
滿足上面兩個條件,HashMap
就會進行擴容,擴容後的大小為擴容前的2倍,然後對key
重新計算它的hash
值以及數組下標。 - 繼續
put
內容,從上面源碼我們可以知道key
為null
的情況下它的hash
值是0,至於bucketIndex
的計算是這樣的h & (length-1)
,也是將hash
值與table
的長度按位相與值也是。至此也就是確定了key
為null
的這個節點將存放在table的第一個位置上。然後就會調用createEntry(0, null, "map-value", 0);
- 在
createEntry(int hash, K key, V value, int bucketIndex)
這個方法里首先拿到table
中下標為bucketIndex
的鏈表的表頭:Entry<K,V> e = table[bucketIndex];
然後再用Entry
對象的構造方法new
一個Entry
將我們的hash
值,key
,value
,鏈表的表頭作為參數傳入:table[bucketIndex] = new Entry<>(hash, key, value, e);
就這樣我們的這個新節點就放在了原來的表頭的前面作為新的表頭了。沒看懂的再回到上面看一下Entry
的構造方法,我有重點標註的。
2.2.2、 key
不為null
的情況
源碼依舊拿下來
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
- 首先說一下
hash
值:對於相同的key
它們的hash
值是相同的。但是hash
值相同,它們的key
卻不一定是相同的,這就是哈希碰撞。 key
不為null
的話它會根據key
算出這個key
對就的hash
值以及它的bucketIndex
,然後拿到table
中下標為bucketIndex
的這個Entry
鏈表,然後遍歷這個鏈表,判斷當前節點的hash
其實也就是當前節點的key
的hash
是否等於我們傳入的map-key
的hash
,然後判斷當前節點的key
是否與我們傳入的key
相同。
如果以上條件都滿足了,那麼就是key
相同了,就會跟Key
為null
的分析中的第一條一樣將新值覆蓋舊值,並將舊值返回回去。
如果遍歷完這個鏈表以上條件沒有得到滿足,那麼就會跟key
為null
的分析中的第四條一樣,獲得table
下標為i
的鏈表的表頭e
,然後將我們的map-key
,map-value
,hash
以及表頭e
作為參數new
一個新的Entry
對象並將它的next
指向原來的表頭e
,它也就變成了新的表頭了。
3、完結
最怕你的能力配不上你的野心。