1. 程式人生 > >Java容器——HashMap(Java8)原始碼解析(一)

Java容器——HashMap(Java8)原始碼解析(一)

一 概述

HashMap是最常用的Java資料結構之一,是一個具有常數級別的存取速率的高效容器。相對於List,Set等,結構相對複雜,本篇我們先對HashMap的做一個基本說明,對組成元素和構造方法進行介紹。

二 繼承關係

首先看HashMap的繼承關係,比較簡單,實現了Map和序列化等。

                                                                       圖1 HashMap繼承關係圖

HashMap繼承自Map,Map作為一個重要的介面,很有必要需要介紹一下。

                                                                       圖2 Map介面

 Map介面定義了一些通用方法,包括插入,刪除,替換,遍歷元素等常規集合方法。這裡有必要重要關注的有:

1 Entry介面:

Entry<K,V>是Map元素的組成形式,它是一個鍵值對,Key是Map的索引,Value是儲存的元素。由於Key是為了便於快速查詢,並且能夠唯一標識,所以推薦使用不變類,如String來做鍵,若是自己實現的類,則必須重寫hashcode和equals方法。Entry是Map元素的組成形式,可以並且只能通過iterator來遍歷。

2 forEach方法:

    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch (IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

注意著是一個介面中的default方法。Java自1.8以後支援在介面中實現預設方法,不同於抽象方法,子類必須重新實現,default方法是開箱即用。這裡可以看到,forEach方法是對Entry進行遍歷並且執行指定操作。

三 組成元素

3.1 類例項變數

    /**
     * Entry陣列
     */
    transient Node<K,V>[] table;

    /**
     * Entry集合
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * Entry的數量
     */
    transient int size;

    /**
     * HashMap被修改的次數
     */
    transient int modCount;

    /**
     * 擴容閾值
     *
     */
    int threshold;

    /**
     * HashMap的裝載因子
     *
     */
    final float loadFactor;

可以看到,這其中的變數和HashTable中基本一致,事實上,HashMap就是HashTable的去同步鎖以及提升單節點效率的優化版。為何需要提生效率,一方面同步操作沒有必要使用synchronisd這種重量級鎖,另一方面,HashTable的設計方式可能會發生效能機具下降。

需要注意的變數關係是capability * loadFactor = threshold。翻譯一下就是HashMap的擴容閾值是當前容量乘以承載因子。這個閾值不是table中的下標數量,而是整個HashMap已經裝載的元素。

                                                                圖2 HashTable出現極端Key碰撞

當HashTable的Key碰撞了以後,會在單一Node節點處形成單向連結串列。所以假設Key選取的不是很合適,衝突很多,HashTable就退化成LinkedList了,查詢效率和插入效率都劇烈下降,這也背離了設計的初衷。                         

 HashMap要如何解決這個問題呢,可以從下面定義的變數中一窺一二。

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

為了避免出現HashMap變連結串列,HashMap引入了紅黑樹。紅黑樹是一種儘量保持平衡的搜尋二叉樹,簡單而言對紅黑樹的增刪改查都可以再O(lgn)時間內完成,較連結串列的O(n)有了巨大的提升,詳細瞭解見紅黑樹維基百科

這些變數也說明了連結串列與紅黑樹相互轉化的條件:

1 當連結串列長度超過TREEIFY_THRESHOLD時,同時滿足capacity大於MIN_TREEIFY_CAPACITY時,連結串列轉化為樹;

            2 當樹節點少於UNTREEIFY_THRESHOLD時,從樹轉化為連結串列。

四 建構函式

HashMap的初始化構造方法有四個,都是圍繞initialCapacity和loadFactor這兩個變數展開的,由此可見這兩個變數的重要性,最後一個是根據已有的Map集合初始化新的Map。

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

到這兒了,基本把HashMap的關鍵元素介紹完了,接下來就是HashMap的具體實現了。那麼HashMap究竟有哪些關鍵操作,並且是如何實現的,請看HashMap(Java8)原始碼解析(二)