概述
HashMap是基于哈希表的 Map 接口的實現(xiàn)。數(shù)據(jù)以鍵值對的形式存儲,和HashTab的差別在于HashMap可以以null作為鍵值,但是HashMap是線程不安全的。如果要實現(xiàn)線程同步可以使用:
- Map map = Collections.synchronizedMap(new HashMap());
- ConcurrentHashMap
HashMap的數(shù)據(jù)結構
HashMap是通過數(shù)組和鏈表(散列鏈表)來實現(xiàn)數(shù)據(jù)存儲的。之所以HashMap查詢速度很快,是因為它是通過散列碼來決定存儲位置。通過獲取key的hashcode和數(shù)組的長度的&值來確定存儲的位置,如果有相同的key的hashcode,那么就是所謂的hash沖突,就添加到對應的鏈表結構的數(shù)據(jù)中。
紫色代表數(shù)組代表哈希表,也稱為哈希數(shù)組,綠色代表鏈表。數(shù)組存儲具有相同key值hashcode的鏈表的表頭。數(shù)組的元素和鏈表的節(jié)點都是Entry對象。
HashMap源碼分析(Android中的源碼)
- HashMapEntry對象:
/** HashMapEntry是單向鏈表。
* 它是 “HashMap鏈式存儲法”對應的鏈表。
* 它實現(xiàn)了Map.Entry 接口,即實現(xiàn)getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數(shù)
**/
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry<?, ?> e = (Entry<?, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
}
@Override public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
@Override public final String toString() {
return key + "=" + value;
}
}
HashMapEntry就是一個單向鏈表,每個節(jié)點包含了key、value、hash值、和下一個節(jié)點。
- 重要屬性:
private static final int MINIMUM_CAPACITY = 4;//最小的容量,也是默認的初始容量
static final float DEFAULT_LOAD_FACTOR = .75F;//默認的加載因子
transient HashMapEntry<K, V>[] table;//哈希表
transient int modCount;//被修改的次數(shù)
transient int size;//存放元素的個數(shù)
private transient int threshold;//閾值,當前的元素個數(shù)查過閾值進行擴容,閾值 = 加載因子*總?cè)萘?
加載因子在Android 的源碼中被閹割了,固定為0.75,這是和在jdk中不同的地方
public HashMap(int capacity, float loadFactor) {
this(capacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Load factor: " + loadFactor);
}
/* 加載因子固定為3/4
* Note that this implementation ignores loadFactor; it always uses
* a load factor of 3/4. This simplifies the code and generally
* improves performance.
*/
}
加載因子表示HashMap填充的程度,加載因子越大,HashMap填充的越滿才進行擴容,空間利用率越高,造成的問題是存放的數(shù)據(jù)越多,hash沖突的可能性越大,查找的效率越低。反之亦反。這是一個空間和效率的之間的取舍。一般用默認的就行了。
- put方法:
public V put(K key, V value) {
if (key == null) {
//存放null的key值,則將該鍵值對添加到table[0]中。
return putValueForNullKey(value);
}
//對key的hashcode值進行再次計算得到hash。
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
//通過hash值和數(shù)組長度來確定數(shù)組的下標,這里的值不是隨便取的
int index = hash & (tab.length - 1);
//遍歷對應的數(shù)組下標的鏈表,如果存在相同的key值就替換原來的value
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
//否則加載列表的頭部(也就是數(shù)組對應的位置)
//在添加新的數(shù)據(jù)之前檢查是否需要擴容。如果數(shù)據(jù)的大小超過閾值,數(shù)組的容量就擴大到原來的2倍
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
//把數(shù)據(jù)添加到表頭
addNewEntry(key, value, hash, index);
return null;
}
hash & (tab.length - 1)的算法來取到數(shù)組的下標值,這個方式即可實現(xiàn)均勻的散列,還可以使數(shù)組不越界。那為什么擴容需要2的次冪呢,因為hash & (tab.length - 1)中,如果括號中的結果數(shù)奇數(shù)的話最后一位為1,hash&xx最后的接口有可能是奇數(shù)也有可能是偶數(shù);如果括號中的值是偶數(shù)的話,那最后的結果也只能是偶數(shù),那么數(shù)組存放的值只能存放在偶數(shù)下標中,浪費了一般的資源,如果是2的倍數(shù)減一,剛好能夠控制能過取到所有的下標值。因此,length取2的整數(shù)次冪,是為了使不同hash值發(fā)生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。
擴容:
private HashMapEntry<K, V>[] doubleCapacity() {
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity; j++) {
/*
* Rehash the bucket using the minimum number of field writes.
* This is the most subtle and delicate code in the class.
*/
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
新建了一個數(shù)組,是原來數(shù)組容量的兩倍,然后重新計算hashcode在數(shù)組中的位置,并且存放在數(shù)組中。
那為什么擴容呢?隨著HashMap存放的數(shù)據(jù)越來越多,hash沖入產(chǎn)生的概率就越來越大,造成查找效率越來越低,所以進行擴容,但是擴容需要把所有的元素遍歷重新賦值,還要新建一個數(shù)組,這是HashMap很消耗資源的地方。
在達到閾值的時候開始擴容,如:現(xiàn)在的容量大小為8,8*0.75 = 6,當HashMap中的元素個數(shù)達到6的時候就開始擴容。