先從構造函數講起吧java
HashMap有不少個構造函數,不過咱們比較經常使用的是不帶參數的默認構造函數,其源代碼以下:程序員
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
事實上,HashMap的全部鍵值對被存放在一個Entry數組中(變量table),DEFAULT_INITIAL_CAPACITY(爲16)便是默認的Entry數組長度的初始值,固然咱們也能夠自定義entry數組的長度,可是長度必須爲2的n次方,爲何要如此,下面會講解到。算法
Entry數組每個索引處均可以存放一個Entry鏈(經過下面Entry的構造函數能夠發現Entry對象能夠指明它的下一個對象,因此多個Entry對象能夠以Entry鏈的形式存在)數組
Entry類的構造函數以下:函數
Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
每一個存入HashMap的鍵值對都會以Entry對象的形式儲存,Entry類除了擁有key和value變量外,還有兩個重要的變量:性能
next指明瞭下一個Entry對象
hash是該Entry對象的哈希值(Hash碼)this
HashMap在存入鍵值對時首先會根據key的hashCode() 返回值來計算 Hash 碼,而後經過該Hash碼計算出這個這個鍵值對要放在數組的哪一個索引處,設計
若是該索引處尚未存放entry對象則用要存入的鍵值對新建一個Entry對象並將此對象存放於這個索引處,並將next變量賦爲null;指針
若是該索引已經存在entry鏈,則遍歷該entry鏈,若是entry鏈中含有key和要存入的key相等的entry對象,則將該對象的value值替換成咱們要存入的value;code
若是該索引已經存在entry鏈,且entry鏈中沒有key和要存入的key相等的entry對象,則用要存入的鍵值對新建一個entry對象,而後將此對象存入改索引處,並將next指向該索引處以前的對象,也就是說將新的entry對象放入到entry鏈的鏈頭中;
存入鍵值對的方法源代碼以下:
public V put(K key, V value) { // 若是 key 爲 null,調用 putForNullKey 方法進行處理 if (key == null) return putForNullKey(value); // 根據 key 的 keyCode 計算 Hash 值 int hash = hash(key.hashCode()); // 搜索指定 hash 值在對應 table 中的索引 int i = indexFor(hash, table.length); // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 找到指定 key 與須要放入的 key 相等(hash 值相同 // 經過 equals 比較放回 true) if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 若是 i 索引處的 Entry 爲 null,代表此處尚未 Entry modCount++; // 將 key、value 添加到 i 索引處 addEntry(hash, key, value, i); return null; }
計算Hash碼的方法hash是一個純粹的數學計算,其方法以下:
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 Hash 碼值老是相同的。
接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。
indexFor(int h, int length) 方法的代碼以下:
static int indexFor(int h, int length) { return h & (length-1); }
這個方法很是巧妙,它老是經過 h &(table.length -1) 來獲得該對象的保存位置——而 HashMap 底層數組的長度老是 2 的 n 次方
當 length 老是 2 的倍數時,h & (length-1) 將是一個很是巧妙的設計:
假設 h=5,length=16, 那麼 h & length - 1 將獲得 5;
若是 h=6,length=16, 那麼 h & length - 1 將獲得 6 ……若是 h=15,length=16, 那麼 h & length - 1 將獲得 15;
可是當 h=16 時 , length=16 時,那麼 h & length - 1 將獲得 0 了;
當 h=17 時 , length=16 時,那麼 h & length - 1 將獲得 1 了……
這樣保證計算獲得的索引值老是位於 table 數組的索引以內
其實看到這裏也許你問,爲何不直接將hash碼除以length而後取餘數來得到索引值呢,這樣不也是也能夠實現索引值老是位於table數組的索引以內麼?
緣由我以爲應該是:與運算比除運算來的更有效率
當向 HashMap 中添加 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 對象)的存儲位置。
當兩個 Entry 對象的 key 的 hashCode() 返回值相同時,將由 key 經過 eqauls() 比較值決定是採用覆蓋行爲(返回 true),仍是產生 Entry 鏈(返回 false)。
上面程序中還調用了 addEntry(hash, key, value, i); 代碼,其中 addEntry 是 HashMap 提供的一個包訪問權限的方法,該方法僅用於添加一個 key-value 對。
下面是該方法的代碼:
void addEntry(int hash, K key, V value, int bucketIndex) { // 獲取指定 bucketIndex 索引處的 Entry Entry<K,V> e = table[bucketIndex]; // ① // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若是 Map 中的 key-value 對的數量超過了極限 if (size++ >= threshold) // 把 table 對象的長度擴充到 2 倍。 resize(2 * table.length); // ② }
size:該變量保存了該 HashMap 中所包含的 key-value 對的數量
threshold是閥值的意思,閥值等於Entry數組長度乘以負載因子,從上面的例子能夠看出當size++ >= threshold 時,HashMap 會自動調用 resize 方法擴充 HashMap 的容量。每擴充一次,HashMap 的容量就增大一倍。
loadFactor即負載因子,默認的負載因子(DEFAULT_INITIAL_CAPACITY )爲0.75負載
增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);
減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間
掌握了上面知識以後,咱們能夠在建立 HashMap 時根據實際須要適當地調整 load factor 的值;
若是程序比較關心空間開銷、內存比較緊張,能夠適當地增長負載因子;若是程序比較關心時間開銷,內存比較寬裕則能夠適當的減小負載因子。一般狀況下,程序員無需改變負載因子的值。
若是開始就知道 HashMap 會保存多個 key-value 對,能夠在建立時就使用較大的初始化容量,若是 HashMap 中 Entry 的數量一直不會超過極限容量(capacity * load factor),HashMap 就無需調用 resize() 方法從新分配 table 數組,從而保證較好的性能。固然,開始就將初始容量設置過高可能會浪費空間(系統須要建立一個長度爲 capacity 的 Entry 數組),所以建立 HashMap 時初始化容量設置也須要當心對待。
接下來咱們來看一下HashMap 的讀取實現
當 HashMap 的每一個 bucket 裏存儲的 Entry 只是單個 Entry ——也就是沒有經過指針產生 Entry 鏈時,此時的 HashMap 具備最好的性能:當程序經過 key 取出對應 value 時,系統只要先計算出該 key 的 hashCode() 返回值,在根據該 hashCode 返回值找出該 key 在 table 數組中的索引,而後取出該索引處的 Entry,最後返回該 key 對應的 value 便可。
HashMap 類的 get(K key) 方法代碼:
public V get(Object key) { // 若是 key 是 null,調用 getForNullKey 取出對應的 value if (key == null) return getForNullKey(); // 根據該 key 的 hashCode 值計算它的 hash 碼 int hash = hash(key.hashCode()); // 直接取出 table 數組中指定索引處的值, for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; // 搜索該 Entry 鏈的下一個 Entr e = e.next) // ① { Object k; // 若是該 Entry 的 key 與被搜索 key 相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
對於 HashMap 及其子類而言,它們採用 Hash 算法來決定集合中元素的存儲位置。當系統開始初始化 HashMap 時,系統會建立一個長度爲 capacity 的 Entry 數組,這個數組裏能夠存儲元素的位置被稱爲「桶(bucket)」,每一個 bucket 都有其指定索引,系統能夠根據其索引快速訪問該 bucket 裏存儲的元素。
不管什麼時候,HashMap 的每一個「桶」只存儲一個元素(也就是一個 Entry),因爲 Entry 對象能夠包含一個引用變量(就是 Entry 構造器的的最後一個參數)用於指向下一個 Entry,所以可能出現的狀況是:HashMap 的 bucket 中只有一個 Entry,但這個 Entry 指向另外一個 Entry ——這就造成了一個 Entry 鏈。如圖 1 所示:
圖 1. HashMap 的存儲示意