1. 程式人生 > >Java8 HashMap詳解

Java8 HashMap詳解

Java8 HashMap

Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。

根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具體下標,但是之後的話,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決於連結串列的長度,為 O(n)

為了降低這部分的開銷,在 Java8 中,當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)

來一張圖簡單示意一下吧:
這裡寫圖片描述

注意,上圖是示意圖,主要是描述結構,不會達到這個狀態的,因為這麼多資料的時候早就擴容了。

下面,我們還是用程式碼來介紹吧,個人感覺,Java8 的原始碼可讀性要差一些,不過精簡一些。

Java7 中使用 Entry 來代表每個 HashMap 中的資料節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於連結串列的情況,紅黑樹的情況需要使用 TreeNode

我們根據陣列元素中,第一個節點資料型別是 Node 還是 TreeNode 來判斷該位置下是連結串列還是紅黑樹的。

put 過程分析

public V put(K key, V value) {
    return putVal(hash(key), key, value
, false, true); } // 第三個引數 onlyIfAbsent 如果是 true,那麼只有在不存在該 key 時才會進行 put 操作 // 第四個引數 evict 我們這裡不關心 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 也要初始化陣列長度
// 第一次 resize 和後續的擴容有些不一樣,因為這次是陣列從 null 初始化到預設的 16 或自定義的初始容量 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; // 如果該節點是代表紅黑樹的節點,呼叫紅黑樹的插值方法,本文不展開說紅黑樹 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 到這裡,說明陣列該位置上是一個連結串列 for (int binCount = 0; ; ++binCount) { // 插入到連結串列的最後面(Java7 是插入到連結串列的最前面) if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // TREEIFY_THRESHOLD 為 8,所以,如果新插入的值是連結串列中的第 9 個 // 會觸發下面的 treeifyBin,也就是將連結串列轉換為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 如果在該連結串列中找到了"相等"的 key(== 或 equals) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 此時 break,那麼 e 為連結串列中[與要插入的新值的 key "相等"]的 node break; p = e; } } // e!=null 說明存在舊值的key與要插入的key"相等" // 對於我們分析的put操作,下面這個 if 其實就是進行 "值覆蓋",然後返回舊值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 如果 HashMap 由於新插入這個值導致 size 已經超過了閾值,需要進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

和 Java7 稍微有點不一樣的地方就是,Java7 是先擴容後插入新值的,Java8 先插值再擴容,不過這個不重要。

陣列擴容

resize() 方法用於初始化陣列或陣列擴容,每次擴容後,容量為原來的 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;
        }
        // 將陣列大小擴大一倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 將閾值擴大一倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 對應使用 new HashMap(int initialCapacity) 初始化後,第一次 put 的時候
        newCap = oldThr;
    else {// 對應使用 new HashMap() 初始化後,第一次 put 的時候
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    // 用新的陣列大小初始化新的陣列
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 如果是初始化陣列,到這裡就結束了,返回 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 { 
                    // 這塊是處理連結串列的情況,
                    // 需要將此連結串列拆成兩個連結串列,放到新的陣列中,並且保留原來的先後順序
                    // loHead、loTail 對應一條連結串列,hiHead、hiTail 對應另一條連結串列,程式碼還是比較簡單的
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        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;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 第二條連結串列的新的位置是 j + oldCap,這個很好理解
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get 過程分析

相對於 put 來說,get 真的太簡單了。

  • 計算 key 的 hash 值,根據 hash 值找到對應陣列下標: hash & (length-1)
  • 判斷陣列該位置處的元素是否剛好就是我們要找的,如果不是,走第三步
  • 判斷該元素型別是否是 TreeNode,如果是,用紅黑樹的方法取資料,如果不是,走第四步
  • 遍歷連結串列,直到找到相等(==或equals)的 key
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;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (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;
}

相關推薦

Java8 HashMap

Java8 HashMap Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。 根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具

HashMap

== nan lba illegal 需要 我們 bar toolbar 它的 HashMap也是我們使用非常多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,

java程式設計思想讀書筆記三(HashMap

Map Map介面規定了一系列的操作,作為一個總規範它所定義的方法也是最基礎,最通用的。 AbstractMap AbstractMap是HashMap、TreeMap,、ConcurrentHashMap 等類的父類。當我們巨集觀去理解Map時會發現,其實Map就是一

HashMap

紅黑樹性質 紅黑樹是平衡二叉樹的一種, 但是它的平衡因子是可以大於 1 紅黑樹的節點要麼是紅色, 要麼是黑色, 這裡的紅黑色只是用來區分的一種方式, 為了定義規則 根節點一定是黑色 葉子節點也是黑色, 實際上葉子節點都是由 NULL 組成 紅色節點的子節點是黑色 根節點到葉

HashMap

連結串列轉樹結構 根據詳解四, 當連結串列長度大於 8 時, 為了更高效的查詢, 需要轉成紅黑樹結構, 使用的方法是 treeifyBin. 過程是先把連結串列結構調整為雙向連結串列結構, 再把雙向連結串列結構調整為紅黑樹結構. /** * tab: 陣列 * hash: 新節點 key 的雜

Java:hashMap

原 Java集合:HashMap詳解(JDK 1.8) 置頂 2018年01月07日 18:00:41 JoonWhee

HashMap( JDK8 之前與之後對比)

HashMap簡介 HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。 HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。 HashMap 的實現不是同步的,這意味著它不是執行緒安全的。它的k

hashMap與例項

在Java集合類中最常用的除了ArrayList外,就是HashMap了。本文儘自己所能,儘量詳細的解釋HashMap的原始碼。一山還有一山高,有不足之處請之處,定感謝指定並及時修正。     在看HashMap原始碼之前先複習一下資料結構。     Ja

java8 Stream

Stream組成 在傳統Java程式設計,或者說是類C語言程式設計中,我們如何操作一個數組資料呢?或者更泛化的講,我們如何操作一個“集合”(Collection)資料呢?在Java中我們利用java.util包裡的各種資料結構封裝,來很好的表示了陣列(Array)、

java中HashMap

上面方法的程式碼很簡單,但其中包含了一個非常優雅的設計:系統總是將新新增的 Entry 物件放入 table 陣列的 bucketIndex 索引處——如果 bucketIndex 索引處已經有了一個 Entry 物件,那新新增的 Entry 物件指向原有的 Entry 物件(產生一個 Entry 鏈),如果

面試題--HashMap

先上hashCode和equals原始碼: /** JNI,呼叫底層其它語言實現 */ public native int hashCode(); /** 默認同==,直接比較物件 */ public boolean equals(Object

Java集合(四)HashMap

HashMap簡介 java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> public class HashMap<K,V> ex

Java HashMap實現原理2——HashMap

博主的前兩篇文章Java HashMap實現原理0——從hashCode,equals說起,Java HashMap實現原理1——散列表已經講述了HashMap設計的知識點,包括:hashCode(),equals(),散列表結構,雜湊函式、衝突解決等,在散列表

Java8 ConcurrentHashMap

Java8 ConcurrentHashMap Java7 中實現的 ConcurrentHashMap 說實話還是比較複雜的,Java8 對 ConcurrentHashMap 進行了比較大的改動。建議讀者可以參考 Java8 中 HashMap 相對於

Java8 Optional

在Java8中新增了一個Optional類,官方描述是該類是一個容器物件,其中可能包含一個空或非空的值。如果存在一個值,isPresent()將返回true,get()將返回該值。 錯誤使用姿勢 簡單的根據描述,我們認為Optional可以幫我們解決NP

java8 lambda

一)輸入引數在Lambda表示式中,輸入引數是Lambda運算子的左邊部分。它包含引數的數量可以為0、1或者多個。只有當輸入引數為1時,Lambda表示式左邊的一對小括弧才可以省略。輸入引數的數量大於或者等於2時,Lambda表示式左邊的一對小括弧中的多個引數質檢使用逗號(,

Java8學習筆記(五)--Stream API[轉]

有效 編程效率 實時處理 phaser 綜合 files -- bin 並發模式 為什麽要使用StreamStream 作為 Java 8 的一大亮點,它與 java.io 包裏的 InputStream 和 OutputStream 是完全不同的概念。它也不同於 StAX

HashTable和HashMap的區別

body 線程安全 serializa javadoc cloneabl 允許 哈希 安全性 rac HashMap是基於哈希表實現的,每一個元素是一個key-value對,其內部通過單鏈表解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長。 HashMap

HashMap重點

映射 != ash lstat 放置 運算 畫圖 blog while     Map即映射表一般稱為散列表。開發中常用到這種數據結構,Java中HashMap和ConcurrentHashMap被用到的頻率較高,本文重點說下HashMap的實現原理以及設計思路。     

【Java】HashMap源碼分析——常用方法

fir 設置 直接 dfa 構造方法 change mage null 這也 上一篇介紹了HashMap的基本概念,這一篇著重介紹HasHMap中的一些常用方法:put()get()**resize()** 首先介紹resize()這個方法,在我看來這是HashMap中一個