HashMap源碼解讀

HashMap是基於以Hash算法計算Key的hash值並提供K-V類型存儲的Map非同步實現類,由於是非同步實現,所以是線程不安全的,但是HashMap支持key值和value值的null的插入。


先看一下HashMap提供的構造方法:

/**
     * 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雖然提供了四個構造方法,但是最終都是通過this調用了上文的構造方法,這個方法接收一個初始容量,一個裝載因子作爲構造參數,如果沒有手動傳入,將採用HashMap默認的這兩個參數,默認初始容量爲16,默認裝載因子爲0.75(裝載因子用來衡量在Map的容量達到多少時進行擴容,實時的裝載因子計算方法爲size/Capacity)。

接下來看一下init方法內部:

/**
     * Initialization hook for subclasses. This method is called
     * in all constructors and pseudo-constructors (clone, readObject)
     * after HashMap has been initialized but before any entries have
     * been inserted.  (In the absence of this method, readObject would
     * require explicit knowledge of subclasses.)
     */
    void init() {
    }

init並未做任何事,根據註釋,這個方法將在Map初始化後和插入數據之前被調用,所以可以重寫此方法做一些初始化時需要做的事情。HashMap未用這個方法做任何事情,但是LinkedHashMap作爲HashMap的子類,重寫了這個init方法,進行有序鏈表的初始化維護。

/**
     * Called by superclass constructors and pseudoconstructors (clone,
     * readObject) before any entries are inserted into the map.  Initializes
     * the chain.
     */
    @Override
    void init() {
        header = new Entry<>(-1, null, null, null);
        header.before = header.after = header;
    }

注意Initializes the chain.


接下來看HashMap的Put方法,也是這次解讀的重點。

先上源碼:

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

先看註釋,允許制定的key和value進入Map,如果Map中存在這個key,則老的value會被替代。

其實看完上面的HashMap的構造其實還有點疑惑,因爲根本沒有見它初始化任何容器,只是標定了容器的體積。看put就知道,應該是在第一次put的時候初始化容器,if (table == EMPTY_TABLE)。

/**
     * Inflates the table.
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

上面爲inflateTable方法,在map第一次收到put時,會判斷容器table是否爲空,如果爲空則對table進行初始化,容量一定爲2的次冪。(這部分的設計原理還不是很理解,需要記錄一下以後再看看)

上面說到HashMap接收null最爲key或者value。

/**
     * Offloaded version of put for null keys
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

這裏說明了key作爲value,hashmap是如何處理的,用Entry<K,V>[] table數組的第一位,放置key爲null的鍵值對數據。

如果key不爲null,底層將對key進行hash值的計算   sun.misc.Hashing.stringHash32((String) k)。

拿到hash值後通過 indexFor(int h, int length) 計算這個key在table容器中的位置,其中h是剛剛計算所得hash值,length爲table.length。indexFor方法將保證計算所得的index在數組中。

接下來從table數組容器中獲取Entry,當if (e.hash == hash && ((k = e.key) == key || key.equals(k)))對這個Entry進行key和value的更新操作。

對Entry操作結束後,將對modCount進行一次自增(這裏的modCount代表修改次數,在增刪改中都會用到,與hashMap的線程不安全有關)。

在對modCount的操作結束後,是addEntry:

/**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

這裏主要是對map的容量進行重新計算,當map的成員數量大於由裝載因子計算出來的擴容界限,將對map進行擴容,擴容之後的map容量將是原來map的容量的兩倍。這裏要提下map的擴容,map的擴容是通過新建一個當前map兩倍容量的map後,將原來的map裏面的數據全部移動進去得到的,如果裝載因子設置的過低,裝載界限值極小,將頻繁的發生map擴容的操作,大量的map創建和Entry的移動,會導致效率的降低和內存的丟失,需要注意。

最後就是對Entry的正式創建和map長度的增加:

/**
     * Like addEntry except that this version is used when creating entries
     * as part of Map construction or "pseudo-construction" (cloning,
     * deserialization).  This version needn't worry about resizing the table.
     *
     * Subclass overrides this to alter the behavior of HashMap(Map),
     * clone, and readObject.
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

至此,put結束。


接下來看下上文源碼頻繁出現的Entry是什麼來歷。

Entry實際類名爲HashMap$Entry,是HashMap的一個靜態內部類,實現了Map$Entry接口。提供了一個構造方法以及Key和Value的get/set。說Entry是鏈表結構我感覺有點不太嚴謹,但是確實它是一部分存儲數據,一部分存儲指針。HashMap就是Entry數據的集合,也就是HashMap的數據結構是數組和鏈表的結合體。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章