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

HashMap原始碼分析(上)

HashMap是一種散列表,通過陣列加連結串列的方式實現。在JDK1.8版本後新增加了紅黑樹結構來提高效率。

HashMap

HashMap是Java的Map介面的一種基礎雜湊表實現。它提供了Map的所有操作,並且支援以null為key或value。(除了是非同步的以及支援null以外,HashMap與HashTable基本相同)HashMap是無序的,即每次遍歷的順序都可能不同。

假如hash演算法將每個元素均勻的分散在不同的桶空間(bucket)中,那麼HashMap的get和put方法的時間效能將會是常量級別(O(1))。迭代HashMap所需的時間與其容量(桶空間的數量)和大小(鍵值對的數量)之和成正比。所以,如果迭代效能比較重要,最好不要將初始容量設定的太大或負載因子設定的過小。

一個HashMap例項的效能將受到兩個引數的影響,分別是初始容量(initial capacity)和負載因子(load factor)。容量就是HashMap中桶空間的數量,顯然初始容量就是HashMap被初始化時的容量。負載因子則是衡量什麼時候HashMap的容量會被自動增加。當HashMap的大小達到容量和負載因子的乘積時,HashMap將會rehash(內部資料結構將會重新構建),rehash之後HashMap的容量大約是之前的兩倍。

一般來說,預設的負載因子(0.75)較好的平衡了時間複雜度和空間複雜度。調高負載因子的值可以減少所需的空間大小但是會增加查詢耗時(影響到HashMap的許多操作,包括常用的get和put)。在初始化HashMap時應該根據預期要儲存的鍵值對數量和期望的負載因子的值來設定一個合適的初始容量,從而儘量減少rehash的次數。如果設定的初始容量大於鍵值對可能的最大數量值除以負載因子,那麼rehash將永遠不會發生。

如果一個HashMap例項要儲存大量的鍵值對,那麼設定一個足夠大的初始容量會比讓它自動rehash擴容儲存效率更高。注意,不論是哪一種雜湊表,如果大量的key的hash值相同,那麼其效率一定會降低。為了減輕這種影響,當key實現了Comparable介面,HashMap會利用key之間的比較順序來幫助打破聯絡(紅黑樹節點時有用)。

一定要注意HashMap不是同步的,即非執行緒安全。如果多個執行緒併發的訪問同一個HashMap,並且有一個執行緒及以上的執行緒在結構上修改這個HashMap,那麼一定要在外部保證同步訪問。(在結構上修改是指新增或刪除一個或多個鍵值對,而不是僅僅是修改一個已經存在的key對應的value。)一種經典的做法是在封裝了Map的物件中進行同步。

如果沒有這樣的物件,那麼應該使用Collections.synchronizedMap()方法來包裝Map。最好在建立物件時就這麼做,從而避免意料之外的非同步訪問。

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

任何方式獲得的HashMap的迭代器在建立後,都會由於除了當前迭代器本身的remove方法之外導致的對應例項發生結構上的改變而發生fail-fast,此時迭代器將丟擲ConcurrentModificationException異常。也就是說,當發生併發修改時迭代器會乾脆利落的停止執行,而不是留下一個未知的風險。

注意迭代器的fail-fast行為不能保證一定會觸發,因為一般而言,無法絕對確認非同步的併發修改的發生。迭代器只能儘可能地丟擲ConcurrentModificationException異常。因此依賴這個異常的程式可能會發生錯誤。迭代器的fail-fast行為應該只被用來發現bug。

預設引數

/**
 * 預設的初始容量,必須是2的冪。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大容量,當試圖設定一個大於該值的容量時HashMap會在內部使用該值。設定容量必須是2的冪且小於等於2的30次方。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 負載因子的預設值。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 以list形式儲存桶節點數量的上限,達到(大於等於)這個上限後再繼續新增節點就會轉換成紅黑樹的形式儲存。這個值 
 * 必須大於2並且應該大於8,與UNTREEIFY_THRESHOLD的值有一定關係,要大於UNTREEIFY_THRESHOLD。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 以紅黑樹形式儲存桶節點數量的下限,低於這個下限則轉換成list的形式儲存。這個值應該小於TREEIFY_THRESHOLD,
 * 並且最大為6。
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 桶可以被轉換為紅黑樹結構的最小容量,如果HashMap當前容量小於這個值,那麼即使一個桶中的節點數量超過
 * TREEIFY_THRESHOLD也不會被轉換為紅黑樹結構,而是會發生rehash。為了避免,應該至少是TREEIFY_THRESHOLD
 * 的4倍,
 */
static final int MIN_TREEIFY_CAPACITY = 64;

節點

/**
 * 雜湊表的基礎桶節點,大部分鍵值對都是這個型別。(HashMap中的TreeNode和LinkedHashMap中的Entry都是它的
 * 子類)
 */
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;
    }
}

內部靜態方法

/**
 * 計算key.hashCode()並將結果的高16位與低16位進行異或。因為桶陣列(table)的大小為2的冪,所以只有部分bit
 * 位是有效的。(如當前容量為16,那麼只有後4位有效)眾所周知的例子是所有的float數都可以作為key儲存在一個小容
 * 量的HashMap中。所以將高位的影響向下傳遞。這是一種折衷了速度、實用性和質量的方法。因為許多常見的hash值已經
 * 很好的分散了(所以不會從該方法獲益),並且當桶中衝突嚴重時會轉換為紅黑樹來處理,所以採用了高位地位相異或這
 * 種性能消耗很低的方法來儘可能的避免系統性的缺陷,也是為了讓高位也能產生作用,否則由於HashMap容量的限制,高
 * 位數將永遠不會被用到。
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 * 如果x類定義具有"class C implements Comparable<C>"的形式,那麼返回x的Class,否則返回null
 */
static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; ParameterizedType p;
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        if ((ts = c.getGenericInterfaces()) != null) {
            for (Type t : ts) {
                if ((t instanceof ParameterizedType) &&
                    ((p = (ParameterizedType) t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}

/**
 * 如果x不等於null且是kc類的例項,返回k.compareTo(x)的結構,否則返回0。
 */
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}

/**
 * 根據給定的cap返回一個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;
}

屬性

/**
 * 桶陣列,首次使用時進行初始化並且會在必要時進行rehash來擴容。當對其初始化時,length永遠是2的冪。(在某些 
 * 操作中,length可以為0,便於懶載入。)
 */
transient Node<K,V>[] table;

/**
 * 快取entrySet()方法的結果,注意keySet()和values()方法的快取結果使用的是AbstractMap中的屬性keySet和
 * values。
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * HashMap中鍵值對的數量。
 */
transient int size;

/**
 * 當前HashMap例項被結構化修改的次數。結構化修改是指改變HashMap中鍵值對的數量或者修改了內部結構(例如
 * rehash)。這個屬性被用來實現迭代器的fail-fast(參考 ConcurrentModificationException)。
 */
transient int modCount;

/**
 * 當鍵值對數量超過這個值之後HashMap就會進行rehash操作(容量 * 負載因子)。注意,當桶陣列未被初始化時這個值 
 * 等於初始容量,當未顯示指定HashMap的初始容量時這個值為0(代表預設初始容量DEFAULT_INITIAL_CAPACITY)。
 *
 * @serial
 */
int threshold;

/**
 * 雜湊表的負載因子。
 *
 * @serial
 */
final float loadFactor;

構造方法

/**
 * 以給定的初始容量和負載因子構造一個空的HashMap。
 *
 * @param  initialCapacity 初始容量。
 * @param  loadFactor      負載因子。
 * @throws IllegalArgumentException 如果initialCapacity是負數或loadFactor是非正數則會丟擲
 * IllegalArgumentException。
 */
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);
}

/**
 * 以給定的初始容量和預設的負載因子(0.75)構造一個空的HashMap。
 *
 * @param  initialCapacity 初始容量
 * @throws IllegalArgumentException 如果initialCapacity是負數或loadFactor是非正數則會丟擲
 * IllegalArgumentException。
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 以預設的初始容量(16)和預設的負載因子(0.75)構造一個空的HashMap。
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 構造一個與給定Map具有相同的鍵值對的HashMap。(注:新Map中對應的key和value指向同一個物件)
 * 新構造的HashMap的負載因子為預設的負載因子(0.75),初始容量則是足夠大的值,可以儲存給的Map的所有鍵值對。
 *
 * @param   m 要儲存的鍵值對所屬的Map。
 * @throws  NullPointerException 當m為null時丟擲該異常。
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

基本方法

put方法

/**
 * 將給定的key和value關聯起來並存儲到HashMap中。如果已經存在給定key的鍵值對,舊value將被新value替換。
 *
 * @param key 給定的value將要與之相關聯。
 * @param value 將要與key相關聯。
 * @return 與key相關聯的舊value, 如果沒有則返回null。當然返回null也可能是與key相對應的舊value就是
 * null。
 */
public V put(K key, V value) {//懶得翻譯
    return putVal(hash(key), key, value, false, true);
}

/**
 * Map.put及相關方法的實現。
 *
 * @param hash key的hash值
 * @param key 要操作的key值
 * @param value 要操作的value值
 * @param onlyIfAbsent 如果是true,則不更新舊值(即只能進行插入操作)。
 * @param evict 如果是false則代表是建立模式。
 * @return 舊的value值,如果沒有則返回null。
 */
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)//根據hash找到key在桶陣列中的索引,p為第一個節點
        tab[i] = newNode(hash, key, value, null);//當前桶為空,直接插入節點
    else {//當前桶不為空
        Node<K,V> e; K k;//注意這個e,代表桶中與key對應的節點,e!=null則表示要插入的鍵值對已經存在。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//p就是key對應的節點
            e = p;
        else if (p instanceof TreeNode)//p是紅黑樹節點,根據紅黑樹相關方法找到key對應節點
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//p是普通連結串列節點
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {//當前桶中無key對應節點則插入新節點
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) //當前桶中數量大於等於TREEIFY_THRESHOLD則轉為紅黑樹儲存
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))//找到key對應節點,退出迴圈
                    break;
                p = e;
            }
        }
        if (e != null) { // 要插入的鍵值對已經存在,更新value,並返回舊的value
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);//修改之後的回撥,HashMap中沒有任何操作。
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)//檢查擴容
        resize();
    afterNodeInsertion(evict);//插入之後的回撥,HashMap中沒有任何操作。
    return null;
}

get方法

/**
 * 返回HashMap中給定key對應的value,沒有則返回null。
 *
 * 嚴格來說, 如果HashMap中有任意一對鍵值對key為k,value為v,使(key==null ? k==null : 
 * key.equals(k))為true那麼返回v,否則返回null。
 *
 * 返回值為null並不一定代表HashMap中沒有給定key的鍵值對,也可能是給定key對應的value就是null。可以使用
 * containsKey方法來區分這兩種情況。
 *
 * @see #put(Object, Object)
 */
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * Map.get及相關方法的實現。
 *
 * @param hash key的hash值。
 * @param key 指定的key。
 * @return 返回key對應的鍵值對,沒有則返回null。
 */
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) {//桶陣列及key雜湊對映對應的桶不為空則繼續查詢。
        if (first.hash == hash && // 總是檢查第一個節點,是要查詢的則直接返回。
            ((k = first.key) == key || (key != null && key.equals(k))))//對應key相同就返回。
            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;
}

remove方法

/**
 * 如果給定key對應的鍵值對存在則刪除該鍵值對。
 *
 * @param  key 要刪除的鍵值對的key。
 * @return 返回舊的value值,如果不存在則返回null。返回值為null不一定表示key對應的鍵值對不存在,也可能是舊
 * 的value為null。
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * Map.remove及相關方法的實現。
 *
 * @param hash key的hash值
 * @param key 要操作的key
 * @param value 如果matchValue為false則沒有意義。
 * @param matchValue 如果是true則只有在key對應的value與給定value相同才刪除。
 * @param movable 如果是false則刪除時不移動其他節點。(紅黑樹節點專用)
 * @return 返回key對應節點,不存在則返回null。
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;//注意這個p
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {//桶陣列及key雜湊對映對應的桶不為空則繼續查詢。
        Node<K,V> node = null, e; K k; V v;//如果查到要刪除的節點,這個node就指向要刪除的節點。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//第一個節點就是要刪除的。
            node = p;//p和node指向要刪除的節點。
        else if ((e = p.next) != null) {//第一個不是,繼續查詢。
            if (p instanceof TreeNode)//紅黑樹節點則呼叫對應方法查詢。
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {//普通連結串列則迴圈查詢。
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;//p指向要刪除的節點的前一個節點。
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)//要刪除的節點為紅黑樹節點則呼叫相關方法刪除。
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)//p和node都指向要刪除的節點(首節點),首節點指向原首節點的下一個節點。
                tab[index] = node.next;
            else//p指向要刪除的節點的前一個節點,將p的next指向要刪除的節點的next。
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);//刪除節點回調方法,HashMap中無操作。
            return node;
        }
    }
    return null;
}

resize方法

/**
 * 初始化或翻倍HashMap的大小.  如果table為null根據threshold或預設值代表的初始容量來分配記憶體。否則,節點 
 * 要麼儲存在原索引位置,要麼移動指定的偏移量後儲存到新table中。
 *
 * @return the table
 */
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) {//oldCap > 0代表是擴容即發生了rehash
        if (oldCap >= MAXIMUM_CAPACITY) {//oldCap達到最大值,threshold賦值為int最大值,不rehash
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)//容量翻倍後小於最大值且oldCap大於等於預設初始容量(這裡oldCap要大於DEFAULT_INITIAL_CAPACITY?)
            newThr = oldThr << 1; // newThr翻倍
    }
    else if (oldThr > 0) // 未初始化且手動設定了初始容量,threshold就是初始容量(此時為初始化)
        newCap = oldThr;
    else {               // oldCap和threshold都為0代表使用預設值(此時為初始化操作)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {//計算新的threshold
        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)//當前桶中只有一個節點(即沒發生hash衝突)
                    newTab[e.hash & (newCap - 1)] = e;//重新計算索引並插入新位置
                else if (e instanceof TreeNode)//如果是紅黑樹節點則呼叫split方法處理
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 普通連結串列節點,保持原來的順序
                    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) {
//這裡由於HashMap擴容是擴大2倍,所以(e.hash & oldCap) == 0則節點e仍儲存與原位置即可(等同於儲存到e.hash & (newCap - 1))
                            if (loTail == null)//指向頭節點,方便賦值
                                loHead = e;
                            else
                                loTail.next = e;//保留原順序
                            loTail = e;
                        }
                        else {
//(e.hash & oldCap) != 0則節點e要儲存到原位置加偏移量oldCap(等同於儲存到e.hash & (newCap - 1))
                            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;
}

點選訪問我的個人博