Java HashMap的工作原理和實現
目錄
概述
HashMap的基本操作如下:
map.put("Chinese", 1);
map.put("Math", 2);
map.put("Englist", 3);
map.put("Chemistry", 4);
map.put("Biology", 5);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
定義
HashMap實現了Map介面,繼承子AbstractMap。其中,Map介面定義了鍵對映到值的規則。
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
建構函式
HashMap提供了三個建構函式,具體實現如下。
- 構造一個預設具有初始容量(16)和預設載入因子(0.75)的空HashMap
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
- 構造一個具有預設因子(0.75)的空HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 構造一個帶指定初始容量和載入因子的空HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
// init函式為空,需要有特殊需求的子類單獨實現
init();
}
通過上面的三個建構函式,我們可以看出,HashMap的建構函式完成的工作就是對loadFactor和threshold這兩個成員屬性賦值。而這兩個成員屬性的含義如下:
- threshold: 初始容量,表示雜湊表中桶的數量。
- loadFactor:負載因子,表示當前雜湊表的最大填滿比例。當threshold * loadFactor < 當前雜湊表中桶數目時,雜湊表的threshold需要擴大為當前的2倍。
資料結構
JAVA中HashMap是由陣列和引用實現的”連結串列雜湊”。HashMap底層實現是陣列,但是陣列的每一項都是一個連結串列,其中initialCapacity就代表了陣列的長度。HashMap初始化資料結構的程式碼如下:
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
其中,Entry為HashMap的內部類,它包含了鍵key、值value、下一個節點next,以及hash值。這個內部類非常重要,正是由於Entry才構成table陣列的項為連結串列。
儲存實現:put(key, value)
講完了HashMap的資料結構,我們就來看一下put儲存函式的原始碼實現:
public V put(K key, V value) {
// 當有資料需要儲存時,才對table陣列分配記憶體
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 當key為null時,呼叫putForNullKey方法儲存key為null的鍵值對。將該key儲存在table陣列下標為0的位置上。
if (key == null)
return putForNullKey(value);
// 計算key的hash值
int hash = hash(key);
// 計算插入資料所在連結串列的下標,使用的方法是hash值取餘數組長度
int i = indexFor(hash, table.length);
// 遍歷此下標對應的連結串列,看是否存在該key值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判斷該條連結串列上是否有相同hash值的entry,如果有,則替換entry的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
// 返回舊值,結束插入操作
return oldValue;
}
}
// 在下標i對應的連結串列中沒有找到key相同的Entry,則建立一個新的Entry,進行插入操作
modCount++;
// 使用頭插法在下標為i的連結串列中進行插入操作
addEntry(hash, key, value, i);
return null;
}
讀取實現:get(key)
通過對儲存函式put方法的講解,我們很容易就能理解get方法的實現。原始碼如下:
public V get(Object key) {
// 若key為null,呼叫getForNullKey方法,其實就是查詢下標為0的連結串列中key為null的Entry的value
if (key == null)
return getForNullKey();
// getEntry方法實現見下面的函式
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
// 獲取key的hash值
int hash = (key == null) ? 0 : hash(key);
// 根據hash值獲取索引值
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 若搜尋的key與查詢的key相同,則返回對應的value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
JAVA實現HashMap
在Github上實現了一個HashMap的程式碼,還沒來得及實現擴容,歡迎指導。
自定義HashMap
面試常考問題
什麼時候會使用HashMap?他有什麼特點?
當需要儲存鍵值對時需要使用HashMap,它可以接收key為null的鍵值對,但是是非執行緒同步的。
HashMap的工作原理?
這個問題很大,其實上面講的就是HashMap的工作原理。簡單的說如下:
HashMap底層是陣列實現的,陣列的每個元素是連結串列,由Entry內部類實現。HashMap通過put方法儲存物件,通過get方法獲取物件。
儲存物件時,我們將K/V鍵值對傳給put方法,它首先呼叫hash方法計算K的hash值,取餘HashMap陣列長度後獲取該鍵值對所在連結串列的陣列下標,進一步儲存時,會適當調整陣列大小,並且採用頭插法將Entry鍵值對插入到連結串列中。
獲取物件時,我們將K傳給get方法,也是先呼叫hash方法計算hash值獲取陣列中所在連結串列的下標。然後,順序遍歷連結串列,查詢相同Entry的key的value值。
equals()和hashCode()都有什麼作用?
通過取key的hashCode()獲取初步的hash值,使用equals()方法來判斷key值是否相等。
如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
如果超過了負載因子(預設0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新呼叫hash方法。
HashMap和HashTable的區別
- HashTable是執行緒安全的,HashMap不是。所以,在不需要執行緒安全的場景下,HashMap的效率更高。
- HashTable不允許儲存key為null的鍵值對,HashMap可以儲存,儲存在資料下標為0的連結串列中。