jdk原始碼分析之HashMap
HashMap的底層資料結構
HashMap底層採用陣列加連結串列的資料結構儲存鍵值對
Hash根據key的雜湊值轉化為陣列的下標將鍵值對存入陣列中,陣列的元素是一個連結串列,衝突的key放置在陣列的同一個位置,使用連結串列將衝突的資料連結起來
陣列的底層結構如下:
/**
* An empty table instance to share when the table is not inflated.
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
可見陣列儲存的元素是Entry,而Entry表示連結串列的節點,節點儲存了key、value,key的hash值,指向下一個節點的指標域next
Entry資料hashCode()方法是通過key的hash值和value的hash值異或得到的
equals方法判斷兩個Entry相等的標準是key相等且value相等
重寫equals方法必須重寫hashCode方法,滿足兩個物件相等(equals返回true)則這兩個物件的hash值必須相等
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
根據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);
}
陣列的長度是2的整次冪,h & (length-1)等價於
h % length
但是求模運算通過除法實現,除法的的運算比較耗費CPU時間,因此巧妙地轉化為位運算來求模
根據key獲取entry
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(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;
}
首先判斷size是否為0,size為0表示HashMap中沒有鍵值對,此時通過key獲取value必然返回null
然後判斷key是否為null,null作為key,其hash值為0,null key在entry陣列中的位置為index=0處
key不為null的話通過indexFor(hash, table.length)計算桶的位置
根據桶的位置獲取連結串列頭,然後遍歷連結串列獲取所求的entry
Entry<K,V> e = table[indexFor(hash, table.length)];
此時的e表示連結串列的頭部,此連結串列的所有節點key的hash值相同(衝突),但是key不同,因此掃描連結串列,獲取對應key的entry,然後返回此entry。
鍵key為null時候的特殊處理
HashMap允許null的鍵和null的值,鍵為null的entry都放在entry陣列索引為0地方,即下標為0的桶內(也有可能其他非null鍵hash值剛好為0,因此需要掃描比較)
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
如果size==0,則value必然為null,直接返回null
否則通過Entry<K,V> e = table[0]獲取連結串列頭部
通過e.key == null找到key為null的entry,返回此entry的value
通過鍵獲取值
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
分類處理,key為null通過getForNullKey()獲取對應的value
否則通過getEntry(key)獲取對應的entry,通過entry獲取value
向entry連結串列新增一個新的entry
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
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);
}
首先判斷size >= threshold,如果儲存的鍵值對數量size大於閾值threshold的話,需要擴容,然後rehash,重新計算hash值和桶的位置
然後呼叫createEntry把entry新增到連結串列頭
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++;
}
根據key刪除資料
根據key的hash值找到桶,遍歷桶中連結串列,找到對應key的entry,刪除。刪除的辦法就是entry的前序節點的後繼指標直接指向entry節點的後繼節點
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
如果size==0,HashMap中沒有儲存任何鍵值對,對任何key取value均返回null
否則:
計算key的hash值,key==null設定key的hash值為0
否則通過hash(key)計算hash值
然後根據key的hash值計算桶的位置
int i = indexFor(hash, table.length);
然後開始遍歷連結串列,找到與傳入key相等(==或equals)的entry
此entry的前序節點的後繼指標指向entry的後繼節點,跳過此entry節點即可
判斷是否包含某個value
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
HashMap判斷是否包含某個value,只能通過窮舉的辦法,在每個桶內一個一個挨著比較,找到就返回true
HashMap接受null的鍵值,null的鍵儲存在下標為0的桶內,null的值儲存在對應key所在的桶內
判斷是否包含某一特定的value,需要分類討論
如果該value是null,則不能通過equals方法比較,通過==比較
/**
* Special-case code for containsValue with null argument
*/
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
如果value不為null,窮舉查詢比較
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
相關文章
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- 死磕 jdk原始碼之HashMap原始碼分析JDK原始碼HashMap
- JDK1.8原始碼分析之HashMapJDK原始碼HashMap
- JDK 1.6 HashMap 原始碼分析JDKHashMap原始碼
- HashMap原始碼分析(JDK8)HashMap原始碼JDK
- JDK1.8 hashMap原始碼分析JDKHashMap原始碼
- HashMap原始碼分析 JDK1.8HashMap原始碼JDK
- 原始碼分析之 HashMap原始碼HashMap
- JDK1.8原始碼分析筆記-HashMapJDK原始碼筆記HashMap
- 【JDK原始碼分析】淺談HashMap的原理JDK原始碼HashMap
- Jdk1.7下的HashMap原始碼分析JDKHashMap原始碼
- Jdk1.8下的HashMap原始碼分析JDKHashMap原始碼
- 原始碼分析系列1:HashMap原始碼分析(基於JDK1.8)原始碼HashMapJDK
- jdk原始碼分析之TreeMapJDK原始碼
- 集合框架原始碼學習之HashMap(JDK1.8)框架原始碼HashMapJDK
- 原始碼分析——HashMap原始碼HashMap
- HashMap 原始碼分析HashMap原始碼
- HashMap原始碼分析HashMap原始碼
- 死磕 java集合之HashMap原始碼分析JavaHashMap原始碼
- Java:HashMap原始碼分析JavaHashMap原始碼
- 【JDK】JDK原始碼分析-ReentrantLockJDK原始碼ReentrantLock
- 原始碼|jdk原始碼之棧、佇列及ArrayDeque分析原始碼JDK佇列
- HashMap jdk1.7和1.8原始碼剖析HashMapJDK原始碼
- Java HashMap 原始碼逐行解析(JDK1.8)JavaHashMap原始碼JDK
- JDK1.8_HashMap原始碼__tableSizeFor方法解析JDKHashMap原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(3)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(2)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(1)JDK原始碼
- HashMap-put原始碼分析HashMap原始碼
- hashmap原始碼面試分析HashMap原始碼面試
- HashMap原始碼實現分析HashMap原始碼
- JDK原始碼分析-TreeSetJDK原始碼
- HashMap原始碼(JDK1.8)-手動註釋HashMap原始碼JDK
- 原始碼衝浪之HashMap原始碼HashMap
- HashMap原始碼分析,未完待續HashMap原始碼
- java基礎:HashMap — 原始碼分析JavaHashMap原始碼
- Java基礎——HashMap原始碼分析JavaHashMap原始碼