HashMap詳解( JDK8 之前與之後對比)
HashMap簡介
HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。
HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。
HashMap 的實現不是同步的,這意味著它不是執行緒安全的。它的key、value都可以為null。
此外,HashMap中的對映不是有序的。
HashMap 的例項有兩個引數影響其效能:“初始容量” 和 “載入因子”。容量 是雜湊表中桶的數量,初始容量 只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。
通常,預設載入因子是 0.75
關於hashcode和HashMap的hash演算法參考部落格:
https://blog.csdn.net/q5706503/article/details/85114159
HashMap的建構函式
// 預設建構函式。 HashMap() // 指定“容量大小”的建構函式 HashMap(int capacity) // 指定“容量大小”和“載入因子”的建構函式 HashMap(int capacity, float loadFactor) // 包含“子Map”的建構函式 HashMap(Map<? extends K, ? extends V> map)
HashMap的API ( JDK8 )
修飾符和型別 | 方法和描述 |
---|---|
void |
clear() 從此對映中刪除所有對映。 |
Object |
clone() 返回此HashMap例項的淺克隆:未克隆鍵和值本身。 |
V |
compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) 嘗試計算指定鍵及其當前對映值的對映(或者 |
V |
computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction) 如果指定的鍵尚未與值關聯(或對映到 |
V |
computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) 如果指定鍵的值存在且為非null,則嘗試在給定鍵及其當前對映值的情況下計算新對映。 |
boolean |
containsKey(Object key) 如果此對映包含指定鍵的對映,則返回true。 |
boolean |
containsValue(Object value) 如果此對映將一個或多個鍵對映到指定值,則返回true。 |
Set<Map.Entry<K,V>> |
entrySet() 返回 |
void |
forEach(BiConsumer<? super K,? super V> action) 對此對映中的每個條目執行給定操作,直到處理完所有條目或操作引發異常。 |
V |
get(Object key) 返回指定鍵對映到的值,或者 |
V |
getOrDefault(Object key, V defaultValue) 返回指定鍵對映到的值,或者 |
boolean |
isEmpty() 如果此對映不包含鍵 - 值對映,則返回true。 |
Set<K> |
keySet() 返回 |
V |
merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) 如果指定的鍵尚未與值關聯或與null關聯,則將其與給定的非空值關聯。 |
V |
put(K key, V value) 將指定的值與此對映中的指定鍵相關聯。 |
void |
putAll(Map<? extends K,? extends V> m) 將指定對映中的所有映射覆制到此對映。 |
V |
putIfAbsent(K key, V value) 如果指定的鍵尚未與值關聯(或對映到 |
V |
remove(Object key) 從此對映中刪除指定鍵的對映(如果存在)。 |
boolean |
remove(Object key, Object value) 僅當指定鍵當前對映到指定值時才刪除該條目的條目。 |
V |
replace(K key, V value) 僅當指定鍵當前對映到某個值時,才替換該條目的條目。 |
boolean |
replace(K key, V oldValue, V newValue) 僅當前對映到指定值時,才替換指定鍵的條目。 |
void |
replaceAll(BiFunction<? super K,? super V,? extends V> function) 將每個條目的值替換為在該條目上呼叫給定函式的結果,直到所有條目都已處理或函式丟擲異常。 |
int |
size() 返回此對映中鍵 - 值對映的數量。 |
Collection<V> |
values() 返回 |
關於entry, entrySet()的說明和map的遍歷可以參考我的另一篇部落格:
https://blog.csdn.net/q5706503/article/details/85122343
JDK8中Entry的名字變成了Node( Node類是HashMap的一個靜態內部類,實現了 Map.Entry<K,V>介面),原因是和紅黑樹的實現TreeNode相關聯。
HashMap的繼承關係
說明:
HashMap繼承於AbstractMap類,實現了Map介面。
Map是"key-value鍵值對"介面,AbstractMap實現了"鍵值對"的通用函式介面。
影響HashMap效能的有兩個引數:初始容量(initialCapacity) 和載入因子(loadFactor)。容量 是雜湊表中桶的數量,初始容量只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。
關於版本說明:
HashMap就是一個散列表, 在JAVA8之前它是通過“拉鍊法”解決雜湊衝突的。
Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。
Java7 HashMap 查詢的時候,根據 hash 值我們能夠快速定位到陣列的具體下標,但是之後的話,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決於連結串列的長度,為 O(n) 。
為了降低這部分的開銷,在 Java8 中,當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)。
JDK8中,當同一個hash值的節點數不小於8時,將不再以單鏈表的形式儲存了,會被調整成一顆紅黑樹(上圖中null節點沒畫)。這就是JDK7與JDK8中HashMap實現的最大區別。
JDK8中Entry的名字變成了Node( Node類是HashMap的一個靜態內部類,實現了 Map.Entry<K,V>介面),原因是和紅黑樹的實現TreeNode相關聯。
HashMap使用示例:
以下示例基於jdk1.7, 取自文尾引用的部落格
import java.util.Map;
import java.util.Random;
import java.util.Iterator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Collection;
/*
* @desc HashMap測試程式
*
* @author skywang
*/
public class HashMapTest {
public static void main(String[] args) {
testHashMapAPIs();
}
private static void testHashMapAPIs() {
// 初始化隨機種子
Random r = new Random();
// 新建HashMap
HashMap map = new HashMap();
// 新增操作
map.put("one", r.nextInt(10));
map.put("two", r.nextInt(10));
map.put("three", r.nextInt(10));
// 打印出map
System.out.println("map:"+map );
// 通過Iterator遍歷key-value
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
System.out.println("next : "+ entry.getKey() +" - "+entry.getValue());
}
// HashMap的鍵值對個數
System.out.println("size:"+map.size());
// containsKey(Object key) :是否包含鍵key
System.out.println("contains key two : "+map.containsKey("two"));
System.out.println("contains key five : "+map.containsKey("five"));
// containsValue(Object value) :是否包含值value
System.out.println("contains value 0 : "+map.containsValue(new Integer(0)));
// remove(Object key) : 刪除鍵key對應的鍵值對
map.remove("three");
System.out.println("map:"+map );
// clear() : 清空HashMap
map.clear();
// isEmpty() : HashMap是否為空
System.out.println((map.isEmpty()?"map is empty":"map is not empty") );
}
}
(某一次)執行結果:
map:{two=7, one=9, three=6}
next : two - 7
next : one - 9
next : three - 6
size:3
contains key two : true
contains key five : false
contains value 0 : false
map:{two=7, one=9}
map is empty
我們來看下JDK8中HashMap的原始碼實現
Entry的名字變成了Node,原因是和紅黑樹的實現TreeNode相關聯。
transient Node<K,V>[] table;
當衝突節點數不小於8-1時,轉換成紅黑樹。
static final int TREEIFY_THRESHOLD = 8;
以put方法在JDK8中有了很大的改變
public V put(K key, V value) {
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;
//如果當前map中無資料,執行resize方法。並且返回n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果要插入的鍵值對要存放的這個位置剛好沒有元素,那麼把他封裝成Node物件,放在這個位置上就完事了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//否則的話,說明這上面有元素
else {
Node<K,V> e; K k;
//如果這個元素的key與要插入的一樣,那麼就替換一下,也完事。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//1.如果當前節點是TreeNode型別的資料,執行putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//還是遍歷這條鏈子上的資料,跟jdk7沒什麼區別
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//2.完成了操作後多做了一件事情,判斷,並且可能執行treeifyBin方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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) //true || --
e.value = value;
//3.
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判斷閾值,決定是否擴容
if (++size > threshold)
resize();
//4.
afterNodeInsertion(evict);
return null;
}
其中treeifyBin()就是將連結串列轉換成紅黑樹。
之前的indefFor()方法消失 了,直接用(tab.length-1)&hash,所以看到這個,代表的就是陣列的下角標。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
覺得不夠詳細可以看看以下大佬的:
http://www.importnew.com/20386.html
https://www.cnblogs.com/yangming1996/p/7997468.html
下面的有JDK8的部分HashMap原始碼分析
https://blog.csdn.net/fighterandknight/article/details/61624150
https://blog.csdn.net/zxt0601/article/details/77413921
參考以下部落格: