HashMap 是Map接口的經常使用實現類而且 是以鍵值對(key,value)採用一種所謂的「Hash算法」,來決定每一個元素的存儲位置。程序員
HashMap的存儲實現 算法
當程序試圖將多個 key-value 放入 HashMap 中時,以以下代碼片斷爲例: 數組
HashMap map = new HashMap();性能
map.put("語文",80.0);this
map.put(「數學」,89.0);指針
map.put(「英語」,78.2)code
當程序執行map.put(「語文」,80.0)時,系統將調用「語文」的hashCode方法獲得其HashCode值-每一個Java對象都有一個HashCode方法,均可以經過該方法得到它的hashCode值。獲得這個對象的hashCode值以後,系統會根據該hashCode值來決定元素的存儲位置。對象
看一下HashMap類的put()方法源碼代碼以下。索引
public V put(K key, V value) {接口
//table 爲空則初始化table(Entry[])大小,和hash的掩碼值(須要的時候初始化)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//key值爲空則調用putForNullKey方法進行處理
if (key == null)
return putForNullKey(value);
//根據key的keyCode計算Hash值
int hash = hash(key);
//搜索指定的hash值對應table中的索引
int i = indexFor(hash, table.length);
//若是i索引處的Entry 不爲null,經過循環不斷遍歷e元素的下一個元素
for (Entry 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;
}
上面的源碼程序中用到了一個重要的內部接口 Map.Entry,每一個Map.Entry實際上是一個key-value對。當系統決定存儲HashMap的key-value對時,徹底沒有考慮Entry 中的value,而僅僅只是根據key來計算並決定每一個Entry的存儲位置。
從上面put方法的源碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,首先根據key的hashcode()返回值決定該Entry的存儲對象的位置:若是兩個Entry的key的hashcode()返回值相同,那它們的存儲位置相同;若是這兩個Entry 的key經過equals比較返回true,新添加Entry的value將覆蓋集合中的原有的Entry的value,但key不會覆蓋;若是這兩個Entry 的key經過equals比較返回false,新添加的Entry將與集合中原有的Entry 造成Entry鏈,並且新添加的Entry位於Entry鏈的頭部-具體看AddEntry()方法說明
注意:當向HashMap中添加key-value對,由其key的hashcode()返回值決定該key-value對(Entry對象)的存儲位置。當兩個Entry對象的Key的hashcode()返回值相同時,將由key的equeals()比較值決定是採用覆蓋行爲(返回true執行),仍是產生Entry鏈(返回false執行)
上面程序中還調用了addEntry(hash,key,value,i);代碼,其中addEntry是hashMap提供的一個包的訪問權限的方法,該方法僅用於添加一個key-value對,下面是該方法的源碼
void addEntry(int hash, K key, V value, int bucketIndex) {
//若是Map中的key-value對數量超過了極限,當size>=threshold時,HashMap會自動調用resize方法擴充HashMap的容量。每擴充一次,hashMap就增大一倍。
if ((size >= threshold) && (null != table[bucketIndex])) {
//把table對象的長度擴充2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//獲取指定bucketIndex索引處的Entry
Entry e = table[bucketIndex]; //1
//將建立的Entry彷彿bucketIndex索引處,並讓新的Entry指向原來的Entry
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
上面的源代碼很簡單,系統將新添加的Entry對象放入table數組的bucketIndex索引處,若是bucketIndex索引出已經有一個Entry對象,新添加的Entry對象指向原有的Entry對象(產生一個Entry鏈);若是bucketIndex索引處沒有Entry對象,也就是1處的e變量爲null,即新放入的Entry對象指向null,就沒有產生Entry鏈。(注意:在同一個bucketIndex存儲Entry鏈的狀況下,新放入的Entry老是位於bucketIndex索引中,而最先放入該bucketIndex索引位置的Entry則位於Entry鏈的最末端)
size:改變量保存了該HashMap中全部包含key-value對的數量。
threshold:該變量包含了HashMap能容納的key-value對的極限,它的值等於HashMap容量乘以負載因子(DEFAULT_LOAD_FACTOR)
table是一個普通的數組,每一個數組都有一個固定的長度,這個數組的長度就是HashMap的容量。
jdk1.7之前,建立HashMap時,系統會自動建立一個table數組來保存HashMap 的Entry。jdk1.7中是當掉用put方法時纔對建立table數組。下面源碼:
jdk1.6HashMap構造方法
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能爲負數
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//若是初始容量大於最大容量,讓初始容量等於最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//負載因子必須是大於0的數值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//========如下方法是jdk1.7之前版本的內容。
//計算出大於initialCapacity的最小的2的n次方值
int capacity =1;
while(capacity<initialCapacity)
capacity<<=1;
this.loadFactor = loadFactor;
// 設置容量極限等於容量乘以負載因子
threshold =(int)(capacity*loadFactory);
table = new Entry[capacity];
//=========================
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
jdk1.7的put方法:
public V put(K key, V value) {
//若是table值爲空則建立
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;
}
對於HashMap和子類而言,它們採用Hash算法來決定集合中元素的存儲位置。當系統開始初始化HashMap時(jdk1.7之前版本)或者調用put(key,value)(jdk1.7以上版本)方法時。系統會建立一個長度爲capacity的Entry數組。這個數組存儲元素的位置被稱爲「桶(bucket)」,每一個bucket都有指定的索引,系統能夠根據其索引快速訪問該bucket裏存儲的元素。
不管什麼時候,HashMap的每一個「桶」只能存儲一個元素(即一個Entry),因爲Entry對象包含一個引用變量(就是entry構造器的最後一個參數)用於指向下一個Entry,所以可能出現:HashMap的bucket中的只有一個Entry,但這個Entry指向另外一個Entry----造成一個Entry鏈。以下圖:
當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) {
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;
}
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
代碼中能夠看出,若是hashMap的每一個bucket裏只有一個Entry,HashMap能夠根據索引快速的取出該bucket裏的Entry.在發生Entry衝突狀況下,單個bucket裏存儲的不是一個Entry,而是一個Entry鏈,系統只能按書序遍歷每一個Entry,直到直到想要的Entry爲止。若是下號要搜索的Entry位於該Entry鏈的末端(該Entry最先放入該bucket鏈,那麼系統必須循環到最後才能找到元素)。
總結:HashMap在底層將key-value當成一個總體進行處理。這個總體是一個Entry對象。HashMap底層採用一個Entry[]數組來保存全部的key-value對,當須要存儲一個Entry對象時,會根據Hash算法來決定其存儲位置;當須要取出一個Entry時,也會根據算法找到其存儲位置,直接取出該Entry。因而可知,HashMap之因此能快速存、取它所包含的Entry,徹底相似於現實生活中的:不一樣的東西放在不一樣的位置,須要時才能快速找到它。
當建立HashMap時,有一個默認的負載因子(load factor),其默認值爲0.75.這是時間和空間成本的一種折中;增大負載因子能夠減小Hash表(減小Entry數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的操做(HashMap 的get()與put()方法都要用到查詢);減少負載因子會提升數據的查詢性能,但會下降Hash表所佔的內存空間。
掌握了上面的知識。能夠在建立HashMap的時候根據實際狀況適當地調整load factor的值。。若是程序比較關係內存的開銷。內存比較緊張,能夠適當的增長負載因子;若是程序比較關心時間開銷,內存比較寬裕,則能夠適當地減小負載因子,一般狀況下。程序員不須要改變負載因子的值