HashMap只容許一個爲null的key。html
HashMap的擴容:當前table數組的兩倍數組
HashMap實際能存儲的元素個數: capacity * loadFactor函數
HashMap在擴容的時候,會從新計算hash值,並對hash的位置進行從新排列, 所以,爲了效率,儘可能給HashMap指定合適的容量,避免屢次擴容this
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; // transient修飾不會被序列化 //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; } // final修飾的方法不能重寫 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(); //k1和k2 要麼是相同的對象(內存地址相等==),要麼對象的值相等(equals且不爲null) 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; } // 鍵值對的hashCode,鍵的hashCode 和 值的hashCode 與運算。 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調用自身構造函數 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) { // 判斷相同hash位置的全部元素,只要有key相同的元素,用新值替換舊值,而後返回 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; } }