jdk集合源碼之HashMap

分析完數組和鏈表,再來分析下HashMap的源碼,基於K-V方式的存儲結構,簡單想象一下,典型的實現是基於數組+鏈表,正好我們前面分析完了數組和鏈表。廢話不多說,直接上源碼。

還是先從構造函數看起:

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

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

    /**
     * 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;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }
public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

HashMap提供了四種構造函數,來看最簡單的一個構造函數,主要完成了成員變量的初始化。loadFactor裝載因子後面會講到,threshold等於裝載因子和容量的積。table就是數組的大小,這裏的數組是一個Entry數組,默認大小爲16,注意這裏的大小爲2的冪數,這裏面是有講究的後面會提到。init方法默認未提供實現。其他兩個構造函數提供的也是初始化的動作,值得看一下的是第一個構造函數,如果傳入的容量不是2的冪數,它會找到大於該容量的最小的2的冪數。實現方式是不斷的左移來判斷。最後一個構造函數後面講。


在看新增的方法前,先看下Entry的結構,Entry是HashMap的一個靜態內部類。將上圖的每個節點放大來看:


很簡單的結構,可以看出在每一個數組的位置,會根據hash值形成一個單項的鏈表。好了,看下最基本的put方法:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        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;
    }

值得說明的是Map中的key可以爲null,當key爲null時做單獨處理調用putForNullKey方法,因爲null值並沒有自己hash值:

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

可以看到null對象的key始終都是放到table[0]中,如果數組當前第一個元素爲空,則調用addEntry添加。

 void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

初始化第一個元素,傳入的hash值爲0,這裏用到了threshold這個成員變量,可以看到如果當前map的size大於等於閾值則將前map的數組擴大一倍,threshold和loadfactor直接相關,主要是爲了防止過多的hash衝突,通過使用更多的空間來提高查找效率,假設women有初始大小爲8的map,則當存入第7個元素的時候,就會調用resize方法重置當前的map。


看一下resize的方法:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

整個轉換的過程主要由transfer方法完成。

 void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

通過外層的for循環遍歷數組,內層的while循環遍歷每個節點上的鏈表,然後對鏈表上的每個節點調用indexFor方法進行重新hash獲取在新數組的位置i。然後將e的下一個節點指向i處原來的鏈表,將e插入的鏈表的頭部,繼續整個循環。

通過對putForNullKey方法的分析,認識了hashmap的擴充策略,下面接着看該方法的下半部分,先看下非常重要的hash規則,int hash = hash(key.hashCode());hash方法通過對key的再一次hash來獲取哈希值,爲的是避免key的哈希值分配不均,然後通過哈希值獲取在數組中的下標,主要通過indexFor方法實現:

static int indexFor(int h, int length) {
        return h & (length-1);
    }

這個方法實現的很巧妙,主要是因爲通過前面的保證,數組的長度始終是2的冪數,因此這裏的length-1用二進制表示全部是1,拿哈希值和全1的二進制做與操作,避免了和0與操作的相同性,其實這裏的與操作,就是對length取餘操作,保證從0-length的均勻性。put方法的後半部分和putForNullKey類似就不再分析了(如果找到hash值相同且key相同則覆蓋,否則新增到鏈表頭部)。

未插入元素u之前的結構圖


將u插入到位置7(假設hash後下標爲7)後的結構圖:


看完單個元素的插入,接着看批量的插入,上源碼:

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

        /*
         * 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 (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<? extends K, ? extends V> e = i.next();
            put(e.getKey(), e.getValue());
        }
    }

整個方法沒有難理解的地方,除了if (numKeysToBeAdded > threshold)這裏的判斷,這裏不使用numKeysToBeAdded + size判斷,而是使用 numKeysToBeAdded,這是一種保守的做法,因爲考慮的被插入的集合可能和原來的集合有相同的key,避免造成空間的浪費。

看完新增,再看最開始提到的第四種構造函數,裏面的邏輯就容易理解多了,和我們前面分析的put操作差不太多。除了它裏面調用的是createEntry

void createEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        size++;
    }
由於構造函數保證了不會超過閾值,所以這裏只有新增,沒有判斷是否超過閾值。

看完新增照慣例來看刪除操作,主要看下removeEntryForKey,根據key去刪除節點,大體猜測一下實現,先確認是否爲null,是的話從0的位置查找,否則根據hash計算下標,然後遍歷對應的鏈表,比較key值,沒有找到則返回null。

final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

主要看下while循環中的動作,假設當前找到的鏈表結構如下,需要刪除的是b:


第一次循環if條件不滿足,則prev=e,e=next;


經過幾次循環假設現在滿足了if條件


這裏prev!=e則將prev的next節點直接指向next節點,從而將b節點刪除,那麼什麼時候prev==e呢?很明顯,當鏈的頭結點即是我們要刪除的節點時,直接將頭結點的next指針指向next元素。

新增和刪除看完後,其他的比如get,contain等方法就不難理解了,這裏也不一一進行講解了。

其實分析HashMap需要重點看的是它提供各種各樣的迭代器,HashMap的內部類比前面看的ArrayList和LinkedList都要多。

private final class ValueIterator extends HashIterator<V> {
        public V next() {
            return nextEntry().value;
        }
    }

    private final class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }

    private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }

value,value和entry的迭代器都是基於HashIterator實現,那麼就來看下HashIterator:

<pre name="code" class="java">private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;	// next entry to return
        int expectedModCount;	// For fast-fail
        int index;		// current slot
        Entry<K,V> current;	// current entry

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
	    current = e;
            return e;
        }

        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }

    }

如果你觀察仔細會發現HashIterator少實現了接口Iterator中的一個next方法,該方法其實是留給了子類去實現,而它只提供了基礎的功能。看構造方法,從數組的0開始查找,直到該位置處的元素不爲null。所以顯而易見的hasNext方法只要判斷next是否爲null即可,重點看一下nextEntry這是HashIterator自己新增的方法,返回下一個entry對象。


仍然來看圖:
<img src="https://img-blog.csdn.net/20150102193655302?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdGFuZ3lvbmd6aGU=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
如果遍歷完當前節點的鏈表(即next == null),則繼續尋找下一個不爲null的節點,依次進行遍歷。爲了統一接口next方法由子類藉助nextEntry實現。上面迭代器都由對應的KeySet,Values,entrySet使用,對外呈現給開發人員。

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