HashMap是什麼想必你們都是知道的,平常開發中常用,並且常駐於筆試題目及面試中,那麼今天將從源碼的角度來深刻理解一下HashMap。java
PS:本文如下分析基於jdk1.7,1.8的改動會在文後總結。面試
HashMap是基於哈希表的Map接口實現,是一個key-value型的數據結構。他在性能良好的狀況下,存取的時間複雜度皆爲O(1).數組
要知道數組的獲取時間複雜度爲O(1),可是他的插入時間複雜度爲O(n).bash
那麼HashMap是怎麼作到的呢?數據結構
看一下HashMap的屬性:app
//內部數組的默認初始容量,做爲hashmap的初始容量,是2的4次方,2的n次方的做用是減小hash衝突
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默認的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認負載因子,當容器使用率達到這個75%的時候就擴容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** *當數組表還沒擴容的時候,一個共享的空表對象 */
static final Entry<?,?>[] EMPTY_TABLE = {};
//內部數組表,用來裝entry,大小隻能是2的n次方。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//存儲的鍵值對的個數
transient int size;
/** * 擴容的臨界點,若是當前容量達到該值,則須要擴容了。 * 若是當前數組容量爲0時(空數組),則該值做爲初始化內部數組的初始容量 */
int threshold;
//由構造函數傳入的指定負載因子
final float loadFactor;
//Hash的修改次數
transient int modCount;
//threshold的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//計算hash值時候用,初始是0
transient int hashSeed = 0;
//含有全部entry節點的一個set集合
private transient Set<Map.Entry<K,V>> entrySet = null;
private static final long serialVersionUID = 362498820763181265L;
複製代碼
註釋已經比較完備,便再也不作過多的說明。函數
由裏面的 性能
能夠看出,HashMap的主體實際上是個數組,是Entry這個內部類的數組。學習
Entry內部類是啥呢?優化
這是Entry內部類的屬性,能夠看出這是個單鏈表的節點,由於它內部有指向下一個節點的next。
那麼就至關明瞭了,HashMap內部是一個數組,數組的每個節點是一個鏈表的頭結點,也就是拉鍊式。
對於HashMap來講,平常使用的就是兩個方法,get()
,put()
.
put
.public V put(K key, V value) {
//判斷當前HashMap是否爲空,爲空則初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//判斷傳入的key是否爲null,爲null則放到table[0]的位置或者其鏈表上
if (key == null)
return putForNullKey(value);
//計算key的hash值
int hash = hash(key);
//計算key存放在數組中的下標
int i = indexFor(hash, table.length);
//遍歷該位置上的鏈表,若是存在key值和傳入的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;
}
}
modCount++;
//若是沒有這個值,則添加一個Entry
addEntry(hash, key, value, i);
return null;
}
/** * Offloaded version of put for null keys */
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;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//判斷是否須要擴容,是的話進行擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//新建一個Entry
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//將傳入的key-value放在鏈表的頭部,而且指向原鏈表的頭。
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
複製代碼
代碼中添加了一些註釋,大概是能夠看懂的,那麼這裏總結一下流程。
get()
方法。public V get(Object key) {
//key爲null,則在數組0位置尋找值
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
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;
}
final Entry<K,V> getEntry(Object key) {
//若是hashMap中存的值數量爲0,則返回null
if (size == 0) {
return null;
}
//計算key的hash值
int hash = (key == null) ? 0 : hash(key);
//用indexof函數算出數組下標
//在該下標位置上的鏈表中遍歷,尋找與傳入key相等的key,不然返回null
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;
}
複製代碼
一樣這裏總結一下流程:
你們可能注意到了,在get()
和put()
方法的實現中,都使用到了這兩個方法,那麼這裏看一下源碼:
//經過一系列複雜的計算拿到一個int類型的hash值
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/** * Returns index for hash code h. */
//將hash值和數組長度與,結果等同於hash%length,拿到數組下標
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
複製代碼
這裏重點是:indexOf()方法,將hash值和數組長度與
,結果等同於hash%length,拿到數組下標。
結果等同於取模法,可是運算過程更加快速。這裏有一個重要的知識點,後續會說噢。
在put()
方法及其調用的方法中,當在數組上新添加一個節點時,會判斷當前是否須要擴容,怎麼判斷的呢?
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
複製代碼
能夠看到,噹噹前已經存儲值得size大於閥值,則將數組擴容爲原來的兩倍。
閥值threshold怎麼計算呢?容量 * 負載因子。即 capacity * loadFactory
擴容的方法爲:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/** * Transfers all entries from current table to newTable. */
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
複製代碼
新建一個容量爲原來兩倍的數組,而後將舊數組中的值,rehash以後從新放入新數組,以保證散列均勻。
rehash這個操做是比較費時間的,總的來講擴容操做就比較費時間,由於須要將舊的值移動到新的數組中,所以若是在使用前能預估數量,儘可能使用帶有參數的構造方法,指定初始容量,儘可能避免過多的擴容操做
差點忘記remove()方法了。。
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
/** * Removes and returns the entry associated with the specified key * in the HashMap. Returns null if the HashMap contains no mapping * for this key. */
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//計算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;
}
複製代碼
具體的實現思路也是同樣的:首先計算hash繼而計算下標,而後遍歷數組在該位置的鏈表,找到該key-value而後將其移除掉。
capacity * loadFactory
?首先了解一下
所以能夠發現,HashMap的性能問題又來到了時間和空間的取捨上,當你不擴容,仍然能夠存儲,只是因爲鏈表的變長,性能降低。當你進行太多的擴容,hash碰撞減小,鏈表長度統一減小,性能提升了可是浪費的空間又多了。0.75這個值是開發者定義的一個對時間空間的折中值。
當存入的值愈來愈多,卻不擴容,HashMap性能就會降低,那麼咱們極限一點。
HashMap的容量只有1,存入了100個值。由上面的分析可知,這時候HashMap退化成了單鏈表,存取得時間複雜度都是O(n)。
HashMap的容量爲16,存入一個值,在存入第二個值,當即擴容,這樣能夠儘可能的避免hash碰撞,避免產生鏈表,存取時間複雜度都爲O(1).
所以,當你對存取速度要求很高,能夠適當調低loadfactory,當你當前對速度無所謂,可是內存很小,但是調大loadfactory,固然大部分時候默認值0.75都是一個不錯的選擇。
loadfactory的值爲:0.75,2,4等數字都是合法值
看過上面的代碼咱們能夠發現,HashMap的初始容量爲16,擴容爲原容量乘以2。
也就是說,HashMap的容量永遠是2的次冪,這是爲何呢?
想想哪裏使用到了容量這個參數呢?
在拿到key的hash值,計算當前key在數組中的下標的時候,運用了以下的方法進行計算:
真實的length爲16,咱們假設一個假的lengthWrong = 15;
同時咱們有兩個key,hash以後拿到的hash=8,和hash=9;
length - 1 | 二進制 | 8 & length - 1 | 9 & length- 1 |
---|---|---|---|
15 | 1111 | 1000 & 1111 = 1000 = 8 | 1001 & 1111 = 1001 = 9 |
14 | 1110 | 1000 & 1110 = 1000 = 8 | 1001 & 1110 = 1000 = 8 |
能夠看到當長度爲15時,當h = 8,h =9 h & length - 1
拿到的結果同樣都爲8,也就是這兩個key都存在數組中下標爲8的鏈表上。這是爲何呢?
當length爲偶數時,length- 1位奇數,奇數的二進制最後一位必然爲1,而當length = 奇數時,length - 1位偶數,偶數的二進制最後一位爲0.
二進制與運算有以下規則:
1 & 任意 = 任意;
0 & 任意 = 0;
複製代碼
也就是說,當length = 16時,計算的下標能夠爲1-16任意數字,而當length=15時,計算的下標只能爲2,4,6,8 等等偶數,這樣就浪費了通常的存儲空間,同時還增大了hash碰撞的機率,使得HashMap的性能變差。
所以length必須爲偶數,而length爲2的次冪不只能保證爲偶數,還能夠實現h & length - 1 = h % length
,可謂是一箭雙鵰了。666啊。
在3.2中提到,當極限狀況下HashMap會退化成鏈表,存取時間複雜度變爲O(n),這顯然是不能接受的,所以在java8中對這一點作了優化。
在java7中,存儲在數組上的是一個鏈表的頭結點,當哈希碰撞以後,不斷的增加鏈表的長度,這會致使性能降低。在java8中,引入了紅黑樹數據結構,當鏈表長度小於8時,仍然使用鏈表存儲,而當長度大於8時,會將鏈表轉化爲紅黑樹。同時,當樹的節點數小於6時,會從紅黑樹變成鏈表。
這樣改進以後,即便在性能最差的狀況下,hashMap的存取時間複雜仍爲O(logn).
而紅黑樹的具體實現,這裏再也不詳細敘述,這屬於數據結構的範圍了,在HashMap中展開不合適。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------>呼延十