HashMap梳理

概述

HashMap是基于哈希表的 Map 接口的實現(xiàn)。數(shù)據(jù)以鍵值對的形式存儲,和HashTab的差別在于HashMap可以以null作為鍵值,但是HashMap是線程不安全的。如果要實現(xiàn)線程同步可以使用:

  • Map map = Collections.synchronizedMap(new HashMap());
  • ConcurrentHashMap

HashMap的數(shù)據(jù)結構

HashMap是通過數(shù)組和鏈表(散列鏈表)來實現(xiàn)數(shù)據(jù)存儲的。之所以HashMap查詢速度很快,是因為它是通過散列碼來決定存儲位置。通過獲取key的hashcode和數(shù)組的長度的&值來確定存儲的位置,如果有相同的key的hashcode,那么就是所謂的hash沖突,就添加到對應的鏈表結構的數(shù)據(jù)中。

image.png

紫色代表數(shù)組代表哈希表,也稱為哈希數(shù)組,綠色代表鏈表。數(shù)組存儲具有相同key值hashcode的鏈表的表頭。數(shù)組的元素和鏈表的節(jié)點都是Entry對象。

HashMap源碼分析(Android中的源碼)

  • HashMapEntry對象:
/** HashMapEntry是單向鏈表。    
    * 它是 “HashMap鏈式存儲法”對應的鏈表。    
    * 它實現(xiàn)了Map.Entry 接口,即實現(xiàn)getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數(shù)  
    **/
static class HashMapEntry<K, V> implements Entry<K, V> {
        final K key;
        V value;
        final int hash;
        HashMapEntry<K, V> next;

        HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override public final boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry<?, ?> e = (Entry<?, ?>) o;
            return Objects.equal(e.getKey(), key)
                    && Objects.equal(e.getValue(), value);
        }

        @Override public final int hashCode() {
            return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
        }

        @Override public final String toString() {
            return key + "=" + value;
        }
    }

HashMapEntry就是一個單向鏈表,每個節(jié)點包含了key、value、hash值、和下一個節(jié)點。

  • 重要屬性:
private static final int MINIMUM_CAPACITY = 4;//最小的容量,也是默認的初始容量
static final float DEFAULT_LOAD_FACTOR = .75F;//默認的加載因子
transient HashMapEntry<K, V>[] table;//哈希表
transient int modCount;//被修改的次數(shù)
transient int size;//存放元素的個數(shù)
private transient int threshold;//閾值,當前的元素個數(shù)查過閾值進行擴容,閾值 = 加載因子*總?cè)萘?

加載因子在Android 的源碼中被閹割了,固定為0.75,這是和在jdk中不同的地方

public HashMap(int capacity, float loadFactor) {
        this(capacity);

        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Load factor: " + loadFactor);
        }

        /* 加載因子固定為3/4
         * Note that this implementation ignores loadFactor; it always uses
         * a load factor of 3/4. This simplifies the code and generally
         * improves performance.
         */
    }

加載因子表示HashMap填充的程度,加載因子越大,HashMap填充的越滿才進行擴容,空間利用率越高,造成的問題是存放的數(shù)據(jù)越多,hash沖突的可能性越大,查找的效率越低。反之亦反。這是一個空間和效率的之間的取舍。一般用默認的就行了。

  • put方法:
public V put(K key, V value) {
        if (key == null) {
            //存放null的key值,則將該鍵值對添加到table[0]中。
            return putValueForNullKey(value);
        }
        //對key的hashcode值進行再次計算得到hash。
        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        //通過hash值和數(shù)組長度來確定數(shù)組的下標,這里的值不是隨便取的
        int index = hash & (tab.length - 1);
        //遍歷對應的數(shù)組下標的鏈表,如果存在相同的key值就替換原來的value
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        //否則加載列表的頭部(也就是數(shù)組對應的位置)
        //在添加新的數(shù)據(jù)之前檢查是否需要擴容。如果數(shù)據(jù)的大小超過閾值,數(shù)組的容量就擴大到原來的2倍
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        //把數(shù)據(jù)添加到表頭
        addNewEntry(key, value, hash, index);
        return null;
    }

hash & (tab.length - 1)的算法來取到數(shù)組的下標值,這個方式即可實現(xiàn)均勻的散列,還可以使數(shù)組不越界。那為什么擴容需要2的次冪呢,因為hash & (tab.length - 1)中,如果括號中的結果數(shù)奇數(shù)的話最后一位為1,hash&xx最后的接口有可能是奇數(shù)也有可能是偶數(shù);如果括號中的值是偶數(shù)的話,那最后的結果也只能是偶數(shù),那么數(shù)組存放的值只能存放在偶數(shù)下標中,浪費了一般的資源,如果是2的倍數(shù)減一,剛好能夠控制能過取到所有的下標值。因此,length取2的整數(shù)次冪,是為了使不同hash值發(fā)生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。
擴容:

private HashMapEntry<K, V>[] doubleCapacity() {
        HashMapEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }

新建了一個數(shù)組,是原來數(shù)組容量的兩倍,然后重新計算hashcode在數(shù)組中的位置,并且存放在數(shù)組中。
那為什么擴容呢?隨著HashMap存放的數(shù)據(jù)越來越多,hash沖入產(chǎn)生的概率就越來越大,造成查找效率越來越低,所以進行擴容,但是擴容需要把所有的元素遍歷重新賦值,還要新建一個數(shù)組,這是HashMap很消耗資源的地方。
在達到閾值的時候開始擴容,如:現(xiàn)在的容量大小為8,8*0.75 = 6,當HashMap中的元素個數(shù)達到6的時候就開始擴容。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,290評論 0 16
  • 實際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,526評論 1 37
  • 整個三月份匆匆忙忙的過去了
    橙鉆萌主閱讀 139評論 0 0
  • 從上周六下午開始頭疼,太陽穴兩邊也就是腦仁兒處總是伴隨著心跳“突、突、突”的疼。我們在正常活動的時候一般感覺不到自...
    ZQ鄭閱讀 577評論 1 1
  • 看完《春宴》,心有所感,想說的,也許,差不多就只有這些了吧。沒有劇情,人物來訴說,也沒有太多的講究,大概就是這樣吧...
    小二不2閱讀 293評論 2 3