1. 前言
本文的源碼是基于JDK1.7,JDK1.8中HashMap的實現,引入了紅黑樹,在后面的文章會寫到。
后面還有一篇LinkedHashMap的解析:圖解LinkedHashMap原理。
2. 使用與實現
2.1 基本使用
HashMap很方便地為我們提供了key-value的形式存取數據,使用put方法存數據,get方法取數據。
Map<String, String> hashMap = new HashMap<String, String>();
hashMap.put("name", "josan");
String name = hashMap.get("name");
2.2 定義
HashMap繼承了Map接口,實現了Serializable等接口。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table;
其實HashMap的數據是存在table數組中的,它是一個Entry數組,Entry是HashMap的一個靜態內部類,看看它的定義。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
可見,Entry其實就是封裝了key和value,也就是我們put方法參數的key和value會被封裝成Entry,然后放到table這個Entry數組中。但值得注意的是,它有一個類型為Entry的next,它是用于指向下一個Entry的引用,所以table中存儲的是Entry的單向鏈表。默認參數的HashMap結構如下圖所示:
2.3 構造方法
HashMap一共有四個構造方法,我們只看默認的構造方法。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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);
// 找到第一個大于等于initialCapacity的2的平方的數
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// HashMap擴容的閥值,值為HashMap的當前容量 * 負載因子,默認為12 = 16 * 0.75
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化table數組,這是HashMap真實的存儲容器
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
// 該方法為空實現,主要是給子類去實現
init();
}
initialCapacity是HashMap的初始化容量(即初始化table時用到),默認為16。
loadFactor為負載因子,默認為0.75。
threshold是HashMap進行擴容的閥值,當HashMap的存放的元素個數超過該值時,會進行擴容,它的值為HashMap的容量乘以負載因子。比如,HashMap的默認閥值為16*0.75,即12。
HashMap提供了指定HashMap初始容量和負載因子的構造函數,這時候會首先找到第一個大于等于initialCapacity的2的平方數,用于作為初始化table。至于為什么HashMap的容量總是2的平方數,后面會說到。
繼續看HashMap構造方法,init是個空方法,主要給子類實現,比如LinkedHashMap在init初始化頭部節點,這里暫時先不介紹。
2.4 put方法
public V put(K key, V value) {
// 對key為null的處理
if (key == null)
return putForNullKey(value);
// 根據key算出hash值
int hash = hash(key);
// 根據hash值和HashMap容量算出在table中應該存儲的下標i
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 先判斷hash值是否一樣,如果一樣,再判斷key是否一樣
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;
}
首先,如果key為null調用putForNullKey來處理,我們暫時先不關注,后面會講到。然后調用hash方法,根據key來算得hash值,得到hash值以后,調用indexFor方法,去算出當前值在table數組的下標,我們可以來看看indexFor方法:
static int indexFor(int h, int length) {
return h & (length-1);
}
這其實就是mod取余的一種替換方式,相當于h%(lenght-1),其中h為hash值,length為HashMap的當前長度。而&是位運算,效率要高于%。至于為什么是跟length-1進行&的位運算,是因為length為2的冪次方,即一定是偶數,偶數減1,即是奇數,這樣保證了(length-1)在二進制中最低位是1,而&運算結果的最低位是1還是0完全取決于hash值二進制的最低位。如果length為奇數,則length-1則為偶數,則length-1二進制的最低位橫為0,則&位運算的結果最低位橫為0,即橫為偶數。這樣table數組就只可能在偶數下標的位置存儲了數據,浪費了所有奇數下標的位置,這樣也更容易產生hash沖突。這也是HashMap的容量為什么總是2的平方數的原因。我們來用表格對比length=15和length=16的情況
我們再回到put方法中,我們已經根據key得到hash值,然后根據hash值算出在table的存儲下標了,接著就是這段for代碼了:
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 先判斷hash值是否一樣,如果一樣,再判斷key是否一樣
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
首先取出table中下標為i的Entry,然后判斷該Entry的hash值和key是否和要存儲的hash值和key相同,如果相同,則表示要存儲的key已經存在于HashMap,這時候只需要替換已存的Entry的value值即可。如果不相同,則取e.next繼續判斷,其實就是遍歷table中下標為i的Entry單向鏈表,找是否有相同的key已經在HashMap中,如果有,就替換value為最新的值,所以HashMap中只能存儲唯一的key。
關于需要同時比較hash值和key有以下兩點需要注意
- 為什么比較了hash值還需要比較key:因為不同對象的hash值可能一樣
- 為什么不只比較equal:因為equal可能被重寫了,重寫后的equal的效率要低于hash的直接比較
假設我們是第一次put,則整個for循環體都不會執行,我們繼續往下看put方法。
modCount++;
addEntry(hash, key, value, i);
return null;
這里主要看addEntry方法,它應該就是把key和value封裝成Entry,然后加入到table中的實現。來看看它的方法體:
void addEntry(int hash, K key, V value, int bucketIndex) {
// 當前HashMap存儲元素的個數大于HashMap擴容的閥值,則進行擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 使用key、value創建Entry并加入到table中
createEntry(hash, key, value, bucketIndex);
}
這里牽涉到了HashMap的擴容,我們先不討論擴容,后面會講到。然后調用了createEntry方法,它的實現如下:
void createEntry(int hash, K key, V value, int bucketIndex) {
// 取出table中下標為bucketIndex的Entry
Entry<K,V> e = table[bucketIndex];
// 利用key、value來構建新的Entry
// 并且之前存放在table[bucketIndex]處的Entry作為新Entry的next
// 把新創建的Entry放到table[bucketIndex]位置
table[bucketIndex] = new Entry<>(hash, key, value, e);
// HashMap當前存儲的元素個數size自增
size++;
}
這里其實就是根據hash、key、value以及table中下標為bucketIndex的Entry去構建一個新的Entry,其中table中下標為bucketIndex的Entry作為新Entry的next,這也說明了,當hash沖突時,采用的拉鏈法來解決hash沖突的,并且是把新元素是插入到單邊表的表頭。如下所示:
2.5 擴容
如果當前HashMap中存儲的元素個數達到擴容的閥值,且當前要存在的值在table中要存放的位置已經有存值時,怎么處理的?我們再來看看addEntry方法中的擴容相關代碼:
if ((size >= threshold) && (null != table[bucketIndex])) {
// 將table表的長度增加到之前的兩倍
resize(2 * table.length);
// 重新計算哈希值
hash = (null != key) ? hash(key) : 0;
// 從新計算新增元素在擴容后的table中應該存放的index
bucketIndex = indexFor(hash, table.length);
}
接下來我們看看resize是如何將table增加長度的:
void resize(int newCapacity) {
// 保存老的table和老table的長度
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 創建一個新的table,長度為之前的兩倍
Entry[] newTable = new Entry[newCapacity];
// hash有關
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
// 這里進行異或運算,一般為true
boolean rehash = oldAltHashing ^ useAltHashing;
// 將老table的原有數據,從新存儲到新table中
transfer(newTable, rehash);
// 使用新table
table = newTable;
// 擴容后的HashMap的擴容閥門值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
再來看看transfer方法是如何將把老table的數據,轉到擴容后的table中的:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍歷老的table數組
for (Entry<K,V> e : table) {
// 遍歷老table數組中存儲每條單項鏈表
while(null != e) {
// 取出老table中每個Entry
Entry<K,V> next = e.next;
if (rehash) {
//重新計算hash
e.hash = null == e.key ? 0 : hash(e.key);
}
// 根據hash值,算出老table中的Entry應該在新table中存儲的index
int i = indexFor(e.hash, newCapacity);
// 讓老table轉移的Entry的next指向新table中它應該存儲的位置
// 即插入到了新table中index處單鏈表的表頭
e.next = newTable[i];
// 將老table取出的entry,放入到新table中
newTable[i] = e;
// 繼續取老talbe的下一個Entry
e = next;
}
}
}
從上面易知,擴容就是先創建一個長度為原來2倍的新table,然后通過遍歷的方式,將老table的數據,重新計算hash并存儲到新table的適當位置,最后使用新的table,并重新計算HashMap的擴容閥值。
2.6 get方法
public V get(Object key) {
// 當key為null, 這里不討論,后面統一講
if (key == null)
return getForNullKey();
// 根據key得到key對應的Entry
Entry<K,V> entry = getEntry(key);
//
return null == entry ? null : entry.getValue();
}
然后我們看看getEntry是如果通過key取到Entry的:
final Entry<K,V> getEntry(Object key) {
// 根據key算出hash
int hash = (key == null) ? 0 : hash(key);
// 先算出hash在table中存儲的index,然后遍歷table中下標為index的單向鏈表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 如果hash和key都相同,則把Entry返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
取值,最簡單粗暴的方式肯定是遍歷table,并且遍歷table中存放的單向鏈表,這樣的話,get的時間復雜度就是O(n的平方),但是HashMap的put本身就是有規律的存儲,所以,取值時,可以按照規律去降低時間復雜度。上面的代碼比較簡單,其實節約的就是遍歷table的過程,因為我們可以用key的hash值算出key對應的Entry所在鏈表在在table的下標。這樣,我們只要遍歷單向鏈表就可以了,時間復雜度降低到O(n)。
get方法的取值過程如下圖所示:
2.7 使用entrySet取數據
HashMap除了提供get方法,通過key來取數據的方式,還提供了entrySet方法來遍歷HashMap的方式取數據。如下:
Map<String, String> hashMap = new HashMap<String, String>();
hashMap.put("name1", "josan1");
hashMap.put("name2", "josan2");
hashMap.put("name3", "josan3");
Set<Entry<String, String>> set = hashMap.entrySet();
Iterator<Entry<String, String>> iterator = set.iterator();
while(iterator.hasNext()) {
Entry entry = iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
結果可知,HashMap存儲數據是無序的。
我們這里主要是討論,它是如何來完成遍歷的。HashMap重寫了entrySet。
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
// 相當于返回了new EntrySet
return es != null ? es : (entrySet = new EntrySet());
}
代碼比較簡單,直接new EntrySet對象并返回,EntrySet是HashMap的內部類,注意,不是靜態內部類,所以它的對象會默認持有外部類HashMap的對象,定義如下:
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
// 重寫了iterator方法
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
// 不相關代碼
...
}
我們主要是關心iterator方法,EntrySet 重寫了該方法,所以調用Set的iterator方法,會調用到這個重寫的方法,方法內部很簡單單,直接調用了newEntryIterator方法,返回了一個自定義的迭代器。我們看看newEntryIterator:
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
可看到,直接new了一個EntryIterator對象返回,看看EntryIterator的定義:
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
// 重寫了next方法
public Map.Entry<K,V> next() {
return nextEntry();
}
}
EntryIterator 是繼承了HashIterator,我們再來看看HashIterator的定義:
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // 下一個要返回的Entry
int expectedModCount; // For fast-fail
int index; // 當前table上下標
Entry<K,V> current; // 當前的Entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
// 不相關
......
}
我們先看構造方法:
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
// 這里其實就是遍歷table,找到第一個返回的Entry next
// 該值是table數組的第一個有值的Entry,所以也肯定是單向鏈表的表頭
while (index < t.length && (next = t[index++]) == null)
;
}
}
以上,就是我們調用了Iterator<Entry<String, String>> iterator = set.iterator();代碼所執行的過程。
接下來就是使用while(iterator.hasNext())去循環判斷是否有下一個Entry,EntryIterator沒有實現hasNext方法,所以也是調用的HashIterator中的hasNext,我們來看看該方法:
public final boolean hasNext() {
// 如果下一個返回的Entry不為null,則返回true
return next != null;
}
該方法很簡單,就是判斷下一個要返回的Entry next是否為null,如果HashMap中有元素,那么第一次調用hasNext時next肯定不為null,且是table數組的第一個有值的Entry,也就是第一條單向鏈表的表頭Entry。
接下來,就到了調用EntryIterator.next去取下一個Entry了,EntryIterator對next方法進行了重寫,看看該方法:
public Map.Entry<K,V> next() {
return nextEntry();
}
直接調用了nextEntry方法,返回下一個Entry,但是EntryIterator并沒有重寫nextEntry,所以還是調用的HashIterator的nextEntry方法,方法如下:
final Entry<K,V> nextEntry() {
// 保存下一個需要返回的Entry,作為返回結果
Entry<K,V> e = next;
// 如果遍歷到table上單向鏈表的最后一個元素時
if ((next = e.next) == null) {
Entry[] t = table;
// 繼續往下尋找table上有元素的下標
// 并且把下一個talbe上有單向鏈表的表頭,作為下一個返回的Entry next
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
其實nextEntry的主要作用有兩點
- 把當前遍歷到的Entry返回
- 準備好下一個需要返回的Entry
如果當前返回的Entry不是單向鏈表的最后一個元素,那只要讓下一個返回的Entrynext為當前Entry的next屬性(下圖紅色過程);如果當前返回的Entry是單向鏈表的最后一個元素,那么它就沒有next屬性了,所以要尋找下一個table上有單向鏈表的表頭(下圖綠色過程)
可知,HashMap的遍歷,是先遍歷table,然后再遍歷table上每一條單向鏈表,如上述的HashMap遍歷出來的順序就是Entry1、Entry2....Entry6,但顯然,這不是插入的順序,所以說:HashMap是無序的。
2.8 對key為null的處理
先看put方法時,key為null
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
//其他不相關代碼
.......
}
看看putForNullKey的處理
private V putForNullKey(V value) {
// 遍歷table[0]上的單向鏈表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
// 如果有key為null的Entry,則替換該Entry中的value
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 如果沒有key為null的Entry,則構造一個hash為0、key為null、value為真實值的Entry
// 插入到table[0]上單向鏈表的頭部
addEntry(0, null, value, 0);
return null;
}
其實key為null的put過程,跟普通key值的put過程很類似,區別在于key為null的hash為0,存放在table[0]的單向鏈表上而已。
我們再來看看對于key為null的取值:
public V get(Object key) {
if (key == null)
return getForNullKey();
//不相關的代碼
......
}
取值就是通過getForNullKey方法來完成的,代碼如下:
private V getForNullKey() {
// 遍歷table[0]上的單向鏈表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
// 如果key為null,則返回該Entry的value值
if (e.key == null)
return e.value;
}
return null;
}
key為null的取值,跟普通key的取值也很類似,只是不需要去算hash和確定存儲在table上的index而已,而是直接遍歷talbe[0]。
所以,在HashMap中,不允許key重復,而key為null的情況,只允許一個key為null的Entry,并且存儲在table[0]的單向鏈表上。
2.9 remove方法
HashMap提供了remove方法,用于根據key移除HashMap中對應的Entry
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
首先調用removeEntryForKey方法把key對應的Entry從HashMap中移除。然后把移除的值返回。我們繼續看removeEntryForKey方法:
final Entry<K,V> removeEntryForKey(Object key) {
// 算出hash
int hash = (key == null) ? 0 : hash(key);
// 得到在table中的index
int i = indexFor(hash, table.length);
// 當前結點的上一個結點,初始為table[index]上單向鏈表的頭結點
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--;
// 如果是table上的單向鏈表的頭結點,則直接讓把該結點的next結點放到頭結點
if (prev == e)
table[i] = next;
else
// 如果不是單向鏈表的頭結點,則把上一個結點的next指向本結點的next
prev.next = next;
// 空實現
e.recordRemoval(this);
return e;
}
// 沒有找到刪除的結點,繼續往下找
prev = e;
e = next;
}
return e;
}
其實邏輯也很簡單,先根據key算出hash,然后根據hash得到在table上的index,再遍歷talbe[index]的單向鏈表,這時候需要看要刪除的元素是否就是單向鏈表的表頭,如果是,則直接讓table[index]=next,即刪除了需要刪除的元素;如果不是單向鏈表的頭,那表示有前面的結點,則讓pre.next = next,也刪除了需要刪除的元素。
2.10 線程安全問題
由前面HashMap的put和get方法分析可得,put和get方法真實操作的都是Entry[] table這個數組,而所有操作都沒有進行同步處理,所以HashMap是線程不安全的。如果想要實現線程安全,推薦使用ConcurrentHashMap。
3 總結
- HashMap是基于哈希表實現的,用Entry[]來存儲數據,而Entry中封裝了key、value、hash以及Entry類型的next
- HashMap存儲數據是無序的
- hash沖突是通過拉鏈法解決的
- HashMap的容量永遠為2的冪次方,有利于哈希表的散列
- HashMap不支持存儲多個相同的key,且只保存一個key為null的值,多個會覆蓋
- put過程,是先通過key算出hash,然后用hash算出應該存儲在table中的index,然后遍歷table[index],看是否有相同的key存在,存在,則更新value;不存在則插入到table[index]單向鏈表的表頭,時間復雜度為O(n)
- get過程,通過key算出hash,然后用hash算出應該存儲在table中的index,然后遍歷table[index],然后比對key,找到相同的key,則取出其value,時間復雜度為O(n)
- HashMap是線程不安全的,如果有線程安全需求,推薦使用ConcurrentHashMap。