1. 程式人生 > >HashMap 原始碼分析(JDK1.8)

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.8HashMap原始碼分析(歡迎探討指正)+ 典型面試題

面試題在最下面 說到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.8HashMap原始碼分析(溫故學習)

鼎力推薦一下一. HashMap結構      HashMap在jdk1.6版本採用陣列+連結串列的儲存方式,但是到1.8版本時採用了陣列+連結串列/紅黑樹的方式進行儲存,有效的提高了查詢時間,解決衝突。這裡有一篇部落格寫的非常好,HashMap的結構圖也畫的非常清楚,鼎力推