1. 程式人生 > >開發日常小結(32):HashMap 原始碼分析

開發日常小結(32):HashMap 原始碼分析

2018年10月05日

目錄

1、Java資料結構圖

Java中有幾種常用的資料結構,主要分為Collection和map兩個主要介面(介面只提供方法,並不提供實現),而程式中最終使用的資料結構是繼承自這些介面的資料結構類。

上圖為Collection 介面 和 Map介面繼承圖;今天分析的就是HashMap ;

2、HashMap構造器

先從建構函式說起,HashMap有四個構造方法。

2.1 HashMap(int initialCapacity, float loadFactor) 

    /**      * 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;
        threshold = initialCapacity;
        init();
    }

說明:初始化HashMap,傳入兩個引數:容量 、 負載因子;

HashMap<Object, Object> hashMap = new HashMap<Object,Object>( 20 , 0.8f );

2.2 HashMap(int 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);
    }

說明:預設負載因子 和 自定義容量引數;

2.3 HashMap()

   /**      * Constructs an empty <tt>HashMap</tt> with the default initial capacity      * (16) and the default load factor (0.75).      */

   public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

說明:無參建構函式;this:見2.1的構造方法;

2.4 HashMap(Map<? extends K, ? extends V> m) 

    /**      * 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(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }

說明:入參是實現Map介面的資料結構;

1)先比較 DEFAULT_LOAD_FACTOR(預設1左移4位,即16個元素)和Map引數的元素個數;哪個大就初始化為哪個;HashMap要求容量必須是2的冪。

2)負載因子使用預設的;

3)putAllForCreate:遍歷入參的Map.Entry,逐個將key 和 Value 放進HashMap裡。HashMap允許插入key和value是null的資料的,而ConcurrentHashMap是不允許key和value是null的。

3、put(K key, V value)方法

    /**      * Associates the specified value with the specified key in this map.      * If the map previously contained a mapping for the key, the old      * value is replaced.      *      * @param key key with which the specified value is to be associated      * @param value value to be associated with the specified key      * @return the previous value associated with <tt>key</tt>, or      *         <tt>null</tt> if there was no mapping for <tt>key</tt>.      *         (A <tt>null</tt> return can also indicate that the map      *         previously associated <tt>null</tt> with <tt>key</tt>.)      */

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

        // key為null呼叫putForNullKey(value)
        if (key == null)
            return putForNullKey(value);

        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

4、get(Object key)方法

    /**      * Returns the value to which the specified key is mapped,      * or {@code null} if this map contains no mapping for the key.      *      * <p>More formally, if this map contains a mapping from a key      * {@code k} to a value {@code v} such that {@code (key==null ? k==null :      * key.equals(k))}, then this method returns {@code v}; otherwise      * it returns {@code null}.  (There can be at most one such mapping.)      *      * <p>A return value of {@code null} does not <i>necessarily</i>      * indicate that the map contains no mapping for the key; it's also      * possible that the map explicitly maps the key to {@code null}.      * The {@link #containsKey containsKey} operation may be used to      * distinguish these two cases.      *      * @see #put(Object, Object)      */

    public V get(Object key) {

        //key值為null,則返回對應的value;
        if (key == null)
            return getForNullKey();

        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

說明:從getForNullKey()方法原始碼可以看到,其實HashMap儲存資料在一個Entry<K,V>的陣列中,獲取鍵值對來返回value;

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

5、remove(Object key)方法

   /**      * Removes the mapping for the specified key from this map if present.      *      * @param  key key whose mapping is to be removed from the map      * @return the previous value associated with <tt>key</tt>, or      *         <tt>null</tt> if there was no mapping for <tt>key</tt>.      *         (A <tt>null</tt> return can also indicate that the map      *         previously associated <tt>null</tt> with <tt>key</tt>.)      */

    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

說明:移除一個元素;

6、成員變數

   /**
     * 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;

    /**
     * An empty table instance to share when the table is not inflated.
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The next size value at which to resize (capacity * load factor).
     * @serial
     */
    // If table == EMPTY_TABLE then this is the initial capacity at which the
    // table will be created when inflated.
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * The default threshold of map capacity above which alternative hashing is
     * used for String keys. Alternative hashing reduces the incidence of
     * collisions due to weak hash code calculation for String keys.
     * <p/>
     * This value may be overridden by defining the system property
     * {@code jdk.map.althashing.threshold}. A property value of {@code 1}
     * forces alternative hashing to be used at all times whereas
     * {@code -1} value ensures that alternative hashing is never used.
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

7、putAll(Map<? extends K, ? extends V> m)方法

    /**      * Copies all of the mappings from the specified map to this map.      * These mappings will replace any mappings that this map had for      * any of the keys currently in the specified map.      *      * @param m mappings to be stored in this map      * @throws NullPointerException if the specified map is null      */

    public void putAll(Map<? extends K, ? extends V> m) {
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0)
            return;

        if (table == EMPTY_TABLE) {
            inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
        }

        /*
         * Expand the map if the map if the number of mappings to be added
         * is greater than or equal to threshold.  This is conservative; the
         * obvious condition is (m.size() + size) >= threshold, but this
         * condition could result in a map with twice the appropriate capacity,
         * if the keys to be added overlap with the keys already in this map.
         * By using the conservative calculation, we subject ourself
         * to at most one extra resize.
         */
        if (numKeysToBeAdded > threshold) {
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity <<= 1;
            if (newCapacity > table.length)
                resize(newCapacity);
        }

        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

說明:threshold = HashMap的初始化容量;

8、1.7和1.8的HashMap的不同點

1)JDK1.7用的是頭插法,而JDK1.8及之後使用的都是尾插法。

為什麼要這樣做呢?因為JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法就是能夠提高插入的效率,但是也會容易出現逆序且環形連結串列死迴圈問題。

但是在JDK1.8之後是因為加入了紅黑樹使用尾插法,能夠避免出現逆序且連結串列死迴圈的問題。

2)擴容後資料儲存位置的計算方式也不一樣:

  • 在JDK1.7的時候是直接用hash值和需要擴容的二進位制數進行&(這裡就是為什麼擴容的時候為啥一定必須是2的多少次冪的原因所在,因為如果只有2的n次冪的情況時最後一位二進位制數才一定是1,這樣能最大程度減少hash碰撞)(hash值 & length-1) 。

  • 而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而不再是JDK1.7的那種異或的方法。但是這種方式就相當於只需要判斷Hash值的新增參與運算的位是0還是1就直接迅速計算出了擴容後的儲存方式。

3)JDK1.7的時候使用的是陣列+ 單鏈表的資料結構。但是在JDK1.8及之後時,使用的是陣列+連結串列+紅黑樹的資料結構(當連結串列的深度達到8的時候,也就是預設閾值,就會自動擴容把連結串列轉成紅黑樹的資料結構來把時間複雜度從O(N)變成O(logN)提高了效率)。