JAVA筆記 —— HashMap(1.7) 底層實現原理
HashMap 底層實現原理
兩年前,我總覺得很多東西會用就行,不喜歡總結,不喜歡深入瞭解,這或許就是因為當時太懶。一年前,我覺得必須要把在工作積累到的東西、遇到的問題及解決方法給總結記錄下來,以便快速提升自己,所以從那時候起就開始寫 txt 文字,做一些簡單記錄。而至今,工作近三年,我越來越覺得了解底層原理的重要性。
HashMap本質:陣列 + 連結串列
在JAVA資料結構中,常用陣列和連結串列這兩種結構來儲存資料。
陣列的儲存區間(在記憶體的地址)是連續的,其大小固定,一旦分配就不能被其他引用佔用,佔用記憶體嚴重。陣列的特點是:定址容易,查詢操作快,時間複雜度為O(1);但插入和刪除的操作比較慢,時間複雜度是O(n)。
連結串列的儲存區間是非連續(離散)的,其大小不固定,可以擴容,佔用記憶體比較寬鬆,故空間複雜度很小。連結串列的特點是:定址困難,查詢速度慢,複雜度是O(n),插入快,時間複雜度為O(1)。
HashMap的資料結構:陣列 + 連結串列(單鏈表),結合了兩者的優點。HashMap的主幹是一個Entry陣列,陣列每一個元素的初始值都是Null。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。
HashMap的初始長度為16,且每次自動擴容或者手動初始化的時候必須是2的冪(以2次方增長)。所以,HashMap 的容量值都是 2^n
Entry是HashMap中的一個靜態內部類。原始碼如下:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; // 儲存指向下一個Entry的引用,單鏈表結構 int hash; // 對key的hashcode值進行hash運算後得到的值,儲存在Entry,避免重複計算 /** * 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) { } }
HashMap
HashMap -- Put 方法實現
方法實現:將指定值與此對映中的指定鍵關聯。如果對映以前包含了鍵的對映,則值被替換。
原始碼如下:
// 將指定值與此對映中的指定鍵關聯。如果對映以前包含了鍵的對映,則值被替換。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 當key為null,呼叫putForNullKey方法,儲存null於table第一個位置中,這是HashMap允許為null的原因
if (key == null)
return putForNullKey(value);
// 計算key的hash值
int hash = hash(key);
// 計算key hash 值在 table 陣列中的位置
int i = indexFor(hash, table.length);
// 從i出開始迭代 e,找到 key 儲存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判斷該條鏈上是否有hash值相同的(key相同)
// 若存在相同,則直接覆蓋value,返回舊value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; // 舊值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; // 返回舊值
}
}
// 修改次數增加1
modCount++;
// 將key、value新增至i位置處
addEntry(hash, key, value, i);
return null;
}
例子 : hashMap.put(“clear”, 888)
首先計算key的hash值:int hash = hash(“clear”);
接著計算key hash 值在 table 陣列中的位置:int i = indexFor(hash, table.length);
假定最後計算出的index是1,那麼結果如下 :
由於HashMap的長度是有限的,當插入的Entry越來越多時,計算出來的index就會有重複,也就是hash衝突。hash值衝突的時候,就將對應節點以連結串列的形式儲存。
頭插法:新節點都增加到頭部,新節點的next指向老節點;如下圖中新的 Entry 2 指向舊的 Entry 1 。
HashMap通過鍵的hashCode來快速的存取元素。當不同的物件hashCode發生碰撞時,HashMap通過單鏈表來解決,將新元素加入連結串列表頭,通過next指向原有的元素。
HashMap原始碼分析
HashMap的建構函式
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... }
HashMap實現了Map介面,繼承AbstractMap。其中Map介面定義了鍵對映到值的規則,而AbstractMap類提供 Map 介面的骨幹實現。
HashMap提供了三個建構函式:
HashMap():構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和指定載入因子的空 HashMap。
原始碼如下:
// HashMap的三個建構函式 -- 原始碼檢視
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial capacity and the default load factor (0.75).
* 用指定的初始容量和預設負載因子(0.75)來構造空<TT> HashMap </TT>
* 如果初始容量為負值,則丟擲非法的異常。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity (16) and the default load factor (0.75).
*使用預設初始容量(16)和預設負載因子(0.75)來構造空<TT> HashMap </TT>
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial capacity and load factor.
* 用指定的初始容量和負載係數 來構造空<TT> HashMap </TT>
* initialCapacity 設定的初始化容量,或者說是 HashMap 擴充陣列時的閥值
* loadFactor 負載因子,預設時 0.75
* 如果初始容量為負值或負載因子為非正,則丟擲非法邏輯異常
*/
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始容量不能 > 最大容量值,HashMap的最大容量值為2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 負載因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
HashMap執行緒不安全原因
在多執行緒情況下,會導致hashmap出現連結串列閉環,一旦進入了閉環get資料,程式就會進入死迴圈,所以導致HashMap是非執行緒安全的。