HashMap其實就那麼一回事兒之原始碼淺析
阿新 • • 發佈:2019-04-06
上篇文章《LinkedList其實就那麼一回事兒之原始碼分析》介紹了LinkedList, 本次將為大家介紹HashMap。
在介紹HashMap之前,為了方便更清楚地理解原始碼,先大致說說HashMap的實現原理, HashMap 是基於陣列 + 連結串列實現的, 首先HashMap就是一個大陣列,在這個陣列中,通過hash值去尋對應位置的元素, 如果遇到多個元素的hash值一樣,那麼怎麼儲存,這就引入了連結串列,在同一個hash的位置,儲存多個元素(通過連結串列關聯起來)。HashMap 所實現的基於<key, value>的鍵值對形式,是由其內部內Entry實現。 好啦,簡單講完HashMap的實現原理後,就開始正式分析原始碼啦:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //預設的HashMap的空間大小16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //hashMap最大的空間大小 static final int MAXIMUM_CAPACITY = 1 << 30; //HashMap預設負載因子,負載因子越小,hash衝突機率越低,至於為什麼,看完下面原始碼就知道了 static final float DEFAULT_LOAD_FACTOR = 0.75f; static final Entry<?,?>[] EMPTY_TABLE = {}; //table就是HashMap實際儲存陣列的地方 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //HashMap 實際儲存的元素個數 transient int size; //臨界值(即hashMap 實際能儲存的大小),公式為(threshold = capacity * loadFactor) int threshold; //HashMap 負載因子 final float loadFactor; //HashMap的(key -> value)鍵值對形式其實是由內部類Entry實現,那麼此處就先貼上這個內部類 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; //儲存了對下一個元素的引用,說明此處為連結串列 //為什麼此處會用連結串列來實現? //其實此處用連結串列是為了解決hash一致的時候的衝突 //當兩個或者多個hash一致的時候,那麼就將這兩個或者多個元素儲存在一個位置,用next來儲存對下個元素的引用 Entry<K,V> next; int hash; 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(); } void recordAccess(HashMap<K,V> m) { } void recordRemoval(HashMap<K,V> m) { } } //以上是內部類Entry //構造方法, 設定HashMap的loadFactor 和 threshold, 方法極其簡單,不多說 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(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } //構造方法,傳入Map, 將Map轉換為HashMap public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); //初始化HashMap, 這個方法下面會詳細分析 inflateTable(threshold); //這就是將指定Map轉換為HashMap的方法,後面會詳細分析 putAllForCreate(m); } //初始化HashMap private void inflateTable(int toSize) { //計算出大於toSize最臨近的2的N此方的值 //假設此處傳入6, 那麼最臨近的值為2的3次方,也就是8 int capacity = roundUpToPowerOf2(toSize); //由此處可知:threshold = capacity * loadFactor threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //建立Entry陣列,這個Entry陣列就是HashMap所謂的容器 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); } private static int roundUpToPowerOf2(int number) { //當臨界值小於HashMap最大容量時, 返回最接近臨界值的2的N次方 //Integer.highestOneBit方法的作用是用來計算指定number最臨近的2的N此方的數 return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } //這就是將指定Map轉換為HashMap的方法,主要看下面的putForCreate方法 private void putAllForCreate(Map<? extends K, ? extends V> m) { for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) putForCreate(e.getKey(), e.getValue()); } private void putForCreate(K key, V value) { //計算hash值, key為null的時候,hash為0 int hash = null == key ? 0 : hash(key); //根據hash值,找出當前hash在table中的位置 int i = indexFor(hash, table.length); //由於table[i]處可能不止有一個元素(多個會形成一個連結串列),因此,此處寫這樣一個迴圈 //當key存在的時候,直接將key的值設定為新值 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } //當key不存在的時候,就在table的指定位置新建立一個Entry createEntry(hash, key, value, i); } //在table的指定位置新建立一個Entry 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++; } //下面就開始分析我們常用的方法了(put, remove) //先看put方法 public V put(K key, V value) { //table為空,就先初始化 if (table == EMPTY_TABLE) { //這個方法上面已經分析過了,主要是在初始化HashMap,包括建立HashMap儲存的元素的陣列等操作 inflateTable(threshold); } //key 為null的情況, 只允許有一個為null的key if (key == null) return putForNullKey(value); //計算hash int hash = hash(key); //根據指定hash,找出在table中的位置 int i = indexFor(hash, table.length); //table中,同一個位置(也就是同一個hash)可能出現多個元素(連結串列實現),故此處需要迴圈 //如果key已經存在,那麼直接設定新值 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++; //key 不存在,就在table指定位置之處新增Entry addEntry(hash, key, value, i); return null; } //當key為null 的處理情況 private V putForNullKey(V value) { //先看有沒有key為null, 有就直接設定新值 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++;、 //當前沒有為null的key就新建立一個entry,其在table的位置為0(也就是第一個) addEntry(0, null, value, 0); return null; } //在table指定位置新增Entry, 這個方法很重要 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //table容量不夠, 該擴容了(兩倍table),重點來了,下面將會詳細分析 resize(2 * table.length); //計算hash, null為0 hash = (null != key) ? hash(key) : 0; //找出指定hash在table中的位置 bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } //擴容方法 (newCapacity * loadFactor) void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果之前的HashMap已經擴充打最大了,那麼就將臨界值threshold設定為最大的int值 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //根據新傳入的capacity建立新Entry陣列,將table引用指向這個新建立的陣列,此時即完成擴容 Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; //擴容公式在這兒(newCapacity * loadFactor) //通過這個公式也可看出,loadFactor設定得越小,遇到hash衝突的機率就越小 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } //擴容之後,重新計算hash,然後再重新根據hash分配位置, //由此可見,為了保證效率,如果能指定合適的HashMap的容量,會更合適 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; } } } //上面看了put方法,接下來就看看remove public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } //這就是remove的核心方法 final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } //老規矩,先計算hash,然後通過hash尋找在table中的位置 int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; //這兒又神奇地回到了怎麼刪除連結串列的問題(上次介紹linkedList的時候,介紹過) //李四左手牽著張三,右手牽著王五,要刪除李四,那麼直接讓張三牽著王五的手就OK 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; } }
以上就是對於HashMap 核心原始碼部分的分析。下面再對HashMap的實現原始碼作一次總結:
1. HashMap只允許一個為null的key。
2. HashMap的擴容:當前table陣列的兩倍
3. HashMap實際能儲存的元素個數: capacity * loadFactor
4. HashMap在擴容的時候,會重新計算hash值,並對hash的位置進行重新排列, 因此,為了效率,儘量給HashMap指定合適的容量