本篇博客僅為本人了解HashMap原理過程中,查閱多篇博客后,為了加強記憶,寫下此篇,在此對多篇博客多有借鑒
HashMap概述
HashMap是基于哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。
HashMap數據結構
HashMap中數據的存儲是由數組與鏈表一起實現的。HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。
數組
數組是在內存中開辟一段連續的空間,因此占用內存嚴重,故空間復雜的很大。我們只要知道數組首個元素的地址,在數組中尋址就會非常容易,其時間復雜度為O(1)。但是當要插入或刪除數據時,時間復雜度就會變為O(n)。數組的特點是:尋址容易,插入和刪除困難;
鏈表
鏈表在內存的存儲區間是離散的,其插入和刪除操作的內存復雜度為O(1),但是尋址操作的復雜度卻是O(n)。鏈表的特點是:尋址困難,插入和刪除容易。
從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。
HashMap原理
HashMap類有一個叫做Entry的內部類。這個Entry類包含了key-value作為實例變量。 每當往hashmap里面存放key-value對的時候,都會為它們實例化一個Entry對象,這個Entry對象就會存儲在前面提到的Entry數組table中。Entry具體存在table的那個位置是 根據key的hash值來決定。
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry就是數組中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。
HashMap存取實現
存儲
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
//HashMap允許存放null鍵值
//當key為null是,調用putForNullKey()方法,將value插入到數組的第一個位置,即角標為0的位置
if (key == null)
return putForNullKey(value);
//計算key的hash值
int hash = hash(key);
//搜索hash值對應的指定數組的索引
int i = indexFor(hash, table.length);
//如果i處的索引處Entry不為null,遍歷e元素的下一個元素
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;
}
}
//如果i出索引Entry為null,表明此處沒有Entry
modCount++;
//將key和value添加到i處索引
addEntry(hash, key, value, i);
return null;
}
從上面的源代碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hash值,得到這個元素在數組中的位置(即下標),然后再在該索引上的單向鏈表進行循環遍歷用equals比較key是否存在,如果存在則用新的value覆蓋原值,如果不存在,則插入鏈表的頭部,這與后面的多線程安全相關。
putForNullKey(V value)方法
/**
* Offloaded version of put for null keys
*/
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;
}
addEntry(int hash, K key, V value, int bucketIndex) 方法
根據計算出的hash值,將key-value對放在數組table的i索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼如下:
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The next size value at which to resize (capacity * load factor).一般是大于0.75,開始擴容,double
* @serial
*/
int threshold;
/**
* 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);
}
createEntry(int hash, K key, V value, int bucketIndex)方法
/**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
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++;
}
讀取
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
//如果key為null,調用getForNullkey()方法,如果數組0角標對應的Entry不為null,遍歷e元素的下一個元素
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hash值,找到數組中對應位置的某一元素,然后通過key的equals方法在對應位置的鏈表中找到需要的元素。
getForNullKey()方法
/**
* Offloaded version of get() to look up null keys. Null keys map
* to index 0. This null case is split out into separate methods
* for the sake of performance in the two most commonly used
* operations (get and put), but incorporated with conditionals in
* others.
*/
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
getEntry(Object key)方法
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
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;
}
總結
HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash值來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash值找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。
山外山
HashMap的兩個重要屬性是容量capacity和裝載因子loadfactor,默認值分別為16和0.75,當容器中的元素個數大于 capacity*loadfactor = 12時,容器會進行擴容resize 為2n,在初始化Hashmap時可以對著兩個值進行修改,負載因子0.75被證明為是性能比較好的取值,通常不會修改,那么只有初始容量capacity會導致頻繁的擴容行為,這是非常耗費資源的操作,所以,如果事先能估算出容器所要存儲的元素數量,最好在初始化時修改默認容量capacity,以防止頻繁的resize操作影響性能。
java8對hashmap做了優化 ,底層有兩種實現方法,一種是數組和鏈表,一種是數組和紅黑樹,hsahmap會根據數據量選擇存儲結構
if (binCount >= TREEIFY_THRESHOLD - 1)
當符合這個條件的時候,把鏈表變成treemap,這樣查找效率從o(n)變成了o(log n)