HashMap的系統介紹:
HashMap實現了Map接口(注意:map類容器都沒有實現Collection接口,只有set,list這類的容器才實現Collection),其對一般的基本操作(put,get,contains)能夠保證常數時間,當然前提是hash function能讓各個key分佈的均勻。然而HashMap不能維護其內<key, value>對的順序,也不保證其中的順序是一直不變的。
有兩個參數能夠影響HashMap的性能: initial capacity 與 load factor,前者指創建HashMap時指定的bucket(抽象list)數量,即底層數組的length,默認爲16;後者指裝填因子,即當 NUMS(Entry) > load factor * capacity 時,自動擴充數組rehash,默認爲0.75。
此外,HashMap is not synchronized。可以使用工具類Collections中的方法:Map m = Collections.synchronizedMap(new HashMap(...));來獲取一個併發的hashmap。當遍歷HashMap時,有另一個Thread試圖修改hashmap,會立即終止迭代並拋出 ConcurrentModificationException ,即所謂的fail-fast策略。
實現原理的概述:
在hashmap的實際實現時,其底層爲bucket的數組(bucket=bin)。爲讓Node分佈更均勻,不至於扎堆集中到同一個bin中,通過key.hashCode()經位運算得一個h值,在利用此h值來計算數組下標index。據此index,在數組中定位到bucket,然後在bucket中進行查找或插入。bucket有list和tree兩種形式:list式的node更小,但是可能導致bucket很深,遍歷list時更耗時;treenode更大,但其能有效降低bucket的深度,能夠更加快速的遍歷bucket。在實際使用時,只有當map比較大時,纔會採用tree式的bucket,以空間換時間。
具體實現源碼分析:
以下分析均基於list數組形式,不考慮tree node(一個TreeNode大概是普通Node的兩倍大小)。且主要考慮get,put,contains三個方法
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默認bucket數量(數組大小)爲16
static final int MAXIMUM_CAPACITY = 1 << 30; //默認最大capacity爲2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默認裝填因子爲0.75
static final int TREEIFY_THRESHOLD = 8; //當一個bucket中多餘8的元素時,這個bin(bucket)就會轉換爲tree實現
static final int UNTREEIFY_THRESHOLD = 6; //當bin中元素小於6時,就會轉換爲list形式實現bucket的功能
static final int MIN_TREEIFY_CAPACITY = 64; //當大於64個bin時,纔會考慮向tree轉化
//幾個重要屬性
transient Node<K,V>[] table; //bucket的數組,即底層list的數組
transient int size; //<key, value>對的數量,調用size()方法返回的就是這個量
transient int modCount; //記錄在迭代時,map被修改的次數,據此在併發環境下報告異常
//利用靜態內部類Node來封裝<key-value>對數據,同時用next屬性來構成list結構的bucket
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //指向下一個Node,構成list,將index相同的Node,都裝到同一個bucket中
Node(int hash, K key, V value, Node<K,V> next) { //constructor
... ...
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
... ...
}
public final boolean equals(Object o) { //判斷兩個<key, value>是否完全相等
... ...
}
}
構造方法:
//在構造時注意,table size(即數組長度)永遠是2的n次方。例如按HashMap(15),table size應爲2^4=16
public HashMap(int initialCapacity, float loadFactor) { //用戶指定初始數組大小及裝填因子
... ...
}
public HashMap(int initialCapacity) { ... ... }
public HashMap() { ... ... }
//靜態工具方法:
static final int hash(Object key) { //根據key的hashCode來計算出一個值h
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//分析:在查找或插入Node時,不直接使用key.hashCode(),而將key.hashCode經過位運算求得一個h值,再根據h值確定數組下標index。
//原因:爲了讓Node能更加均勻的分佈到數組中各個bucket中,儘量避免扎堆
get() 與 containsKey()方法的實現:
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null; //使用h值來定位bucket
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) { //被get與contains調用的工具方法,輸入的hash不是key.hashCode,而是上面提到的h值
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
//index = (table.length-1) & h ,定位到bucket
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) //若這個bin中第一個元素即爲所找就直接返回
return first;
if ((e = first.next) != null) { //否則,就得遍歷這個bucket來查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key); //bin爲tree式的
do { //bin爲list式的
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e; //在遍歷bucket時得不斷調用equals()方法
} while ((e = e.next) != null);
}
}
return null;
}
put(key, value)方法的實現:
public V put(K key, V value) { //key已有時,就更新其對應的value,否則新建一個Node放入value
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //被put()調用的工具方法
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //resize()方法將底層bucket的數組table擴充爲2倍
if ((p = tab[i = (n - 1) & hash]) == null) //index = (table.length-1) & h ,定位到bucket
tab[i] = newNode(hash, key, value, null); //當這個bucket爲null時,就新建一個Node,這個Node就是此bin的第一個節點
else { //當bucket不爲null,有同key的就替換其value,否在就插入一個新Node
Node<K,V> e; K k; //Node e 爲需要放入value的Node,要麼是bucket中已有的,要麼是新插入的
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //bucket的第一個node與欲put的同key
e = p; //記錄下這個Node,在後面統一替換
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //tree式bucket
else { //list式bucket
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //最初p是bucket中第一個Node,且上面已經判斷p不是所找的Node
p.next = newNode(hash, key, value, null); //在循環中,不斷變更p,且利用binCount來記錄此bin中已有多少Node了
if (binCount >= TREEIFY_THRESHOLD - 1) //根據binCount判斷是否需要tree化bucket
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //在bucket中找到了同key的node,就停下,後面更新其value
break; //若沒找到同key的就說明bin中沒有,就e = newNode
p = e;
}
}
if (e != null) { //此時e要麼爲bucket中新插入的Node,要麼爲同key需要更新value的
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //記錄修改次數,以使當併發錯誤時能拋出異常
if (++size > threshold) //更新size,並判斷是否需要擴容table
resize();
afterNodeInsertion(evict);
return null;
}
//此兩個方法在源碼中沒有具體實現,意思應該是當訪問或插入之後,回調此方法,完成一些額外的功能
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
//再看一下map中的一個foreach方法,在進行迭代時,使用modCount來保證併發出錯時能終止迭代,並拋出異常
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc) //在迭代時,若map被其他的線程修改了,就拋出異常
throw new ConcurrentModificationException();
}
}