JDK源碼學習系列08----HashMap
1.HashMap簡介
HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射。
HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。
HashMap 的實現不是同步的,這意味着它不是線程安全的。它的key、value都可以爲null。此外,HashMap中的映射不是有序的。
<span style="font-size:10px;">public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable</span>
Map接口定義了所有Map子類必須實現的方法。Map接口中還定義了一個內部接口Entry(爲什麼要弄成內部接口?改天還要學習學習)。Entry將在後面有詳細的介紹。
AbstractMap也實現了Map接口,並且提供了兩個實現Entry的內部類:SimpleEntry和SimpleImmutableEntry。
2.HashMap的數據結構
Java最基本的數據結構有數組和鏈表。數組的特點是空間連續(大小固定)、尋址迅速,但是插入和刪除時需要移動元素,所以查詢快,增加刪除慢。鏈表恰好相反,可動態增加或減少空間以適應新增和刪除元素,但查找時只能順着一個個節點查找,所以增加刪除快,查找慢。有沒有一種結構綜合了數組和鏈表的優點呢?當然有,那就是哈希表(雖說是綜合優點,但實際上查找肯定沒有數組快,插入刪除沒有鏈表快,一種折中的方式吧)。一般採用拉鍊法實現哈希表。
ps:圖片來源於網絡
3.HashMap成員變量
HashMap 的實例有兩個參數影響其性能:“初始容量” 和 “加載因子”。容量 是哈希表中桶的數量,初始容量 只是哈希表在創建時的容量。加載因子 是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操作(即重建內部數據結構),從而哈希表將具有大約兩倍的桶數。
通常,默認加載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減少 rehash 操作次數。如果初始容量大於最大條目數除以加載因子,則不會發生
rehash 操作。
/**
* 默認的初始容量,必須是2的冪。
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默認裝載因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 存儲數據的Entry數組,長度是2的冪。
*/
transient Entry[] table;
/**
* map中保存的鍵值對的數量
*/
transient int size;
/**
* 需要調整大小的極限值(容量*裝載因子)
*/
int threshold;
/**
*裝載因子
*/
final float loadFactor;
/**
* map結構被改變的次數
*/
transient volatile int modCount;
HashMap是通過"拉鍊法"實現的哈希表。
它包括幾個重要的成員變量:table, size, threshold, loadFactor, modCount。
table是一個Entry[]數組類型,而Entry實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。
size是HashMap的大小,它是HashMap保存的鍵值對的數量。
threshold是HashMap的閾值,用於判斷是否需要調整HashMap的容量。threshold的值="容量*加載因子",當HashMap中 存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。
loadFactor就是加載因子。
modCount是用來實現fail-fast機制的。
4.HashMap構造函數
/**
*使用默認的容量及裝載因子構造一個空的HashMap
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//計算下次需要調整大小的極限值
table = new Entry[DEFAULT_INITIAL_CAPACITY];//根據默認容量(16)初始化table
init();
}
/**
* 根據給定的初始容量的裝載因子創建一個空的HashMap
* 初始容量小於0或裝載因子小於等於0將報異常
*/
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);
int capacity = 1;
//設置capacity爲大於initialCapacity且是2的冪的最小值
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
/**
*根據指定容量創建一個空的HashMap
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);//調用上面的構造方法,容量爲指定的容量,裝載因子是默認值
}
/**
*通過傳入的map創建一個HashMap,容量爲默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子爲默認值
*/
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
5.HashMap的內部類Entry<K,V>
HashMap底層是用一個Entry<k,v>數組實現的,每個Entry對象的內部又含有指向下一個Entry類型對象的引用。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//對下一個節點的引用(看到鏈表的內容,結合定義的Entry數組,是不是想到了哈希表的拉鍊法實現?!)
final 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;//返回的是之前的Value
}
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();
// Key相等且Value相等則兩個Entry相等
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是Key的hashCode和Value的hashCode的異或的結果
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
// 重寫toString方法,是輸出更清晰
public final String toString() {
return getKey() + "=" + getValue();
}
/**
*當調用put(k,v)方法存入鍵值對時,如果k已經存在,則該方法被調用(爲什麼沒有內容?)
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* 當Entry被從HashMap中移除時被調用(爲什麼沒有內容?)
*/
void recordRemoval(HashMap<K,V> m) {
}
}
其中,Map接口:
K getKey();//獲取Key
V getValue();//獲取Value
V setValue();//設置Value,至於具體返回什麼要看具體實現
boolean equals(Object o);//定義equals方法用於判斷兩個Entry是否相同
int hashCode();//定義獲取hashCode的方法
6.HashMap的常用方法解析
6.1 V put(K key, V value)
public V put(K key, V value) {
// 若“key爲null”,則將該鍵值對添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不爲null”,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“該key”對應的鍵值對不存在,則將“key-value”添加到table中
modCount++;
addEntry(hash, key, value, i);
return null;
}
put時的步驟爲:①.若key爲null,調用putForNullKey(value);
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;
}
HashMap將“key爲null”的元素都放在table的位置0處。②.key不爲null
先用hash()得到key的Hash碼,然後通過indexFor得到在數組中的索引。再通過key.equals()在鏈表中找到插入 的位置
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 設置“bucketIndex”位置的元素爲“新Entry”,
// 設置“e”爲“新Entry的下一個節點”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
6.2 V get(Object key)public V get(Object key) {
if (key == null)
return getForNullKey();
// 獲取key的hash值
int hash = hash(key.hashCode());
// 在“該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.equals(k)))
return e.value;
}
return null;
}
6.3 void putAll(Map<? extends K, ? extends V> m)
public void putAll(Map<? extends K, ? extends V> m) {
// 有效性判斷
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
// 計算容量是否足夠,
// 若“當前實際容量 < 需要的容量”,則將容量x2。
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
// 通過迭代器,將“m”中的元素逐個添加到HashMap中。
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
put(e.getKey(), e.getValue());
}
}
6.4 containsKey()
containsKey() 首先通過getEntry(key)獲取key對應的Entry,然後判斷該Entry是否爲null。
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
// 獲取哈希值
// HashMap將“key爲null”的元素存儲在table[0]位置,“key不爲null”的則調用hash()計算哈希值
int hash = (key == null) ? 0 : hash(key.hashCode());
// 在“該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;
}
7.關於Hash衝突
8.HashMap的優化
容量調整
對於容量的調整,這個是HashMap較爲重點的部分,仔細想想看,對於hashMap我們應該做的是儘量的避免hash衝突 ,此時對於數組的擴容就應該考慮了。不過一個蛋疼的問題也就 出現了,由於新數組的容量變了,原數組的數據就必須重新計算其再數組中的位置,並放入這就是resize。同時這也是最消耗性能的地方。那麼在什麼情況下對HashMap進行擴容呢?一般當HashMap的元素個事超過數組大小**loadFactory的時候,就會進行擴容,而loadFactor就是上文所說的負加載因子。默認值爲0.75 例如數組空間爲16,當元素超過16*0.75=12的時候就把數組大小擴爲2*16=32,然後resize這是一個非常消耗性能的是,因此如果我們預料到HashMap中元素的個數,這就能夠有效的提高hashMap的性能。
負載因子
爲確定何時調整大小,而不是對每個存儲桶中的鏈接列表的深度進行計數,基於hash的 Map使用一個額外的參數並粗略計算存儲桶的密度。Map在調整大小之前,使用名爲LoadFactory的參數指示Map將承擔的“負載”量,即它的負載程度。loadFactory、map大小、容量之間關係: 如果(負載因子)x(容量)>(Map 大小),則調整 Map 大小
數組長度爲2的n次方
當length總是 2 的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。
假設數組長度分別爲15和16,優化後的hash碼分別爲8和9,那麼&運算後的結果如下:
h & (table.length-1) hash table.length-1
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
-----------------------------------------------------------------------------------------------------------------------
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101
從上面的例子中可以看出:當它們和15-1(1110)“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就
產生了碰撞,8和9會被放到數組中的同一個位置上形成鏈表,那麼查詢的時候就需要遍歷這個鏈表,得到8或者9,這樣就降低了查詢的效率。
同時,我們也可以發現,當數組長度爲15的時候,hash值會與15-1(1110)進行“與”,那麼 最後一位永遠是0,而0001,0011,0101,
1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,
這意味着進一步增加了碰撞的機率,減慢了查詢的效率!
而當數組長度爲16時,即爲2的n次方時,2n-1得到的二進制數的每個位上的值都爲1,這使得在低位上&時,得到的和原hash的低位相同,
加之hash(int h)方法對key的hashCode的進一步優化,加入了高位計算,就使得只有相同的hash值的兩個值纔會被放到數組中的同一個位置上
形成鏈表。
所以說,當數組長度爲2的n次冪的時候,不同的key算得得index相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的機率小,
相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
9.總結ps:參考以下網友,感謝感謝~~
http://www.cnblogs.com/yuyutianxia/p/3800768.html
http://blog.csdn.net/lcore/article/details/8885961
http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html