一 簡介
java.lang.Object
↳ java.util.AbstractMap
↳ java.util.HashMap
public class HashMap
extends AbstractMap
implements Map, Cloneable, Serializable { }
複製程式碼
HashMap是基於雜湊表實現的,每一個元素是一個key-value對,實現了Serializable、Cloneable介面,允許使用null值和null鍵。不保證對映的順序,內部通過單連結串列解決衝突問題,容量超過(容量*載入因子)時,會自動增長。(除了不同步和允許使用null之外,HashMap類與Hashtable大致相同)
HashMap不是執行緒安全的,如果想獲取執行緒安全的HashMap
- 1通過Collections類的靜態方法synchronizedMap獲得執行緒安全的HashMap。
Map map = Collections.synchronizedMap(new HashMap()); 複製程式碼
- 2使用concurrent併發包下的concurrentHashMap。
二 資料結構
HashMap由陣列+連結串列組成的,主幹是一個Entry陣列,每一個entry包含一個(key-value)鍵值對,連結串列則是主要為了解決雜湊衝突而存在的,HashMap通過key的hashCode來計算hash值,當hashCode相同時,通過“拉鍊法”解決衝突,如下圖所示。
如果定位到的陣列位置不含連結串列(當前entry的next指向null),那麼對於查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度依然為O(1),因為最新的Entry會插入連結串列頭部,只需要簡單改變引用鏈即可,而對於查詢操作來講,此時就需要遍歷連結串列,然後通過key物件的equals方法逐一比對查詢。所以效能考慮,HashMap中的連結串列出現越少,效能才會越好。三 原始碼分析
1 關鍵屬性
//預設初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//預設載入因子,當容器使用率達到75%的時候就擴容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap內部的儲存結構是一個陣列,此處陣列為空,即沒有初始化之前的狀態
static final Entry<?,?>[] EMPTY_TABLE = {};
//空的儲存實體
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//實際儲存的key-value鍵值對的個數
transient int size;
//擴容的臨界點,如果當前容量達到該值,則需要擴容了.
//如果當前陣列容量為0時(空陣列),則該值作為初始化內部陣列的初始容量
int threshold;
//由建構函式傳入的指定負載因子
final float loadFactor;
//修改次數,用於快速失敗機制
transient int modCount;
//預設的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
複製程式碼
2 構造方法
構造方法主要完成容量和載入因子的設定
/**
* 通過初始容量和狀態因子構造HashMap
* @param initialCapacity 容量
* @param loadFactor 載入因子
*/
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方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
init();
}
複製程式碼
/**
* 通過擴容因子構造HashMap,容量去預設值,即16
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製程式碼
/**
* 載入因子取0.75,容量取16,構造HashMap
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
複製程式碼
/**
* 通過其他Map來初始化HashMap,容量通過其他Map的size來計算,載入因子取0.75
*/
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);
//新增m中的元素
putAllForCreate(m);
}
複製程式碼
3 儲存資料(put)
/**
* 存入一個鍵值對,如果key重複,則更新value
* @param key 鍵值名
* @param value 鍵值
* @return 如果存的是新key則返回null,如果覆蓋了舊鍵值對,則返回舊value
*/
public V put(K key, V value) {
//如果陣列為空,則新建陣列
if (table == EMPTY_TABLE) {
//初始化HashMap底層的陣列結構
inflateTable(threshold);
}
//如果key為null,則把value放在table[0]中
if (key == null)
return putForNullKey(value);
//生成key所對應的hash值
int hash = hash(key);
//根據hash值和陣列的長度找到:該key所屬entry在table中的位置i
int i = indexFor(hash, table.length);
/**
* 陣列中每一項存的都是一個連結串列,
* 先找到i位置,然後迴圈該位置上的每一個entry,
* 如果發現存在key與傳入key相等,則替換其value。然後結束方法。
* 如果沒有找到相同的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;
}
}
//map操作次數加一
modCount++;
//檢視是否需要擴容,並將該鍵值對存入指定下標的連結串列頭中
addEntry(hash, key, value, i);
//如果是新存入的鍵值對,則返回null
return null;
}
複製程式碼
/**
* 將該鍵值對存入指定下標的連結串列頭中
* @param hash hash值
* @param key 鍵值名
* @param value 鍵值
* @param bucketIndex 索引
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//當size超過臨界閾值threshold,並且即將發生雜湊衝突時進行擴容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
複製程式碼
/**
* 將鍵值對與他的hash值作為一個entry,插入table的指定下標中的連結串列頭中
* @param hash hash值
* @param key 鍵值名
* @param value 鍵值
* @param bucketIndex 索引
*/
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++;
}
複製程式碼
4 hash演算法
static int hash(int h) {
//此功能確保hash碼不同,有限數量的碰撞
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
複製程式碼
5 調整容量(resize)
/**
* 對陣列擴容,即建立一個新陣列,並將舊陣列裡的東西重新存入新陣列
* @param newCapacity 新陣列容量
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length
//如果當前陣列容量已經達到最大值了,則將擴容的臨界值設定為Integer.MAX_VALUE(Integer.MAX_VALUE是容量的臨界點)
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//建立一個擴容後的新陣列
Entry[] newTable = new Entry[newCapacity];
//將當前陣列中的鍵值對存入新陣列
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//用新陣列替換舊陣列
table = newTable;
//計算下一個擴容臨界點
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
複製程式碼
/**
* 將現有陣列中的內容重新通過hash計算存入新陣列
* @param newTable 新陣列
* @param rehash
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍歷現有陣列中的每一個單連結串列的頭entry
for (Entry<K,V> e : table) {
//查詢連結串列裡的每一個entry
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//根據新的陣列長度,重新計算此entry所在下標i
int i = indexFor(e.hash, newCapacity);
//將entry放入下標i處連結串列的頭部(將新陣列此處的原有連結串列存入entry的next指標)
e.next = newTable[i];
//將連結串列存回下標i
newTable[i] = e;
//檢視下一個entry
e = next;
}
}
}
複製程式碼
6 資料讀取(get)
/**
* 返回此hashmap中儲存的鍵值對個數
* @return 鍵值對個數
*/
public int size() {
return size;
}
複製程式碼
/**
* 根據key找到對應value
* @param key 鍵值名
* @return 鍵值value
*/
public V get(Object key) {
//如果key為null,則從table[0]中取value
if (key == null)
return getForNullKey();
//如果key不為null,則先根據key,找到其entry
Entry<K,V> entry = getEntry(key);
//返回entry節點裡的value值
return null == entry ? null : entry.getValue();
}
複製程式碼
/**
* 返回一個set集合,裡面裝的都是hashmap的value。
* 因為map中的key不能重複,set集合中的值也不能重複,所以可以裝入set。
* 在hashmap的父類AbstractMap中,定義了Set<K> keySet = null;
* 如果keySet為null,則返回內部類KeySet。
* @return 含有所有key的set集合
*/
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
複製程式碼
/**
* 返回一個Collection集合,裡面裝的都是hashmap的value。
* 因為map中的value可以重複,所以裝入Collection。
* 在hashmap的父類AbstractMap中,定義了Collection<V> values = null;
* 如果values為null,則返回內部類Values。
*/
public Collection<V> values() {
Collection<V> vs = values;
return (vs != null ? vs : (values = new Values()));
}
複製程式碼
7 移除資料(clear remove)
/**
* 刪除hashmap中的所有元素
*/
public void clear() {
modCount++;
//將table中的每一個元素都設定成null
Arrays.fill(table, null);
size = 0;
}
複製程式碼
/**
* 根據key刪除entry節點
* @param key 被刪除的entry的key值
* @return 被刪除的節點的value,刪除失敗則返回null
*/
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
複製程式碼
/**
* 根據key刪除entry節點
* @param key 被刪除的entry的key值
* @return 被刪除的節點,刪除失敗則返回null
*/
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//計算key的hash值
int hash = (key == null) ? 0 : hash(key);
//計算所屬下標
int i = indexFor(hash, table.length);
//找到下標所儲存的單連結串列的頭節點
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
//迭代單連結串列找到要刪除的節點
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;
}
複製程式碼
8 Fail-Fast機制
“快速失敗”也就是fail-fast,它是Java集合的一種錯誤檢測機制。當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。 注意 :記住是有可能,而不是一定。 例如:假設存在兩個執行緒(執行緒1、執行緒2),執行緒1通過Iterator在遍歷集合A中的元素,在某個時候執行緒2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),這個時候程式就會丟擲ConcurrentModificationException 異常,從而產生fail-fast機制。
四 JDK1.7和1.8 HashMap的區別
1 資料結構
使用一個Node陣列來儲存資料,但這個Node可能是連結串列結構,也可能是紅黑樹結構。如果插入的key的hashcode相同,那麼這些key也會被定位到Node陣列的同一個桶位。如果同一個桶位裡的key不超過8個,使用連結串列結構儲存。如果超過了8個,那麼會呼叫treeifyBin函式,將連結串列轉換為紅黑樹。那麼即使hashcode完全相同,由於紅黑樹的特點,查詢某個特定元素,也只需要O(logn)的開銷。也就是說put/get的操作的時間複雜度最差只有O(log n)。注意:有一個限制:key的物件,必須正確的實現了Compare介面。如果沒有實現Compare介面,或者實現得不正確(比方說所有Compare方法都返回0)。那JDK1.8的HashMap其實還是慢於JDK1.7的。
2 hash演算法
/**
* @param key
* @return
*/
static final int hash(Object key) {
int h;
// h = key.hashCode() 為第一步 取hashCode值
// h ^ (h >>> 16) 為第二步 高位參與運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製程式碼