HashMap 原始碼分析(JDK1.8)
0 概述
HashMap是Java程式設計師使用頻率最高的容器之一,主要原因它的查詢效率比較高,本文基於JDK1.8,深入探討HashMap的結構實現和功能原理。
1 HashMap底層資料結構
首先看下HashMap底層資料結構,本質上就是一個數組+連結串列+紅黑樹(JDK1.8),陣列存放的是一個個連結串列節點,是採用拉鍊法解決Hash衝突,如果連結串列長度過長(大於8),將會轉化為紅黑樹。
//node 陣列
transient Node<K,V>[] table;
// Node 節點
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;
}
}
1 HashMap實現原理
下圖給出向HashMap容器中,put一個key-value的過程,首先根據key 計算出其對應的hash值,然後再根據hash值,計算機這個key存放位置(陣列下標)。
實現細節:
- hash(key),通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),這麼做可以在陣列table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷,進而減少衝突。
- index=h & (length-1)獲取node 陣列具體位置,這個方法比較巧妙,因為HashMap的Node陣列大小都是2的n次方,當length總是2的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。值得說明是可以指定初始化容量,當然如果你知道容量不是2的n次冪,它會用tableSizeFor方法將其處理成2的n次冪。
//
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 定址
index=h & (length-1);
/**
這個方法就是將容量處理成 2 的n次方,
先將cap處理成全是111111(二進位制的),然後再加1,非常的巧妙
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;//n和n右移一位或,高位至少有兩個1 (處理後n=00011****)
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
幾個重要的屬性
transient int size;
int threshold;
final float loadFactor;
- size 表示hashMap 實際儲存了多少對key-valve。
- threshold,是HashMap所能容納的最大資料量的Node(鍵值對)個數,超過就會擴容(陣列擴容)。由於threshold = length * Load factor,所以在陣列定義好長度之後,負載因子越大,所能容納的鍵值對個數越多。
- loadFactor 負載因子,預設的負載因子0.75是對空間和時間效率的一個平衡選擇。當loadFactor過大(大於1),threshold值也就大於長度,出現碰撞的資料也就越多,查詢等效率也就變低,但是空間利用率高;當loadFactor過小(假設為0.1),threshold遠小於長度,出現碰撞的資料也就比較低,查詢等效率也就變高,但是空間利用率不高。
3 HashMap 擴容
擴容(resize)就是重新計算容量,向HashMap物件裡不停的新增元素,當size大於等於前面提到的這個threshold時候,就需要擴大陣列的長度。簡單的說,也就是重新分配一個大的陣列,將老的陣列中資料拷貝到新的陣列中去。因此從這裡我們可以看到如果我們知道我們需要往HashMap中放多少資料時候,可以直接初始化容量,這樣可以有效避免記憶體拷貝。
由於其擴容(2倍)&以及定址特點,對於擴容後資料拷貝過程,對應的陣列節點資料,要麼還在改位置要麼會索引到(不會跑到其的位置去):原索引+oldCap(老的容量)。舉個例子:初始容量16 一個hash值為10010,其位置010010&01111=00010,可以看到其位置是2,那麼擴容後
容量是32,其位置其位置010010&11111=10010(18)可以看到其位置是18=16+2。由此也可以看出擴容資料分配更加均勻。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//超過最大值,就無法擴容了,只能讓他去碰撞了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//沒有超過最大值,那就擴容是原來的2倍(左移1位)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//初始化情況,沒有給定容量情況
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//擴容後重新計算threshold值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//處理紅黑樹節點的情況
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 等於0 表示還在原位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap的位置
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap的位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4 總結
JDK 1.8 的HashMap底層設計的非常棒,非常巧妙,不愧為大師之作。
相關推薦
HashMap原始碼分析(JDK1.8)
一、HashMap簡介 基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許key和value為null(但是隻能有一個key為null,且key不能重複,value可以重複)。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtab
HashMap 原始碼分析(JDK1.8)
0 概述 HashMap是Java程式設計師使用頻率最高的容器之一,主要原因它的查詢效率比較高,本文基於JDK1.8,深入探討HashMap的結構實現和功能原理。 1 HashMap底層資料結構 首先看下HashMap底層資料結構,本質上就是一個數組+連結
HashMap原始碼分析-jdk1.6和jdk1.8的區別
在java集合中,HashMap是用來存放一組鍵值對的數,也就是key-value形式的資料,而在jdk1.6和jdk1.8的實現有所不同。 JDK1.6的原始碼實現: 首先來看一下HashMap的類的定義: HashMap繼承了AbstractHashMap,實現
LinkedHashMap 原始碼分析 (jdk1.8)
類繼承關係: 什麼是LinkedHashMap 雜湊表和Map介面的連結串列實現,與HashMap的不同之處 在於它維護著一個雙向連結串列,這個連結串列定義了迭代排序,通常是插入順序。 * 如果將鍵重新插入中,則插入順序不受影響。 LinkedHashM
HashMap原始碼解讀(jdk1.8)
本文基於jdk1.8解讀HashMap關鍵程式碼 HashMap是非執行緒安全的,在多執行緒環境下要使用ConcurrentHashMap 儲存結構 HashMap的儲存結構是陣列 + 連結串列 + 紅黑樹,當連結串列的長度大於等於8時,連結轉成紅黑樹 初始化 HashM
LinkedList原始碼分析(jdk1.8)
一、概述 上圖為LinkedList的繼承結構圖,根據繼承關係總結如下: LinkedList 是一個繼承於AbstractSequentialList的雙向連結串列。它也可以被當作堆疊、佇列或雙端佇列進行操作。 LinkedList 實現 List 介面,能對它進行佇列操
HashMap原始碼解析jdk1.8:初始化resize,新增put,獲取get
原始碼解析有參考以下部落格: http://www.cnblogs.com/jzb-blog/p/6637823.html HashMap: 以k-v鍵值對儲存格式的容器,key,value都可以為空,key不重複,非執行緒安全(執行緒安全請使用Concur
HashMap原始碼解析(jdk1.8)
寫在篇頭 其實這是在我寫完下邊所有方法解析後寫的。每次看原始碼,有些時候都不知道每一步的意義在哪裡,缺少了自己的思考,直接看會枯燥,甚至不知所云。今天突然想換種說明方式。為什麼會有HashMap這種結構,為了實現什麼目的?為什麼用這種結構?比其他結構的好在哪裡
HashMap 原始碼分析 1.8
之前看過JDK1.7的hashMap的原始碼,1.8在HashMap上做了不少改動,特找了相關文章,分享一下: 1.7版本的hashmap採用:陣列+連結串列; 1.7版本的hashmap採用:陣列+連結串列+紅黑樹; HashMap是Java和Android程式設
ArrayList原始碼分析--jdk1.8
ArrayList概述 1. ArrayList是可以動態擴容和動態刪除冗餘容量的索引序列,基於陣列實現的集合。 2. ArrayList支援隨機訪問、克隆、序列化,元素有序且可以重複。 3. ArrayList初始預設長度10
HashMap 原始碼詳細分析(JDK1.8)
本篇文章我們來聊聊大家日常開發中常用的一個集合類 - HashMap。HashMap 最早出現在 JDK 1.2中,底層基於雜湊演算法實現。HashMap 允許 null 鍵和 null 值,在計算哈鍵的雜湊值時,null 鍵雜湊值為 0。HashMap 並不保證鍵值對的順序,這意味著在進行某些操作
java集合之----HashMap原始碼分析(基於JDK1.7與1.8)
一、什麼是HashMap 百度百科這樣解釋: 簡而言之,HashMap儲存的是鍵值對(key和value),通過key對映到value,具有很快的訪問速度。HashMap是非執行緒安全的,也就是說在多執行緒併發環境下會出現問題(死迴圈) 二、內部實現 (1)結構 HashM
通俗易懂的JDK1.8中HashMap原始碼分析(歡迎探討指正)+ 典型面試題
面試題在最下面 說到HashMap之前,閱讀ArrayList與LinkedList的原始碼後做個總結 ArrayList 底層是陣列,查詢效率高,增刪效率低 LinkedList底層是雙鏈表,查詢效率低,增刪效率高 這裡只是總結看完原始碼後對hashm
JDK1.8 HashMap原始碼分析 ----轉載別人的,以後好複習。
本人看不懂原始碼,邏輯思維差,又懶。連看文件都喜歡跳字閱讀。所以只能去看別人寫的原始碼分析。也不知道能不能轉載。。所以直接貼個地址。 這是幾天下來,翻了好多篇部落格,發現寫的非常詳細,而且步驟和註釋寫的非常清晰的一篇了。。 大神好厲害。拜讀兩遍,以表敬意。 讀技
HashMap原始碼分析(JDK1.8)- 你該知道的都在這裡了
HashMap是Java和Android程式設計師的基本功, JDK1.8對HashMap進行了優化, 你真正理解它了嗎? 考慮如下問題: 1、雜湊基本原理?(答:散列表、hash碰撞、連結串列、紅黑樹)2、hashmap查詢的時間複雜度, 影響因素和原理?
jdk1.8 HashMap原始碼分析(resize函式)
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.len
java集合(4):HashMap原始碼分析(jdk1.8)
前言 Map介面雖然也是集合體系中的重要一個分支,但是Map介面並不繼承自Collection,而是自成一派。 public interface Map<K,V> Map集合儲存鍵對映到值的物件。一個集合中不能包含重複的鍵,每個鍵最多
HashMap原始碼分析(jdk1.8)
private static final long serialVersionUID = 362498820763181265L; //The default initial capacity - MUST be a power of two. static final
JDK1.8 HashMap原始碼分析
JDK1.8 HashMap原始碼分析 一、HashMap概述 在JDK1.8之前,HashMap採用陣列+連結串列實現,即使用連結串列處理衝突,同一hash值的連結串列都儲存在一個連結串列裡。但是當位於一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查
基於jdk1.8的HashMap原始碼分析(溫故學習)
鼎力推薦一下一. HashMap結構 HashMap在jdk1.6版本採用陣列+連結串列的儲存方式,但是到1.8版本時採用了陣列+連結串列/紅黑樹的方式進行儲存,有效的提高了查詢時間,解決衝突。這裡有一篇部落格寫的非常好,HashMap的結構圖也畫的非常清楚,鼎力推