閱讀源碼,HashMap回顧


本文一是總結前面兩種集合,補充一些遺漏,再者對HashMap進行簡單介紹。java

回顧

由於前兩篇ArrayList和LinkedList都是針對單獨的集合類分析的,只見樹木未見森林,今天分析HashMap,能夠結合起來看一下java中的集合框架。下圖只是一小部分,並且爲了方便理解去除了抽象類。node

集合體系

Java中的集合(有時也稱爲容器)是爲了存儲對象,並且多數時候存儲的不止一個對象。數組

能夠簡單的將Java集合分爲兩類:框架

  • 一類是Collection,存儲的是獨立的元素,也就是單個對象。細分之下,常見的有List,Set,Queue。其中List保證按照插入的順序存儲元素。Set不能有重複元素。Queue按照隊列的規則來存取元素,通常狀況下是「先進先出」。函數

  • 一類是Map,存儲的是「鍵值對」,經過鍵來查找值。好比現實中經過姓名查找電話號碼,經過身份證號查找我的詳細信息等。工具

    理論上說咱們徹底能夠只用Collection體系,好比將鍵值對封裝成對象存入Collection的實現類,之因此提出Map,最主要的緣由是效率。this

HashMap簡介

HashMap用來存儲鍵值對,也就是一次存儲兩個元素。在jdk1.8中,其實現是基於數組+鏈表+紅黑樹,簡單說就是普通狀況直接用數組,發生哈希衝突時在衝突位置改成鏈表,當鏈表超過必定長度時,改成紅黑樹。debug

能夠簡單理解爲:在數組中存放鏈表或者紅黑樹3d

  1. 徹底沒有哈希衝突時,數組每一個元素是一個容量爲1的鏈表。如索引0和1上的元素。
  2. 發生較小哈希衝突時,數組每一個元素是一個包含多個元素的鏈表。如索引2上的元素。
  3. 當衝突數量超過8時,數組每一個元素是一棵紅黑樹。如索引6上的元素。

下圖爲示意圖,相關結構沒有嚴格遵循規範。code

HashMap的存儲結構

類簽名

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

以下圖

HashMap繼承體系

實現Cloneable和Serializable接口,擁有克隆和序列化的能力。

HashMap繼承抽象類AbstractMap的同時又實現Map接口的緣由一樣見上一篇LinkedList。

常量

//序列化版本號
private static final long serialVersionUID = 362498820763181265L;

//默認初始化容量爲16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//最大容量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認負載因子,值爲0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;


//如下三個常量應結合看
//鏈表轉爲樹的閾值
static final int TREEIFY_THRESHOLD = 8;

//樹轉爲鏈表的閾值,小於6時樹轉鏈表
static final int UNTREEIFY_THRESHOLD = 6;

//鏈表轉樹時的集合最小容量。只有總容量大於64,且發生衝突的鏈表大於8才轉換爲樹。
static final int MIN_TREEIFY_CAPACITY = 64;

上述變量的關鍵在於鏈表轉樹和樹轉鏈表的時機,綜合看:

  • 當數組的容量小於64是,此時無論衝突數量多少,都不樹化,而是選擇擴容。
  • 當數組的容量大於等於64時,
    • 衝突數量大於8,則進行樹化。
    • 當紅黑樹中元素數量小於6時,將樹轉爲鏈表。

變量

//存儲節點的數組,始終爲2的冪
transient Node<K,V>[] table;

//批量存入時使用,詳見對應構造函數
transient Set<Map.Entry<K,V>> entrySet;

//實際存放鍵值對的個數
transient int size;

//修改map的次數,便於快速失敗
transient int modCount;

//擴容時的臨界值,本質是capacity * load factor
int threshold;

//負載因子
final float loadFactor;

數組中存儲的節點類型,能夠看出,除了K和Value外,還包含了指向下一個節點的引用,正如一開始說的,節點實際是一個單向鏈表。

static class Node<K,V> implements Map.Entry<K,V> {
    
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    
   //...省略常見方法
}

構造方法

常見的無參構造和一個參數的構造很簡單,直接傳值,此處省略。看一下兩個參數的構造方法。

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;
    //將給定容量轉換爲不小於其自身的2的冪
    this.threshold = tableSizeFor(initialCapacity);
}

tableSizeFor方法

上述方法中有一個很是巧妙的方法tableSizeFor,它將給定的數值轉換爲不小於自身的最小的2的整數冪。

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

好比cap=10,轉換爲16;cap=32,則結果仍是32。用了位運算,保證效率。

有一個問題,爲啥非要把容量轉換爲2的冪?以前講到的ArrayList爲啥就不須要呢?其實關鍵在於hash,更準確的說是轉換爲2的冪,必定程度上減少了哈希衝突。

關於這些運算,畫個草圖很好理解,關鍵在於可以想到這個方法很牛啊。解釋的話配圖太多,這裏篇幅限制,將內容放在另外一篇文章。

添加元素

在上面構造方法中,咱們沒有看到初始化數組也就是Node<K,V>[] table的狀況,這一步驟放在了添加元素put時進行。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

能夠看出put調用的是putVal方法。

putVal方法

在此以前回顧一下HashMap的構成,數組+鏈表+紅黑樹。數組對應位置爲空,存入數組,不爲空,存入鏈表,鏈表超載,轉換爲紅黑樹。

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;
    //根據key計算hash值得出數組中的位置i,位置i上爲空,直接添加。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //數組對應位置不爲空
    else {
        Node<K,V> e; 
        K k;
        //對應節點key上的key存在,直接覆蓋value
        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) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    ++modCount;
    //下次添加前需不須要擴容,若容量已滿則提早擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize()方法比較複雜,最好是配合IDE工具,debug一下,比較容易弄清楚擴容的方式和時機,若是幹講的話反而容易混淆。

獲取元素

根據鍵獲取對應的值,內部調用getNode方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法

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 && 
            ((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;
}

總結

HashMap的內容太多,每一個內容相關的知識點也不少,篇幅和我的能力限制,很難講清全部內容,好比最基礎的獲取hash值的方法,其實也很講究的。有機會再針對具體的細節慢慢詳細寫吧。

相關文章
相關標籤/搜索