JDK1.7中的HashMap是由數組+鏈表組成的,而JDK1.8中的HashMap是由數組+鏈表+紅黑樹組成。數組的默認長度(DEFAULT_INITIAL_CAPACITY
)爲16(能夠自動變長,也能夠指定一個長度),加載因子(DEFAULT_LOAD_FACTOR
)爲0.75。node
數組:具備遍歷快,增刪慢的特色。數組在堆中是一塊連續的存儲空間,遍歷時數組的首地址是知道的,因此遍歷快,遍歷的時間複雜度爲
O(1)
;當在中間插入或刪除元素時,會形成該元素後面全部元素地址的改變,因此增刪慢,增刪的時間複雜度爲O(n)
。數組
鏈表:鏈表具備增刪快,遍歷慢的特色。鏈表中各元素的內存空間是不連續的,一個節點至少包含節點數據與後繼節點的引用,因此在插入刪除時,只需修改該位置的前驅節點與後繼節點便可,鏈表在插入刪除時的時間複雜度爲
O(1)
。可是在遍歷時,get(n)元素時,須要從第一個開始,依次拿到後面元素的地址,進行遍歷,直到遍歷到第n個元素,遍歷的時間複雜度爲O(n)
,因此效率極低。安全
指兩個元素經過hash函數計算出的值是同樣的,表示這兩個元素存儲的是同一個地址。當後面的元素要插入到這個地址時,發現已經被佔用了,這時候就產生了哈希衝突。bash
哈希衝突的解決辦法:多線程
HashMap
使用的是鏈地址法。在JDK1.7中,若是鏈表過長,效率就會大大下降,查找和添加操做的時間複雜度都爲O(n)
;在JDK1.8中,若是鏈表長度大於8,鏈表就會轉化爲紅黑樹,時間複雜度也就將爲O(logn)
,性能獲得了很大的提高。app
當紅黑樹節點個數少於6的時候,又會將紅黑樹轉化爲鏈表。由於在數據量較小的狀況下,紅黑樹要維持平衡,比起鏈表,性能上的優點並不明顯。函數
若是HashMap的大小超過了負載因子(默認爲0.75)定義的容量,也就是說,當一個Map填滿了75%的Bucket時候,將會建立原來HashMap大小的兩倍的Bucket數組,來從新調整Map的大小,並將原來的對象放入新的Bucket數組中。源碼分析
閾值 = 數組默認的長度 x 負載因子(閾值 = 16 x 0.75 = 12)性能
1. HashMap擴容限制的負載因子爲何是0.75?爲何不能是0.1或者1呢?ui
2. 爲什麼數組容量必須是2次冪? 索引計算公式爲index = (length - 1) & hash
,若是length爲2次冪,那麼length-1的低位就全是1,哈希值進行與操做時能夠保證低位的值不變,效果等同於hash%length
,從而保證分佈均勻。
JDK1.8中在擴容HashMap的時候,不須要像JDK1.7中去從新計算元素的hash,只須要看看原來的hash值新增的哪一個二進制數是1仍是0就行了「是0仍是1能夠認爲是隨機的」,若是是0的話表示索引沒有變,是1的話表示索引變成「oldCap+原索引」,這樣即省去了從新計算hash值的時間,而且擴容後鏈表元素位置不會倒置。
![]()
JDK1.7中的HashMap是由數組+鏈表組成的,組成鏈表結點的是Entity
包含三個元素:key
、value
和指向下一個Entity的next
。
equals
方法逐一比對查找。table[index]
表示經過hash值計算出元素須要存儲在數組中的位置(bucket桶),先判斷該位置上是否存在Entity。Entity<k,v>
對象,插入結束。若是多線程同時put,若是同時觸發了Rehash擴容操做,會致使HashMap中的鏈表中出現循環節點,進而使得後面get的時候,會出現死循環,因此HashMap是非線程安全的。
先定位到數組元素,再遍歷該元素處的鏈表,在尋找目標元素的時候,除了對比經過key計算出來的hash值,還會用雙等或equals方法對key自己來進行比較,二者都爲true時纔會返回這個元素。
obj1.equals(obj2) = true
,則它們的hashCode必須相同;但若是兩個對象不一樣,則它們的hashCode不必定不一樣。String a = new String(「abc」);
String b = new String(「abc」);
複製代碼
若是不覆蓋的話,那麼a和b的hashCode就會不一樣,把這兩個類當作key存到HashMap中的話就會出現問題,就會和key的惟一性相矛盾。
如何定位元素?即二進制 hashCode & (leng-1)
- 計算"book"的hashcode 十進制 : 3029737 二進制 : 101110001110101110 1001
- HashMap長度是默認的 16,length - 1 的結果 十進制 : 15 二進制 : 1111
- 把以上兩個結果作與運算 101110001110101110 1001 & 1111 = 1001 1001的十進制 : 9,因此 index=9。
JDK1.7中的HashMap是由數組+鏈表+紅黑樹組成的,組成鏈表結點的是Node
包含三個元素:key
、value
和指向下一個Node的next
。
table[i]==null
,直接新建節點添加,轉向⑥,若是table[i]不爲空,則轉向③;hashCode
以及equals
;treeNode
紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對,不然轉向⑤;threshold
,若是超過,進行resize()
擴容。public V put(K key, V value) {
// 對key作hash運算獲得hashCode
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步驟①:tab爲空則建立。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步驟②:計算index。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步驟③:節點key存在,直接覆蓋value。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步驟④:判斷該鏈是否爲紅黑樹。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步驟⑤:鏈表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key,value,null);
// 判斷鏈表長度是否大於8,若是鏈表長度大於8轉換爲紅黑樹進行處理。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 若是key已經存在而且equals相等,則直接覆蓋value。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
} ++modCount;
// 步驟⑥:超過最大容量就擴容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
複製代碼
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// Node不爲空 && 計算索引位置而且索引處有值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判斷key的hashCode是否相等
// && 判斷索引處第一個key與傳入key是否相等
// && 判斷key的equals是否相等。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 判斷鏈表是不是紅黑樹,若是是紅黑樹,就從樹中獲取值。
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 若是不是紅黑樹,遍歷鏈表。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
複製代碼
public class HashMapDemo {
@Test
public void test1() {
// 第一種:普通使用,二次取值
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (String key : map.keySet()) {
map.get(key);
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
@Test
public void test2() {
// 推薦,尤爲是容量大時
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (Map.Entry<String, Object> entry : map.entrySet()) {
entry.getValue();
entry.getKey();
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
@Test
public void test3() {
// 經過Map.entrySet使用iterator遍歷key和value
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
Iterator<Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = (Entry<String, Object>) iterator.next();
entry.getKey();
entry.getValue();
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
@Test
public void test4() {
// 經過Map.values()遍歷全部的value,但不能遍歷key
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (@SuppressWarnings("unused") Object object : map.values()) {
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
}
複製代碼