本系列文章所描述的所有類或接口都是基于 JDK 1.7的源碼,在其它 JDK 的實現方式中可能會有所不同。
一、實現方式
HashMap 是 Map 實現中最常用的,具體實現方式如下。
二、創建 HashMap
將 loadFactor 設為默認的 0.75,threshold 設置為 16。HashMap 還提供了另外三個構造函數:HashMap(int initialCapacity)、HashMap(int initialCapacity, float loadFactor)、HashMap(Map<? extends K, ? extends V> m)。
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);
}
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();
}
void init() {
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
三、添加元素 put(K key, V value)
首先判斷 Entry<K,V>[] 數組是否為空數組,如果是空數組,則初始化數組。在初始化數組的過程中首先計算 capacity 的值和 threshold 的值,注意:這里才是真正初始化 Entry 數組的大小。創建的 Entry 對象數組的大小,并非傳入的初始容量值,而是采用如下方式來決定:
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
比如默認傳進去的 toSize=16,則計算出來的 capacity=16,其中 Integer.highestOneBit 方法是計算最高的1位,其余位設置為0。如果我們在構造函數中指定初始化容量為5,那么計算出來的 capacity=8。如果我們在構造函數中指定初始化容量為10,那么計算出來的 capacity=16。這也是如果我們在初始化時要指定 HashMap 的容量時設置的值最好設置為2的指數方的值的原因。默認情況下創建的 Entry 對象數組的大小為 16,threshold 為 12。
對于 key 為 null 的情況,HashMap 的做法為獲取 Entry 數組中的第一個 Entry 對象,并基于 Entry 對象的 next 屬性遍歷。當找到了其中 Entry 對象的 key 屬性為 null 時,則將其 value 賦值為新的 value,然后返回。如沒有 key 屬性為 null 的 Entry,則增加一個 Entry 對象,增加時為先獲取當前數組的第一個 Entry 對象:e,并創建 Entry 對象,key 為 null,value 為新傳入的對象,next 為之前獲取的 e,如此時 Entry 數組中已有的大小 ≥ threshold,則將 Entry 數組擴大為當前大小的兩倍,擴大時對當前 Entry 對象數組中的元素重新 hash,并填充數組,最后重新設置 threshold 值。
對于 key 不為 null 的情況,首先獲取 key 對象本身的 hashCode,然后再對此 hashCode 做 hash 操作。
hash 完畢后,將 hash 出來的值與 Entry 對象數組的大小減 1 的值進行按位與操作,從而得出當前 key 要存儲到數組的位置。從這個過程可以看出,可能會出現不同的 key 找到相同的存儲位置的問題,也就是數據結構中經典的 hash 沖突的問題了,來看看 HashMap 的解決方法。
在找到要存儲的目標數組的位置后,獲取該數組對應的 Entry 對象,通過調用 Entry 對象的 next 來進行遍歷,尋找 hash 值和計算出來的 hash 值相等,且 key 相等或 equals 的 Entry 對象,如尋找到,則替換此 Entry 對象的值,完成 put 操作,并返回舊的值;如未找到,則往對應的數組位置增加新的 Entry 對象,增加時采取的方法和 key 為 null 的情況基本相同,只是它是替換指定位置的 Entry 對象,可以看出,HashMap 解決沖突采用的是鏈表的方式,而不是開放地址的方法。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
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);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
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;
}
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);
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
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);
}
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++;
}
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
四、獲取元素 get(Object key)
get 的過程和 put 一樣,也是根據 key 是否為 null來分別處理的。對于 key 為 null 的情況下,則直接獲取數組中第一個 Entry 對象,并基于 next 屬性進行遍歷,尋找 key 為null 的 Entry 對象,如找到則返回 Entry 對象的 value,如未找到,則返回 null;對于 key 為非 null 的情況下,則對 key 進行 hash 和按位與操作,找到其對應的存儲位置,然后獲取此位置對應的 Entry 對象,基于 next 屬性遍歷,尋找到 hash 值相等,且 key 相等或 equals 的 Entry 對象,返回其 value,如未找到,則返回 null。
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
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;
}
五、刪除元素 remove(Object key)
remove 的過程和 get 類似,只是在找到匹配的 key 后,如數組上的元素等于 key ,則將數組上的元素的值置為其 next 元素的值;如數據上的元素不等于 key,則對鏈表遍歷,一直到找到匹配的 key 或鏈表的末尾。
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
七、判斷元素是否存在 containsKey(Object key)
通過調用 getEntry 方法來完成,getEntry 方法和 get 過程基本相同。只是在找到匹配的 key 后,直接返回 Entry 對象,而 containsKey 判斷返回的 Entry 對象是否為 null,為 null 則返回 false,不為 null 則返回 true。
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
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;
}
八、遍歷元素 keySet()
在使用 Map 時,經常會通過 keySet 來遍歷 Map 對象,調用 keySet 方法后會返回一個 HashMap 中的 KeySet對象實例,此 KeySet 對象繼承了 AbstractSet。當調用 iterator 方法時,返回一個 KeyIterator 對象實例,調用 next 方法時,遍歷整個數組及 Entry 對象的鏈表,如在遍歷過程中有新的元素加入或刪除了元素,則會拋出 ConcurrentModificationException。
HashMap 在遍歷時是無法保證順序的,如果要保證 Map 中的對象是按順序排列的,最好是使用 TreeMap。
HashMap 是非線程安全的,在并發場景中如果不保持足夠的同步,就有可能在執行 HashMap.get時進入死循環,將 CPU 消耗到 100%,在并發場景中可通過 Collections.synchronizedMap、Collections.unmodifiableMap 或自行同步來保障線程安全,但這些實現方式通常性能會在高并發時下降迅速,最好的方法仍然是使用 ConcurrentHashMap。
六、注意要點
對于 HashMap 而言,最要注意的有以下幾點:
- HashMap 采用數組方式存儲key、value構成的 Entry 對象,無容量限制;
- HashMap 基于 key hash 尋找 Entry 對象存放到數組的位置,對于 Hash 沖突采用鏈表的方式來解決;
- HashMap 在插入元素時可能會要擴大數組的容量,在擴大容量時需要重新計算 hash,并復制對象到新的數組中;
- HashMap是非線程安全的。