1. 程式人生 > >HashMap原始碼淺析(一)—— 建立物件與新增元素

HashMap原始碼淺析(一)—— 建立物件與新增元素

本文屬於個人隨筆,純原創,轉載請註明出處

最近面試,屢屢被問道HashMap的實現,回答得總是不太流暢。回頭想想,HashMap作為平時用得最多的一個集合框架類,我對其其內部實現確實知之甚少。這有點說不過去,於是把原始碼搬出來,嘗試做個簡單分析,如有謬誤,還請指出,謝謝!

(本文所示原始碼均來源於JDK1.8.0_131)

首先翻譯一下類宣告前的一大段註釋吧,只提取關鍵資訊:

1、HashMap是基於雜湊表的Map介面實現類(間接實現:直接繼承自AbstractMap,而AbstractMap實現了Map介面);

2、與HashTable相比,HashMap有兩個區別:其一是HashMap執行緒不安全,其二是HashMap允許鍵為null;

3、鑑於其內部實現是雜湊表,HashMap無法保證內部元素的順序始終不變;

4、影響HashMap讀寫效能的兩個要素:初始容量與載入因子;前者確定HashMap物件建立初期的容量,後者用於HashMap判斷是否達到需要擴容的臨界值(容量*載入因子);如果達到臨界值,HashMap會重建內部資料結構,以獲得較之前相比約兩倍的容量;

5、載入因子預設0.75,以獲得性能與容量的平衡,過高可能會導致讀寫耗時增加、過低可能會浪費過多記憶體;

6、HashMap的迭代器Iterator內建fail-fast機制,不允許在遍歷過程中更改內部資料結構;

7、儘管該類在JDK 1.2開始就有了,但是直到JDK 1.8,該類還進行過大改動,這也從側面反映了這個類的使用範圍之廣,以至於一丁點的效能提升都顯得格為重要;

首先還是來看構造方法:

    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);
    }

四個過載的構造方法,一一說明:

1、空參:載入因子預設0.75,沒有指定初始容量;

2、一個整形引數:指定初始容量、載入因子預設0.75;

3、兩個引數:指定初始容量和載入因子;

4、一個Map型別引數:載入因子預設0.75,並呼叫putMapEntries將引數中的鍵值對新增到HashMap中;

其中,前三個構造方法執行完成後都沒有建立用於存放鍵值對的容器,而第四個構造方法也只是在putVal的方法裡才會呼叫到resize()並建立Node<K,V>陣列;

這裡插一句,tableSizeFor這個方法用於判斷傳入的初始容量是否合法,寫得很牛逼,具體演算法大家直接嘗試看原始碼吧:

    /**
     * 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 >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

在兩個引數的構造方法中,我們注意到:

this.threshold = tableSizeFor(initialCapacity);

按照字面意思,這裡的threshold應該存放載入因子乘以容量的臨界值,為何在此存的是初始容量的校準值(姑且這麼叫吧,就是通過tableSizeFor方法校準過的返回值)?先表明態度——這不是bug,至於為啥,設為疑問A,稍後自會分曉;

既然構造方法本身都不會建立容器,那麼一定和第四個構造方法一樣,我們的容器一定就是在新增第一個元素的時候建立的,來到最常用的put(K key, V value)方法:

    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;
            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) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

和上方的第四個構造方法一樣,會呼叫到final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法中,假設這個HashMap在構造完成後立即進行了put元素的操作,我們可以將構造方法的值直接代入,檢視程式碼走向:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        n = (tab = resize()).length;
        i = (n - 1) & hash;
        tab[i] = newNode(hash, key, value, null);
        ++modCount;
        if (++size > threshold)
            resize();
        return null;
    }

簡化後的程式碼如上,我們可以看到,首先會執行resize()方法:

    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) // 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);
        }
        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;
                            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;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

看到這裡,可以解答一下之前的疑問A了:

將初始容量存入threshold成員變數是為了在resize()方法中做判斷,再將其賦值與newCap變數,用於建立容器時使用,說白了,就是一個記錄臨時變數的作用。

好了,回來繼續看resize()方法,同樣我們按照之前的邏輯將方法簡化:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        newCap = oldThr;
        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;
        return newTab;
    }

可以看出來,初始化容器其實並沒有做太多事情,主要是一些變數間的賦值和一個Node陣列的建立,而這個Node陣列,則是HashMap的內部資料結構;

我們從而得到結論1:HashMap是通過內部維護一個鍵值對陣列來實現的。

基於結論1,我們肯定會問,這跟雜湊有啥關係?

別急,我們只是看了resize()方法,而呼叫它的putVal方法我們還沒看完,於是我們回到putVal方法,來看看resize之後又做了什麼:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        n = (tab = resize()).length;
        i = (n - 1) & hash;
        tab[i] = newNode(hash, key, value, null);
        ++modCount;
        if (++size > threshold)
            resize();
        return null;
    }

關鍵程式碼出現了:

首先,陣列的下標i是通過陣列長度-1再位與上hash值而來;

其次,建立一個Node物件存放鍵值對於雜湊值,並將其賦值給tab[i]。如果說HashMap內部是一個數組,那麼往這個數組裡面的某個元素賦值,則就是往HashMap裡面新增元素的關鍵所在了;

不急,一個一個來。

首先,我們可能需要回頭看下這個整形引數hash的來歷:

翻遍整個HashMap.java,我們看到呼叫putVal並傳入的第一個引數,始終都是通過hash(key)獲得的;

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

再次宣告,本文程式碼基於JDK1.8.0_131,每個版本的方法實現可能不一樣。不過我們暫時不深入瞭解這個,不然就沒完沒了了,我們只需要知道,putVal方法的第一引數是通過HashMap內部的hash方法,對需要put的key進行雜湊運算得到的。

那麼利用運算得到的雜湊值hash位與上鍵值對陣列的最大下標(length - 1)所得的數值i,就是這次putVal操作需要新增的鍵值對存放在內部陣列的位置;

如果每次新增元素的操作都如我簡化後的程式碼這樣執行,那麼這個HashMap也就不會那麼備受推崇了,甚至你會覺得求hash變得沒那麼必要。別急,作為Map的實現類,乃至整個集合框架內使用頻率最高的HashMap,絕對沒那麼簡單。

這一篇先寫到這,本來打算一口氣寫完,不過感覺這樣會很亂,也很冗長。不如在此停頓一下,預計明天再開一篇,跟下putVal的其他幾個if 、else if;

相關推薦

HashMap原始碼淺析—— 建立物件新增元素

本文屬於個人隨筆,純原創,轉載請註明出處 最近面試,屢屢被問道HashMap的實現,回答得總是不太流暢。回頭想想,HashMap作為平時用得最多的一個集合框架類,我對其其內部實現確實知之甚少。這有點說不過去,於是把原始碼搬出來,嘗試做個簡單分析,如有謬誤,還請指出,謝謝!

Android Hook框架adbi原始碼淺析

adbi(The Android Dynamic Binary Instrumentation Toolkit)是一個Android平臺通用hook框架,基於動態庫注入與inline hook技術實現。該框架由兩個主要模組構成,1.hijack負責將動態庫注入到目標程序;2.libbase提供動態庫本身,它實

Android廣播機制實現原始碼淺析

Android系統的廣播機制應用非常的廣泛,是一種方便快捷的程序間通訊的方式。同時它也有一些很有特殊的使用方式,比如它的兩種註冊方式,三種類型的廣播等,這些充斥在整個系統框架中,那麼為了用好廣播,很有必要對其原始碼進行分析,從而避免一些低階失誤。 本文將對整個廣播機制涉及到

tomcat原始碼淺析之 架構

     tomcat程式碼看似很龐大,但從結構上看卻很清晰和簡單,它主要由一堆元件組成,如Server、Service、Connector等,並基於JMX管理這些元件,另外實現以上介面的元件也實現了代表生存期的介面Lifecycle,使其元件履行

zookeeper原始碼淺析

1.基本架構  2.ZAB協議    ZooKeeper並沒有完全採用Paxos演算法,而是使用了一種稱為ZooKeeper Atomic Broadcast(ZAB,zookeeper原子訊息廣播協議)的協議作為其資料一致性的核心演算法。 &n

原始碼分析:Android Okhttp原始碼淺析

Okhttp的呼叫 分析原始碼,我們都需要通過呼叫來檢視原始碼的每一步都做了什麼事情。 看下非同步呼叫 OkHttpClient client = new OkHttpClient(); Request request = n

JavaScript基礎——面向物件的程式設計建立物件的幾種方式總結

簡介 面向物件(Object-Oriented, OO)的語言有一個標誌,那就是它們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的物件。前面提到過,ECMAScript中沒有類的概念,因此它的物件也與基於類的語言中的物件有所不同。 ECMA-262把物件定義為:

Zookeeper客戶端原始碼分析建立連線

本文基於zookeeper-3.4.14,由於zookeeper的很多構造方法都是呼叫的另一個構造方法,所以分析程式碼的時候直接分

Volley原始碼解析——傳送請求結束請求

Volley是一個Android HTTP庫,只支援非同步方式。 傳送請求樣例 final TextView mTextView = (TextView) findViewById(R.id.text); ... // Instantiate

林大媽的JavaScript進階知識物件記憶體

JavaScript中的基本資料型別 在JS中,有6種基本資料型別: string number boolean null undefined Symbol(ES6) 除去這六種基本資料型別以外,其他的所有變數資料型別都是Object。基本型別的操作在JS底層中是這樣實現的: // 1. 申請一塊記憶體,

篇文章徹底讀懂HashMapHashMap原始碼解析

就身邊同學的經歷來看,HashMap是求職面試中名副其實的“明星”,基本上每一加公司的面試多多少少都有問到HashMap的底層實現原理、原始碼等相關問題。 在秋招面試準備過程中,博主閱讀過很多關於HashMap原始碼分析的文章,漫長的拼湊式閱讀之後,博主沒有看到過

Java容器——HashMapJava8原始碼解析

一 概述 HashMap是最常用的Java資料結構之一,是一個具有常數級別的存取速率的高效容器。相對於List,Set等,結構相對複雜,本篇我們先對HashMap的做一個基本說明,對組成元素和構造方法進行介紹。 二 繼承關係 首先看HashMap的繼承關係,比較簡單,實現了Map和序列化

java程式碼優化——建立和銷燬物件

用靜態工廠方法代替構造器 準備知識 自動裝箱:從基本資料型別轉換成包裝型別。 自動拆箱:從包裝型別轉換成基本資料型別。 包裝類: number(數字型別) Byte(byte) Short(short) Integer(int) Long(long) Dou

Rxjava2原始碼分析:Flowable的建立和基本使用過程分析

本文用於記錄一下自己學習Rxjava2原始碼的過程。首先是最簡單的一個使用方法(未做執行緒切換),用來學習Flowable的建立和使用。Flowable .create(new FlowableOnSubscribe<Object>() {

物件池commons-pool框架的研究以及原始碼分析

    物件池是一個物件集合,用於將建立好的物件存在該集合中,當需要使用池中的物件時,再從池中取出,恰當地使用物件池可以有效減少物件生成和初始化時的消耗,提高系統的執行效率。另外,利用物件池還可以對物件的狀態做一定的維護,確保物件是可用的,提高程式的健壯性。注意:物件池技術

seajs原始碼分析-執行機制淺析

前端技術發展簡直是日新月異,隨著angularjs,vuejs,reactjs等等這些框架的不斷興起,轉眼間jquery,seajs,Backbone這些框架已經成了清朝的框架了,再加上es6本身對於模組化的支援,也許,seajs模組化在將來的某天可能會徹底成為

Python 原始碼剖析【python物件

處於研究python記憶體釋放問題,在閱讀部分python原始碼,順便記錄下所得。 (基於《python原始碼剖析》(v2.4.1)與 python原始碼(v2.7.6)) 先列下總結:         python 中一切皆為物件,所以會先講明白pyth

JS面向物件實戰——建立一個新的函式物件的兩種習慣

JavaScript面向物件(一)——建立一個新的函式物件的兩種習慣 工作中可能習慣建立一個函式物件的方式,管理一個模組。那麼針對建立一個函式物件,一般有兩種習慣方式:偏向鏈式程式設計;偏向類 不能汙染函式祖先Function。 我們如果想新增一個方法

面向物件程式設計建立類,例項化,屬性引用

  在python中,用變量表示特徵,用函式表示技能,因而類是變數與函式的結合體,物件是變數與方法(指向類的函式)的結合體。 class 類名:#定義一個類 類體   類有兩種作用:屬性引用和例項化 t=類名()#例項化 類名加括號就是例項化,會自動觸發__in

Qt整體框架淺析 -物件模型

最近在膠著於QT的框架以及其核心, 本文作為學習的總結,如果有誤,望指正。    首先介紹一下Qt的整體框架,Qt作為一個GUI的解決方案,其被設計為基於面向物件,跨平臺,並直接與底層介面的framework,下圖為主要的層次結構:     下邊將從物件模型,事件機制