1. 程式人生 > >JDK原始碼閱讀之HashMap

JDK原始碼閱讀之HashMap

HashMap簡介

基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。

重點:實現map介面,允許null值和null鍵(為什麼允許空值?因為空值呼叫方法會報空指標異常),不保證順序(為什麼不保證順序?)

此實現假定雜湊函式將元素適當地分佈在各桶之間,可為基本操作(get 和 put)提供穩定的效能。迭代 collection 檢視所需的時間與 HashMap 例項的“容量”(桶的數量)及其大小(鍵-值對映關係數)成比例。所以,如果迭代效能很重要,則不要將初始容量設定得太高(或將載入因子設定得太低)。

HashMap 的例項有兩個引數影響其效能:初始容量 和載入因子。容量 是雜湊表中桶的數量,初始容量只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。

通常,預設載入因子 (.75) 在時間和空間成本上尋求一種折衷。載入因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設定初始容量時應該考慮到對映中所需的條目數及其載入因子,以便最大限度地減少 rehash 操作次數。如果初始容量大於最大條目數除以載入因子,則不會發生 rehash 操作。

這三段話都在講HashMap的重要方法rehash,並且與之相關聯的變數,預設載入因子,初始容量,當前所佔容量(雜湊表中的條目數)

另外HashMap不是執行緒同步的,如果想要執行緒同步的map,最好在建立時完成這一操作,以防止對對映進行意外的非同步訪問,如下所示:

Map m = Collections.synchronizedMap(new HashMap(...));

迭代器也是使用fail-fast模式。

HashMap類圖

HashMap類圖

HashMap重要方法

方法變數

// 預設的初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 預設的載入因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 由連結串列轉換成樹的閾值
static final int TREEIFY_THRESHOLD = 8;
//由樹轉換成連結串列的閾值
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中的bin被樹化時最小的hash表容量
static final int MIN_TREEIFY_CAPACITY = 64;

構造方法

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

所有的構造方法的目的只有一個就是確定loadFactor 和threshold這兩個變數。

精華方法

putVal


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者長度為0,進行擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 確定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在陣列中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已經存在元素
    else {
        Node<K,V> e; K k;
        // 比較桶中第一個元素(陣列中的結點)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 將第一個元素賦值給e,用e來記錄
                e = p;
        // hash值不相等,即key不相等;為紅黑樹結點
        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;
                }
                // 判斷連結串列中結點的key值與插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出迴圈
                    break;
                // 用於遍歷桶中的連結串列,與前面的e = p.next組合,可以遍歷連結串列
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值與插入元素相等的結點
        if (e != null) { 
            // 記錄e的value
            V oldValue = e.value;
            // onlyIfAbsent為false或者舊值為null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替換舊值
                e.value = value;
            // 訪問後回撥
            afterNodeAccess(e);
            // 返回舊值
            return oldValue;
        }
    }
    // 結構性修改
    ++modCount;
    // 實際大小大於閾值則擴容
    if (++size > threshold)
        resize();
    // 插入後回撥
    afterNodeInsertion(evict);
    return null;
} 

resize

final Node<K,V>[] resize() {
    // 當前table儲存
    Node<K,V>[] oldTab = table;
    // 儲存table大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 儲存當前閾值 
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 之前table大小大於0
    if (oldCap > 0) {
        // 之前table大於最大容量
        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
    }
    // 之前閾值大於0
    else if (oldThr > 0)
        newCap = oldThr;
    // oldCap = 0並且oldThr = 0,使用預設值(如使用HashMap()建構函式,之後再插入一個元素會呼叫resize函式,會進入這一步)
    else {           
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新閾值為0
    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"})
    // 初始化table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 之前的table已經初始化過
    if (oldTab != null) {
        // 複製元素,重新進行hash
        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;
                    // 將同一桶中的元素根據(e.hash & oldCap)是否為0進行分割,分成兩個不同的連結串列,完成rehash
                    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;
}

重要方法

getNode

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // table已經初始化,長度大於0,根據hash尋找table中的項也不為空
    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;
}

putMapEntries

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 判斷table是否已經初始化
        if (table == null) { // pre-size
            // 未初始化,s為m的實際元素個數
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
            // 計算得到的t大於閾值,則初始化閾值
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 已初始化,並且m元素個數大於閾值,進行擴容處理
        else if (s > threshold)
            resize();
        // 將m中的所有元素新增至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

clone

public Object clone() {
        HashMap<K,V> result;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        result.reinitialize();
        result.putMapEntries(this, false);
        return result;
    }

從原始碼中可以看出clone方法雖然生成了新的HashMap物件,新的HashMap中的table陣列雖然也是新生成的,但是陣列中的元素還是引用以前的HashMap中的元素。這就導致在對HashMap中的元素進行修改的時候,即對陣列中元素進行修改,會導致原物件和clone物件都發生改變,但進行新增或刪除就不會影響對方,因為這相當於是對陣列做出的改變,clone物件新生成了一個數組。

HashMap變動

hashMap在jdk1.8之前:

HashMap的資料結構就是用的連結串列雜湊,將需要儲存的key和value轉變成Entry類 通過key、value封裝成一個entry物件,然後通過key的值來計算該entry的hash值,通過entry的hash值和陣列的長度length來計算出entry放在陣列中的哪個位置上面,如果這個位置上有其他的元素,則通過連結串列來儲存這個元素

jdk1.8之後

hashmap的資料結構是:陣列+連結串列+紅黑樹

HashMap閱讀感想

1)要知道hashMap在JDK1.8以前是一個連結串列雜湊這樣一個數據結構,而在JDK1.8以後是一個數組加連結串列加紅黑樹的資料結構。

2)通過原始碼的學習,hashMap是一個能快速通過key獲取到value值得一個集合,原因是內部使用的是hash查詢值得方法。

說明

本文是本人撰寫,如果本文讓你有些許收穫或感悟,我感到榮幸。如果對這篇文章有不同的意見或發現錯誤,歡迎留言糾正或者聯絡我:[email protected]