HashMap是基於哈希表的Map接口的實現,它對數組以及鏈表做了綜合考慮。在看Handler源碼的時候看到需要了解這方面的知識,於是乎就瞭解下順便寫個博客加深理解。本文只對JDK7的HashMap源碼進行分析,後續版本的紅黑樹先不考慮。
數組:採用一段連續的存儲單元來存儲數據。他的主要特點是:查找速度快,插入和刪除效率低,內存空間要求高,必須有足夠的連續內存空間。
鏈表:插入刪除速度快,內存利用率高。
Hash:翻譯成中文是“散列”的意思。把任意長度的輸入通過散列算法變換成固定長度的輸出,該輸出就是散列值。
Hash衝突:Key鍵值經過行哈希運算得到一個存儲地址,發現已經被其他的元素所佔據。這就是所謂的Hash衝突。
縱向是一個數組,數組的每一項都是一個鏈表。數組相當於藍牙電話列表的首字母,而鏈表相當於對應首字母的一組電話號碼。當然這邊的首字母是舉得一個例子而已,HashMap中對應的是key鍵值經過一定運算得出來的結果。這個結果既要保證數組不能太長以免造成空間的浪費,又要保證鏈表不能太長造成時間的浪費。
OK!在介紹源碼之前先看下幾個重要的變量:
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
// Android-Note: We always use a load factor of 0.75 and ignore any explicitly
// selected values.
final float loadFactor = DEFAULT_LOAD_FACTOR;
size代表的是HashMap中size。
threshold爲當前的閾值,初始化的時候如果沒人設置那麼就是4。
loadFactor爲負載因子,代表了table的填充度有多少,默認是0.75。
2、構造器
接下來就是分析源碼了,對於HashMap可以從get、put以及構造這三方面入手。那麼先看下構造的代碼:
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Android-Note: We always use the default load factor of 0.75f.
// This might appear wrong but it's just awkward design. We always call
// inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
// to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
// the load factor).
threshold = initialCapacity;
init();
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
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).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
大概的意思是如果沒有參數那麼默認table的數組大小爲4,加載因子爲0.75。如果有參數,那麼賦值爲傳進來的參數。
3、put方法介紹
下面介紹下put方法,貼上代碼:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);//標註1
}
if (key == null)
return putForNullKey(value);//標註2
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);//標註3
int i = indexFor(hash, table.length);//標註4
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {//標註5
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;
}
}
modCount++;//標註8
addEntry(hash, key, value, i);//標註7
return null;
}
先看下標註1。當table爲空的時候會調用inflateTable方法:
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
// Android-changed: Replace usage of Math.min() here because this method is
// called from the <clinit> of runtime, at which point the native libraries
// needed by Float.* might not be loaded.
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
threshold = (int) thresholdFloat;
table = new HashMapEntry[capacity];
}
首先他是計算獲取容量,容量的大小必須是2的n次方且大於toSize的數值,然後根據capacity * loadFactor得出閾值,也就是說當元素個數超過容量loadFactor倍的時候才進行擴容。最後是分配容量大小的內存給table。OK,那麼table的初始化完成了。
此處有兩個要重點理解的:1、爲什麼容量一定要是2的n次方。2、HashMapEntry結構包含了那些元素以及作用。
第一個問題接下來分析到indexFor的時候會解答。那麼看下HashMapEntry的變量:
final K key;
V value;
HashMapEntry<K,V> next;
int hash;
key和value就不用多說了,這個是鍵值對的基本參數。next用於建立鏈表,而hash用於存儲獲取到的hash值。
OK,看下put方法的標註2,如果鍵值爲空的情況下會調用putForNullKey方法。貼上putForNullKey方法代碼:
private V putForNullKey(V value) {
for (HashMapEntry<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;
}
邏輯是:遍歷table[0]的鏈表,如果存在key爲null的Entry那麼替換成新值。如果沒找到,那麼在table[0]位置添加該值(該操作由addEntry方法完成,後續介紹)。
回頭再看下put方法的標註3,他的功能是對hashcode進行二次哈希計算,目前只知道他的目的是爲了使哈希值分佈的更加均勻,具體怎麼計算的Mark一下有時間看看。
put方法的標註4是獲取hash值低位的索引號,先看看代碼。
/**
* Returns index for hash code h.
*/
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);
}
我的理解是:1、比如說table有length的長度,比如說是16也就是0~15。OK,那麼有12個元素的hash值,那麼如何均勻地將它分佈在這些數組呢。HashMap應該就是先通過二次哈希計算使得這12個hash值從低位開始儘量地均勻分佈,也就是通過與運算能夠讓這12個值儘量的分散在table上。2、該方法也正回答了前面的疑問,長度如果是2的n次方,那麼對於indexFor的與運算更加的友好。
put方法的標註5,他是循環遍歷table數組中獲取到的索引處的鏈表,如果找出key相等的鍵值對那麼替換成新的值返回舊的值。
下面看下如何判斷key相等首先是e.hash == hash,因爲hash值不相等的話key一定不相等所以首先判斷下這個必要不充分條件,第二步纔是判斷((k = e.key) == key || key.equals(k))。由於equals判斷比hash更耗時間,所以這樣子更能提高效率。
put方法的標註6,modCount是記錄修改次數,他與線程安全有關。在後續的fail-fast策略會提到這個。
put方法的標註7,addEntry(hash, key, value, i)。也就是當在對應的鏈表中找不到的相同key的時候用來增加一個新的Entry。先看看源碼:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
首先是判斷如果元素的數目大於閾值的時候,擴容成原來長度的兩倍並重新計算哈希值以及Index。這邊有個疑問:爲什麼要重新計算哈希值,哈希值難道和低位位數有關?Mark一下。接下來就是重新創建Entry了。總的來說就是如果滿足條件就先擴容,然後再創建鍵值對。
這裏有兩個地方需要理解:第一是resize方法,第二個是createEntry方法。
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
resize也就是根據新的長度創建newTable。具體方法在transfer中實現,遍歷所有的元素重新計算index放入對應新table的桶中。createEntry即增加一個新的Entry。這些操作都是從桶的頭部開始插入!put方法介紹完畢。
4、get方法介紹
下面看下get方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
關鍵是getEntry方法,跟進去看看:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
for (HashMapEntry<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))))//標註1
return e;
}
return null;
}
這邊先通過key的哈希值獲取index,然後只需在對應的桶中遍歷尋找相同的值即可。標註1可以看到必須同時滿足hash值相同以及key相同才能返回e。第一個hash的條件即可屏蔽很多鍵值對,而相對於只用equal來判斷這樣子高效了很多。同時有個疑問:tabel數組的大小永遠會小於元素大小,那麼鏈表不是很難產生,那和鏈表的特性不是體現不出來?從get這個方法看出,或許HashMap可能重點是在判斷hash上面這樣子比直接equal效率高多了。
5、fail-fast(快速失敗)機制
fail-fast 機制,即快速失敗機制,是java集合(Collection)中的一種錯誤檢測機制。當在迭代的過程中該就有可能會發生fail-fast拋出ConcurrentModificationException異常。前面提到modCount變量,註釋可知該變量用於迭代時觸發fail-fast機制。
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
現在看下HashMap拋出異常的代碼:
private abstract class HashIterator<E> implements Iterator<E> {
...
HashIterator() {
expectedModCount = modCount;
...
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
...
}
...
}
在HashIterator方法在構造的時候賦值一下modCount,然後調用nextEntry的時候判斷是否在迭代的過程中被修改了。如果被修改了,那麼就報異常。分爲兩種情況:
1、單線程環境下
while(iterator.hasNext()) {
if (i == 3) list.remove(3);
System.out.println(iterator.next());
i ++;
}
在遍歷的過程中刪除一個元素,那麼就會報出這個異常。
2、多線程條件下,由於HashMap不是線程安全的。所以比如A線程正在迭代的過程中,B線程修改了modCount值。那麼就會報異常。至於modCount修飾符從volatile 變爲transient不是很清楚。Mark一下!
1、與其他集合的差異性可以總結一下
2、Hash表的底層算法原理可以去了解