文章目錄:
1.HashMap原始碼注釋翻譯
2.HashMap中的屬性
3.HashMap中的方法
3.1 構造方法
3.2 get方法
3.3 put方法
3.4 remove方法
3.5 hash方法
3.6 resize方法
3.7 size方法
3.8 isEmpty方法
3.9 clear方法
3.10 containsKey方法
3.11 containsValue方法
3.12 replace方法
3.13 關于遍歷map集合的三個方法
4.傳統HashMap的缺點——引入紅黑樹

1.HashMap原始碼注釋翻譯
* Hash table based implementation of the <tt>Map</tt> interface. This * implementation provides all of the optional map operations, and permits * <tt>null</tt> values and the <tt>null</tt> key. (The <tt>HashMap</tt> * class is roughly equivalent to <tt>Hashtable</tt>, except that it is * unsynchronized and permits nulls.) This class makes no guarantees as to * the order of the map; in particular, it does not guarantee that the order * will remain constant over time.翻譯一下大概就是在說,這個哈希表是基于 Map 介面的實作的,它允許 null 值和null 鍵,它不是執行緒同步的,同時也不保證有序,
* <p>This implementation provides constant-time performance for the basic * operations (<tt>get</tt> and <tt>put</tt>), assuming the hash function * disperses the elements properly among the buckets. Iteration over * collection views requires time proportional to the "capacity" of the * <tt>HashMap</tt> instance (the number of buckets) plus its size (the number * of key-value mappings). Thus, it's very important not to set the initial * capacity too high (or the load factor too low) if iteration performance is * important.再來看看這一段,講的是 Map 的這種實作方式為 get(取)和 put(存)帶來了比較好的性能,但是如果涉及到大量的遍歷操作的話,就盡量不要把 capacity 設定得太高(或 load factor 設定得太低),否則會嚴重降低遍歷的效率,
影響 HashMap 性能的兩個重要引數:“initial capacity”(初始化容量)和”loadfactor“(負載因子),簡單來說,容量就是哈希表桶的個數,負載因子就是鍵值對個數與哈希表長度的一個比值,當比值超過負載因子之后,HashMap 就會進行 rehash操作來進行擴容,
- HashMap集合底層結構是 陣列 + 單向鏈表 + 紅黑樹,
- HashMap集合中的key、value均可為 null,其中key是無序不可重復的,
- HashMap集合的默認初始化容量是16,默認加載因子是 0.75,擴容之后是原容量的2倍,
- 如果HashMap集合中某個桶中的結點數超過了8,則單向鏈表結點會被替換成紅黑樹結點;當桶中的結點數小于6時,會將樹形結點轉回單向鏈表結點,只有當哈希表中的元素數量超過64時,才會進行樹形化(即轉換成紅黑樹這種結構),否則只是進行擴容,
HashMap 的大致結構如下圖所示,其中哈希表是一個陣列,我們經常把陣列中的每一個節點稱為一個桶,哈希表中的每個節點都用來存盤一個鍵值對,在插入元素時,如果發生沖突(即多個鍵值對映射到同一個桶上)的話,就會通過鏈表的形式來解決沖突,因為一個桶上可能存在多個鍵值對,所以在查找的時候,會先通過 key 的哈希值先定位到桶,再遍歷桶上的所有鍵值對,找出 key 相等的鍵值對,從而來獲取 value,


2.HashMap中的屬性
//默認的初始容量為 2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大的容量上限為 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認的加載因子為 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//變成樹型結構的臨界值為 8
static final int TREEIFY_THRESHOLD = 8;
//恢復鏈式結構的臨界值為 6
static final int UNTREEIFY_THRESHOLD = 6;
//當哈希表的大小超過這個閾值,才會把鏈式結構轉化成樹型結構,否則僅采取擴容來嘗試減少沖突
static final int MIN_TREEIFY_CAPACITY = 64;
//哈希表
transient Node<K,V>[] table;
//哈希表中鍵值對的個數
transient int size;
//哈希表被修改的次數
transient int modCount;
//它是通過 capacity*load factor 計算出來的,當 size 到達這個值時,就會進行擴容操作
int threshold;
//負載因子,決定了HashMap集合的資料密度
//負載因子過大,發生碰撞的幾率會越高
//負載因子過小,就越容易觸發擴容,擴容自然也會影響性能
//按照其他語言的參考及研究經驗,會考慮將負載因子設定為0.75,此時平均檢索長度接近于常數
final float loadFactor;
下面是 Node 類的定義,它是 HashMap 中的一個靜態內部類,哈希表中的每一個節點都是 Node 型別,我們可以看到 Node 類中有 4 個屬性,其中除了 key 和
value 之外,還有 hash 和 next 兩個屬性,hash 是用來存盤 key 的哈希值的,next 是在構建鏈表時用來指向后繼節點的,
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
3.HashMap中的方法
3.1 構造方法
關于HashMap原始碼中的構造方法,無非是在更改 初始化容量、加載因子這些引數,這里就不再多說了,(主要是后面的get、put方法)
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;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3.2 get方法
get方法首先是創建了一個 Node 結點物件,然后其中呼叫了 getNode 方法,所以我們著重來看一下這個 getNode 方法,
這個 getNode() 方法首先:如果哈希表不為空 && key 對應的桶上不為空,然后根據哈希表元素個數與哈希值求模( 使用的公式是 (n - 1) &hash )得到 key 所在的桶的頭結點,如果頭結點恰好命中(是我們要get的那個key),則直接回傳,如果頭結點沒有命中,則繼續向后續結點進行判斷,如果頭節點恰好是紅黑樹節點TreeNode,就呼叫紅黑樹節點的 getTreeNode() 方法,否則就執行 do-while 遍歷鏈表節點,
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果哈希表不為空 && key 對應的桶上不為空
if ((tab = table) != null && (n = tab.length) > 0 &&
//根據哈希表元素個數與哈希值求模( 使用的公式是 (n - 1) &hash )得到 key 所在的桶的頭結點
(first = tab[(n - 1) & hash]) != null) {
//是否直接命中
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;
}
get方法實作原理敘述:假如我們呼叫原型是 map.get("a"),首先會呼叫 a 的 hash() 方法得到 a 所對應的哈希值,然后通過哈希演算法 tab[(n-1) & hash] 轉換成桶下標(陣列下標),通過這個桶下標快速定位到當前桶結點上,如果比對成功(hashCode、equals都回傳true),則回傳這個桶結點,如果當前桶結點上什么都沒有則回傳null,如果當前桶結點沒有直接命中,它下面還掛載了其他結點,則繼續判斷后續結點,為紅黑樹結構則轉為紅黑樹的get方法獲取結點;如果不是則為普通單向鏈表結構,此時拿著 a 和單向鏈表上的每一個結點進行 equals 方法比對(因為hashCode只有比對成功才會到當前桶結點下繼續比對),有一個equals回傳 true 則比對成功,回傳對應的結點,比對不成功最侄訓傳 null,
3.3 put方法
put 方法的具體實作是在 putVal 方法中,所以我們重點看下面的 putVal 方法,
第一個if判斷,我們一看就知道:做的是哈希表是否為空的判斷,如果為空,呼叫 resize 方法,這個方法作用就是新創建一個哈希表,
第二個if判斷:如果要插入的鍵值對的key對應的哈希值與當前桶結點的哈希值比對為null(不沖突),則直接為這個key創建一個新結點newNode()插入就行了,
下面走到else:與第二個if相反,走到else則說明,要插入的鍵值對與當前桶結點發生沖突了,if是說如果桶上結點的key與我們要插入的key重復,直接確定插入的位置就是該結點(e = p),else if是指采用紅黑樹方法則呼叫紅黑樹對應的方法進行插入,else表示不是紅黑樹,那只能是傳統的單向鏈表結構,只有桶上結點在上面的if中比對不成功,才會走到這個else,它執行的就是將要插入的key與當前桶下掛載的結點一一進行比對,如果比對到鏈表末尾還沒找到重復的key,則newNode創建新結點將要插入的key添加到鏈表末尾,如果鏈表長度超過臨界值,則轉為紅黑樹,else的最后如果找到了重復的key,就break跳出,
else下面的if:就是說明此時已經找到了(我們要插入的key與桶中某個結點的key相等),那么就將要插入key對應的value值覆寫掉原先的舊值,同時回傳覆寫掉的那個舊值,
最后的if則是進行是否擴容的判斷,
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;
//如果哈希表為空,則先創建一個哈希表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果當前桶沒有碰撞沖突,則直接把鍵值對插入,完事
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果桶上節點的 key 與當前 key 重復,那你就是我要找的節點了
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是采用紅黑樹的方式處理沖突,則通過紅黑樹的 putTreeVal 方法去插入這個鍵值對
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否則就是傳統的單向鏈表結構
else {
for (int binCount = 0; ; ++binCount) {
//到了鏈尾還沒找到重復的 key,則說明 HashMap 沒有包含該鍵
if ((e = p.next) == null) {
//創建一個新節點插入到尾部
p.next = newNode(hash, key, value, null);
//如果鏈的長度大于 TREEIFY_THRESHOLD 這個臨界值,則把鏈變為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//找到了重復的 key
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;
}

put方法實作原理敘述:如果哈希表為null,則創建哈希表;通過計算哈希值確定要映射到哪個桶,如果要插入的key與當前桶沒有沖突,則直接插入;如果要插入的key與當前桶上結點沖突,則處理碰撞沖突(如果是紅黑樹則采用紅黑樹方法進行插入;否則就是單向鏈表,對當前桶上結點一一遍歷,如果最終都不沖突,則將該key插入到鏈表末尾;如果鏈表長度達到臨界值,則轉為紅黑樹);如果桶中存在重復的鍵,則將該鍵的舊值替換為要插入的新值,最終判斷size是否大于閾值,大于則執行擴容操作,
3.4 remove方法
remove 方法的具體實作在 removeNode 方法中,所以我們重點看下面的 removeNode 方法,
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果當前 key 映射到的桶不為空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//如果桶上的節點就是要找的 key,則直接命中
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//查找當前桶下掛載的其他結點
else if ((e = p.next) != null) {
//如果是以紅黑樹處理沖突,則構建一個樹節點
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//如果是以鏈式的方式處理沖突,則通過遍歷鏈表來尋找節點
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//比對找到的 key 的 value 跟要洗掉的是否匹配
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//通過呼叫紅黑樹的方法來洗掉節點
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//使用鏈表的操作來洗掉桶上節點
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
3.5 hash方法
在get 方法和put方法中都需要先計算 key映射到哪個桶上,然后才進行之后的操作,計算的主要代碼如下:
(n - 1) & hash上面代碼中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash 是通過下面這個方法計算出來的,采用了二次哈希的方式,其中 key 的 hashCode 方法是一個 native 方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這個 hash 方法先通過 key 的 hashCode 方法獲取一個哈希值,再拿這個哈希值與它的高 16 位的哈希值做一個異或操作來得到最后的哈希值,計算程序可以參考下圖,
為啥要這樣做呢?注釋中是這樣解釋的:如果當 n 很小,假設為 64 的話,那么 n-1 即為 63(0x111111),這樣的值跟 hashCode()直接做與操作,實際上只使用了哈希值的后 6 位,如果當哈希值的高位變化很大,低位變化很小,這樣就很容易造成沖突了,所以這里把高低位都利用起來,從而解決了這個問題,

正是因為與的這個操作,決定了 HashMap 的大小只能是 2 的冪次方,想一想,如果不是2的冪次方,會發生什么事情?即使你在創建HashMap的時候指定了初始大小,HashMap 在構建的時候也會呼叫下面這個方法來調整大小:
這個方法的作用看起來可能不是很直觀,它的實際作用就是把 cap 變成第一個大于等于 2 的冪次方的數,
例如,16 還是 16,13 就會調整為 16,17 就會調整為 32,
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
3.6 resize方法
當HashMap中的元素越來越多的時候,hash沖突的幾率也就越來越高,因為陣列的長度是固定的,所以為了提高查詢的效率,就要對HashMap的陣列進行擴容,而在HashMap陣列擴容之后,最消耗性能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,并放進去,這就是resize,
那么HashMap 什么時候進行擴容呢 ?
當HashMap中的元素個數超過陣列大小(陣列總大小length不是陣列中個數 size)*loadFactor 時 , 就 會 進 行 數 組 擴 容 , loadFactor 的 默 認 值
(DEFAULT_LOAD_FACTOR)為0.75,這是一個折中的取值,也就是說,默認情況下,陣列大小(DEFAULT_INITIAL_CAPACITY)為16,那么當HashMap中元素個數超過16*0.75=12(這個值就是代碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在陣列中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那么預設元素的個數能夠有效的提高HashMap的性能,
在這里有一個需要注意的地方,有些文章指出當哈希表的 桶占用超過閾值時就進行擴容,這是不對的;
實際上是當哈希表中的 鍵值對 個數超過閾值時,才進行擴容的,
3.7 size方法
回傳map集合中鍵值對的個數,
public int size() {
return size;
}
3.8 isEmpty方法
判斷map集合是否為空,
public boolean isEmpty() {
return size == 0;
}
3.9 clear方法
清空map集合,首先判斷哈希表是否為空,為空的情況下,將size置為0,遍歷哈希表依次清空每隔鍵值對,
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
3.10 containsKey方法
判斷map集合中是否包含指定的key,內部實作是呼叫了getNode方法,這個在上面將get方法的時候已經說過了,
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
3.11 containsValue方法
判斷map集合中是否包含指定的value,
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
3.12 replace方法
將map集合中指定key對應的舊值替換為一個新值,
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
3.13 關于遍歷map集合的三個方法
這三個方法分別是 keySet、entrySet、forEach,原始碼我這里就不再多說了,下面給出的是測驗代碼,
@Test
public void test1() {
Map<String,Object> map = new HashMap<>();
map.put("1001","張起靈");
map.put("1002","小哥");
map.put("1003","小宋");
System.out.println("keySet方法遍歷map集合: ");
Set<String> stringSet = map.keySet();
Iterator<String> iterator = stringSet.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
Object value = map.get(key);
System.out.println("鍵: " + key + ", 值: " + value);
}
System.out.println("======================================");
System.out.println("entrySet方法遍歷map集合: ");
Set<Map.Entry<String,Object>> entrySet = map.entrySet();
Iterator<Map.Entry<String,Object>> entryIterator = entrySet.iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Object> entry = entryIterator.next();
String key = entry.getKey();
Object value = entry.getValue();
System.out.println("鍵: " + key + ", 值: " + value);
}
System.out.println("======================================");
System.out.println("forEach方法遍歷map集合: ");
map.forEach((key,value) -> System.out.println("鍵: " + key + ", 值: " + value));
}

4.傳統HashMap的缺點——引入紅黑樹
(1)JDK 1.8 以前 HashMap 的實作是 陣列+鏈表,即使哈希函式取得再好,也很難達到元素百分百均勻分布,
(2)當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當于一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間復雜度就是 O(n),完全失去了它的優勢,
(3)針對這種情況,JDK 1.8 中引入了紅黑樹(查找時間復雜度為 O(logn))來優化這個問題,
HashMap 是陣列+ 鏈表+ 紅黑樹(JDK1.8 增加了紅黑樹部分)實作的,

關于紅黑樹的三個關鍵引數,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/304519.html
標籤:java
上一篇:6000字總結動態記憶體管理

