LruCache之HashMap分析

LruCache是Android的一個(gè)內(nèi)部類,提供了基于內(nèi)存實(shí)現(xiàn)的緩存,打算分析一下其實(shí)現(xiàn)的原理,不過發(fā)現(xiàn)其內(nèi)部是基于LinkedHashMap,而這個(gè)又是基于HashMap,所以,從頭開始學(xué)習(xí)咯。

在討論HashMap前,有必要先談?wù)剶?shù)組和鏈表這兩種常用數(shù)據(jù)結(jié)構(gòu)。

數(shù)組在內(nèi)存中開辟的空間是連續(xù)的,如果要插入或者刪除一個(gè)node,那么這個(gè)node之后的所有數(shù)據(jù)都要整體move,但數(shù)組的查詢快(二分查找)。其特點(diǎn)是:尋址容易,增刪困難

鏈表在內(nèi)存中離散存儲(chǔ),插入和刪除十分輕松,但查詢較慢,每次都要從頭到尾遍歷一遍。其特點(diǎn)是:尋址困難,增刪容易

哈希表的存在就是取其精華,去其糟粕。

哈希表有多種不同的實(shí)現(xiàn)方案,本文接下來介紹的是最常用的一種(也是JDK中HashMap的實(shí)現(xiàn)方案)—— 拉鏈法,我們可以理解為“數(shù)組+鏈表” 。如圖:

(以下是以 Android SDK 里面的方法,和 Java JDK 里面的些許不同,谷歌程序猿:烏拉)

HashMap內(nèi)部其實(shí)就是一個(gè)Entry數(shù)組(table)

/**
 * The hash table. If this hash map contains a mapping for null, it is
 * not represented this hash table.
 */
transient HashMapEntry<K, V>[] table;

數(shù)組中每個(gè)元素都可以看成一個(gè)桶,每一個(gè)桶又構(gòu)成了一條鏈表。

源碼

構(gòu)造方法

/**
 * Constructs a new empty {@code HashMap} instance.
 */
@SuppressWarnings("unchecked")
public HashMap() {
    table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
    threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}

看構(gòu)造方法,會(huì)涉及到HashMapEntry這個(gè)數(shù)據(jù)結(jié)構(gòu)

static class HashMapEntry<K, V> implements Entry<K, V> {
    final K key;// 鍵
    V value;// 值
    final int hash;// 哈希值
    HashMapEntry<K, V> next;// 指向下一個(gè)節(jié)點(diǎn)

    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;
    }
    // 判斷兩個(gè)Entry是否相等
    
    @Override public final boolean equals(Object o) {
        if (!(o instanceof Entry)) {// 如果Object o不是Entry類型,直接返回false
            return false;
        }
        Entry<?, ?> e = (Entry<?, ?>) o;
        return Objects.equal(e.getKey(), key)
                && Objects.equal(e.getValue(), value);// 若兩個(gè)Entry的“key”和“value”都相等,則返回true。
    }

    @Override public final int hashCode() {// 實(shí)現(xiàn)hashCode(),判斷存儲(chǔ)在哪個(gè)數(shù)組里面
        return (key == null ? 0 : key.hashCode()) ^
                (value == null ? 0 : value.hashCode());
    }

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

這是一個(gè)靜態(tài)內(nèi)部類 HashMapEntry,實(shí)現(xiàn)的是Entry接口,是HashMap鏈?zhǔn)酱鎯?chǔ)對(duì)應(yīng)的鏈表(數(shù)組加鏈表形式)

繼續(xù)看,涉及到了 EMPTY_TABLE 定義如下

/**
 * An empty table shared by all zero-capacity maps (typically from default
 * constructor). It is never written to, and replaced on first put. Its size
 * is set to half the minimum, so that the first resize will create a
 * minimum-sized table.
 */
private static final Entry[] EMPTY_TABLE
        = new HashMapEntry[MINIMUM_CAPACITY >>> 1];

 /**
 * Min capacity (other than zero) for a HashMap. Must be a power of two
 * greater than 1 (and less than 1 << 30).
 */
private static final int MINIMUM_CAPACITY = 4;

MINIMUM_CAPACITY往右移動(dòng)一位,大小變?yōu)?了

所以上面的構(gòu)造函數(shù)就是在HashMap中有一個(gè)table,保存的是一個(gè)HashMapEntry類型的數(shù)組,數(shù)組的大小為2(區(qū)別與OracleJDK,其默認(rèn)大小是16)

/**
 * Constructs a new {@code HashMap} instance with the specified capacity.
 *
 * @param capacity
 *            the initial capacity of this hash map.
 * @throws IllegalArgumentException
 *                when the capacity is less than zero.
 */
public HashMap(int capacity) {
    if (capacity < 0) {//容量小于領(lǐng),拋異常
        throw new IllegalArgumentException("Capacity: " + capacity);
    }

    if (capacity == 0) {//容量為零,替換成默認(rèn)的EMPTY_TABLE
        @SuppressWarnings("unchecked")
        HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
        table = tab;
        threshold = -1; // Forces first put() to replace EMPTY_TABLE
        return;
    }

    if (capacity < MINIMUM_CAPACITY) {//不能小于規(guī)定的最小值,也就是4
        capacity = MINIMUM_CAPACITY;
    } else if (capacity > MAXIMUM_CAPACITY) {不能大于規(guī)定的最大值,也就是 1 << 30
        capacity = MAXIMUM_CAPACITY;
    } else {//尋找最合適的
        capacity = Collections.roundUpToPowerOfTwo(capacity);
    }
    makeTable(capacity);
}

Collections.roundUpToPowerOfTwo()

/**
 * Returns the smallest power of two >= its argument, with several caveats:
 * If the argument is negative but not Integer.MIN_VALUE, the method returns
 * zero. If the argument is > 2^30 or equal to Integer.MIN_VALUE, the method
 * returns Integer.MIN_VALUE. If the argument is zero, the method returns
 * zero.
 * @hide
 */
public static int roundUpToPowerOfTwo(int i) {
    i--; // If input is a power of two, shift its high-order bit right.

    // "Smear" the high-order bit all the way to the right.
    i |= i >>>  1;
    i |= i >>>  2;
    i |= i >>>  4;
    i |= i >>>  8;
    i |= i >>> 16;

    return i + 1;
}

看注釋應(yīng)該知道這個(gè)方法的功能就是返回一個(gè)比指定值i,也就是上面的 capacity 大的2的n次方的最小值

例如輸入i = 17,二進(jìn)制表示為00010001

  • i -- 后,     二進(jìn)制表示為 00010000,十進(jìn)制i = 16
  • i >>> 1 后,   二進(jìn)制表示為 00001000,十進(jìn)制i = 8
  • i |= 后,     二進(jìn)制表示為 00011000,十進(jìn)制i = 24
  • i >>> 2 后,   二進(jìn)制表示為 00000110,十進(jìn)制i = 6
  • i |= 后,     二進(jìn)制表示為 00011110,十進(jìn)制i = 30
  • i >>> 4 后,   二進(jìn)制表示為 00000001,十進(jìn)制i = 1
  • i |= 后,     二進(jìn)制表示為 00011111,十進(jìn)制i = 31
  • i >>> 8 后,   二進(jìn)制表示為 00011111,十進(jìn)制i = 31
  • i |= 后,     二進(jìn)制表示為 00011111,十進(jìn)制i = 31
  • i >>> 16 后,  二進(jìn)制表示為 00011111,十進(jìn)制i = 31
  • i |= 后,     二進(jìn)制表示為 00011111,十進(jìn)制i = 31
  • i + 1 后,    二進(jìn)制表示為 00100000,十進(jìn)制i = 32

所以,比i = 17大的最小的2的次方應(yīng)該是2的5次方

例如輸入i = 16,二進(jìn)制表示為0001000

  • i -- 后,     二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i >>> 1 后,   二進(jìn)制表示為 00000111,十進(jìn)制i = 7
  • i |= 后,     二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i >>> 2 后,    二進(jìn)制表示為 00000011,十進(jìn)制i = 3
  • i |= 后,     二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i >>> 4 后,    二進(jìn)制表示為 00000000,十進(jìn)制i = 0
  • i |= 后,     二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i >>> 8 后,   二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i |= 后,     二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i >>> 16 后,  二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i |= 后,     二進(jìn)制表示為 00001111,十進(jìn)制i = 15
  • i + 1 后,     二進(jìn)制表示為 00010000,十進(jìn)制i = 16

所以,比i = 16大的最小的2的次方應(yīng)該是2的4次方,就是其本身

言歸正傳,繼續(xù)看 makeTable 方法

makeTable()

/**
 * Allocate a table of the given capacity and set the threshold accordingly.
 * @param newCapacity must be a power of two
 */
private HashMapEntry<K, V>[] makeTable(int newCapacity) {
    @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
            = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
    table = newTable;
    threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity 
    return newTable;
}

根據(jù)代碼可知,其初始化了一個(gè)HashMapEntry類型的數(shù)組table,用來存放HashMapEntry,而threshold如它的翻譯般,就是一個(gè)闕值,這個(gè)值是將來擴(kuò)容的參考,是容量的3/4(新閾值 = 新容量/2 + 新容量/4,相當(dāng)于乘以容量的 3/4,not care 加載因子)

/**
 * Constructs a new {@code HashMap} instance with the specified capacity and
 * load factor.
 *
 * @param capacity
 *            the initial capacity of this hash map.
 * @param loadFactor
 *            the initial load factor.
 * @throws IllegalArgumentException
 *                when the capacity is less than zero or the load factor is
 *                less or equal to zero or NaN.
 */
public HashMap(int capacity, float loadFactor) {
    this(capacity);

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

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

上面這個(gè)個(gè)構(gòu)造方法最終會(huì)調(diào)用這個(gè)構(gòu)造方法 HashMap(int capacity),看注釋,里面說明了裝載因子為0.75。

注意:OracleJDK 中的閾值計(jì)算公式是:當(dāng)前 Entry 數(shù)組長(zhǎng)度*加載因子,其默認(rèn)的加載因子是0.75,加載因子也可以通過構(gòu)造器來設(shè)置。AndroidJDK 的加載因子也是0.75,不同的是,AndroidJDK 不支持其他數(shù)值的加載因子

/**
 * Constructs a new {@code HashMap} instance containing the mappings from
 * the specified map.
 *
 * @param map
 *            the mappings to add.
 */
public HashMap(Map<? extends K, ? extends V> map) {
    this(capacityForInitSize(map.size()));
    constructorPutAll(map);
}

這個(gè)構(gòu)造方法傳入的參數(shù)是個(gè) map,根據(jù) map 的大小初始化。

HashMap 的構(gòu)造方法講完后,一般我們使用時(shí),都是先往里面放數(shù)據(jù),下面就看看 put() 方法

put()

/**
 * Maps the specified key to the specified value.
 *
 * @param key
 *            the key.
 * @param value
 *            the value.
 * @return the value of any previous mapping with the specified key or
 *         {@code null} if there was no such mapping.
 */
@Override 
public V put(K key, V value) {
    if (key == null) {// 若key為null,則將該鍵值對(duì)添加到table[0]中。
        return putValueForNullKey(value);
    }
    // 若key不為null,則計(jì)算該key的哈希值,然后將其添加到該哈希值對(duì)應(yīng)的鏈表中。
    int hash = Collections.secondaryHash(key);// Collections.secondaryHash能夠使得hash過后的值的分布更加均勻,盡可能地避免沖突
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);// 根據(jù)hash值計(jì)算存儲(chǔ)位置 index的值永遠(yuǎn)都是0到2的n次方減1之間,可以保證結(jié)果不大于tab.length
    
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {// 循環(huán)遍歷Entry數(shù)組,若key對(duì)應(yīng)的鍵值對(duì)已經(jīng)存在,則用新的value取代舊的value。然后返回原來的 oldValue
        if (e.hash == hash && key.equals(e.key)) {//根據(jù)hash值是否相等以及 key值是否一樣進(jìn)行判斷
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }

    // No entry for (non-null) key is present; create one
    modCount++;// 修改次數(shù)+1
    if (size++ > threshold) {// 如何hashmap的大小超過了闕值,進(jìn)行擴(kuò)容
        tab = doubleCapacity();
        index = hash & (tab.length - 1);
    }
    addNewEntry(key, value, hash, index);// 添加新的Entry
    return null;
}

里面涉及到了,Collections.secondaryHash(key),該函數(shù)的原理在這里

從源碼可以看出,put 方法是有返回值的(可以理解為put也是包含了get方法的精髓),根據(jù)返回值不同,可以有其他作用。

另外,我們構(gòu)造 HashMap 的構(gòu)造方法時(shí), 闕值 threshold = -1 ,是滿足二倍擴(kuò)容的,也就是說,在 AndroidJDK 的 HashMap 中使用無參構(gòu)造方法后,第一次 put 數(shù)據(jù)就會(huì)觸發(fā)哈希表的二倍擴(kuò)容,因?yàn)閿U(kuò)容后數(shù)組的長(zhǎng)度發(fā)生了變化,所以數(shù)據(jù)入桶的位置也會(huì)發(fā)生變化,這個(gè)時(shí)候需要新構(gòu)建 Hash 表。而另外,HashMap 是允許 Key 為null,看看 putValueForNullKey 方法

putValueForNullKey()

private V putValueForNullKey(V value) {
    HashMapEntry<K, V> entry = entryForNullKey;
    if (entry == null) {
        addNewEntryForNullKey(value);
        size++;// hashmap大小 + 1
        modCount++;// 修改次數(shù) + 1
        return null;
    } else {
        preModify(entry);
        V oldValue = entry.value;
        entry.value = value;
        return oldValue;
    }
}

entryForNullKey的定義如下

 /**
 * The entry representing the null key, or null if there's no such mapping.
 */
transient HashMapEntry<K, V> entryForNullKey;

還是個(gè)HashMapEntry

當(dāng)entry為空時(shí),此時(shí)調(diào)用 addNewEntryForNullKey 方法,如下

addNewEntryForNullKey()

/**
 * Creates a new entry for the null key, and the given value and
 * inserts it into the hash table. This method is called by put
 * (and indirectly, putAll), and overridden by LinkedHashMap.
 */
void addNewEntryForNullKey(V value) {
    entryForNullKey = new HashMapEntry<K, V>(null, value, 0, null);
}

會(huì)新構(gòu)造一個(gè)新的HashMapEntry,傳入value,其他為null or 0。

當(dāng)entry不為空時(shí),調(diào)用 preModify 方法,如下

preModify()

/**
 * Give LinkedHashMap a chance to take action when we modify an existing
 * entry.
 *
 * @param e the entry we're about to modify.
 */
void preModify(HashMapEntry<K, V> e) { }// LinkedHashMap實(shí)現(xiàn)

好吧,就一個(gè)空方法,該方法會(huì)在 LinkedHashMap 中實(shí)現(xiàn),接下來就是返回 oldValue

當(dāng)key 不為空時(shí),主要過程已經(jīng)標(biāo)注在了代碼中,這里看一下擴(kuò)容方法 doubleCapacity()

doubleCapacity()

/**
 * Doubles the capacity of the hash table. Existing entries are placed in
 * the correct bucket on the enlarged table. If the current capacity is,
 * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
 * will be new unless we were already at MAXIMUM_CAPACITY.
 */
private HashMapEntry<K, V>[] doubleCapacity() {
    HashMapEntry<K, V>[] oldTable = table;// 原table 標(biāo)記為 oldTable
    int oldCapacity = oldTable.length;// 舊容量
    if (oldCapacity == MAXIMUM_CAPACITY) {// 就容量已經(jīng)是最大值了,就不用擴(kuò)容了(也擴(kuò)不了)
        return oldTable;
    }
    int newCapacity = oldCapacity * 2;// 新容量是舊容量的2倍
    HashMapEntry<K, V>[] newTable = makeTable(newCapacity);// 根據(jù)新容量重新創(chuàng)建一個(gè)
    if (size == 0) {// 如果原來HashMap的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;
        }

        // 下面這三行,忒抽象,舉例說明好了
        //oldCapacity假設(shè)為16(00010000),int highBit = e.hash & oldCapacity能夠得到高位的值,因?yàn)榻?jīng)過與操作過后,低位一定是0
        int highBit = e.hash & oldCapacity;
        HashMapEntry<K, V> broken = null;
        // J 在這里是index,J 與 高位的值進(jìn)行或操作過后,就能得到在擴(kuò)容后的新的index值。
        // 設(shè)想一下,理論上我們得到的新的值應(yīng)該是 newValue = hash & (newCapacity - 1) 
        // 與 oldValue = hash & (oldCapacity - 1) 的區(qū)別僅在于高位上。 
        // 因此我們用 J | highBit 就可以得到新的index值。
        newTable[j | highBit] = e;
        // 下面的操作就是如果當(dāng)前元素下面掛載的還有元素就重新排放
        for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
            //跟上面的類似,以下一個(gè)高位作為排放的依據(jù)
            int nextHighBit = n.hash & oldCapacity;
            if (nextHighBit != highBit) {// 說明位于不同的位置,just存放就可以
                if (broken == null)
                    newTable[j | nextHighBit] = n;
                else
                    broken.next = n;
                broken = e;
                highBit = nextHighBit;
            }// 如果相等說明這兩個(gè)元素肯定還位于數(shù)組的同一位置以鏈表的形式存在,not care
        }
        if (broken != null)
            broken.next = null;
    }
    return newTable;
}

擴(kuò)容方法講完后,繼續(xù) put 方法的 addNewEntry(key, value, hash, index);

addNewEntry()

/**
 * Creates a new entry for the given key, value, hash, and index and
 * inserts it into the hash table. This method is called by put
 * (and indirectly, putAll), and overridden by LinkedHashMap. The hash
 * must incorporate the secondary hash function.
 */
void addNewEntry(K key, V value, int hash, int index) {
    table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}

這個(gè)方法就是將老 Entry 作為新建 Entry 對(duì)象的 next 節(jié)點(diǎn)返回給當(dāng)前數(shù)組元素(物理空間上其實(shí)是在鏈表頭部添加新節(jié)點(diǎn))

put 方法后,就看看 get() 方法

get()

/**
 * Returns the value of the mapping with the specified key.
 *
 * @param key
 *            the key.
 * @return the value of the mapping with the specified key, or {@code null}
 *         if no mapping for the specified key is found.
 */
public V get(Object key) {
    if (key == null) {//取空
        HashMapEntry<K, V> e = entryForNullKey;
        return e == null ? null : e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            return e.value;
        }
    }
    return null;
}

這個(gè)方法,看代碼就應(yīng)該可以理解,和 put 的操作相反,怎么存的再怎么取出來就ok。

下面看看 containsKey 方法

containsKey()

/**
 * Returns whether this map contains the specified key.
 *
 * @param key
 *            the key to search for.
 * @return {@code true} if this map contains the specified key,
 *         {@code false} otherwise.
 */
@Override public boolean containsKey(Object key) {
    if (key == null) {
        return entryForNullKey != null;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            return true;
        }
    }
    return false;
}

和 get 方法類似,返回方法不同而已。

看看孿生兄弟 containsValue

containsValue

/**
 * Returns whether this map contains the specified value.
 *
 * @param value
 *            the value to search for.
 * @return {@code true} if this map contains the specified value,
 *         {@code false} otherwise.
 */
@Override public boolean containsValue(Object value) {
    HashMapEntry[] tab = table;
    int len = tab.length;
    if (value == null) {
        for (int i = 0; i < len; i++) {
            for (HashMapEntry e = tab[i]; e != null; e = e.next) {
                if (e.value == null) {
                    return true;
                }
            }
        }
        return entryForNullKey != null && entryForNullKey.value == null;
    }

    // value is non-null
    for (int i = 0; i < len; i++) {
        for (HashMapEntry e = tab[i]; e != null; e = e.next) {
            if (value.equals(e.value)) {
                return true;
            }
        }
    }
    return entryForNullKey != null && value.equals(entryForNullKey.value);
}

這個(gè)是查找是否含有value,和上面查找是否含有key類似,不過這個(gè)的循環(huán)次數(shù)就比尋找key要多,數(shù)組和鏈表都要查找一遍(沒有辦法啦,誰讓 hashMap 是根據(jù) key 來存,偏偏要取 value ,不得不耗時(shí)咯)。

前面有一個(gè)構(gòu)造方法,沒有仔細(xì)看

  /**
 * Constructs a new {@code HashMap} instance containing the mappings from
 * the specified map.
 *
 * @param map
 *            the mappings to add.
 */
public HashMap(Map<? extends K, ? extends V> map) {
    this(capacityForInitSize(map.size()));
    constructorPutAll(map);
}

首先看看 capacityForInitSize 方法

capacityForInitSize()

/**
 * Returns an appropriate capacity for the specified initial size. Does
 * not round the result up to a power of two; the caller must do this!
 * The returned value will be between 0 and MAXIMUM_CAPACITY (inclusive).
 */
static int capacityForInitSize(int size) {
    int result = (size >> 1) + size; // Multiply by 3/2 to allow for growth

    // boolean expr is equivalent to result >= 0 && result<MAXIMUM_CAPACITY
    return (result & ~(MAXIMUM_CAPACITY-1))==0 ? result : MAXIMUM_CAPACITY;
}

這個(gè)方法是根據(jù) map 的 size 進(jìn)行合理的擴(kuò)容,擴(kuò)容的大小就是 size 的 1.5 倍,最后是根據(jù)擴(kuò)容的大小判斷返回值,如果擴(kuò)容的大小大于 1 << 30 則返回 1 << 30 (MAXIMUM_CAPACITY),否則就返回?cái)U(kuò)容后的大小。

下面就是 constructorPutAll 方法

constructorPutAll()

/**
 * Inserts all of the elements of map into this HashMap in a manner
 * suitable for use by constructors and pseudo-constructors (i.e., clone,
 * readObject). Also used by LinkedHashMap.
 */
final void constructorPutAll(Map<? extends K, ? extends V> map) {
    if (table == EMPTY_TABLE) {
        doubleCapacity(); // Don't do unchecked puts to a shared table.
    }
    for (Entry<? extends K, ? extends V> e : map.entrySet()) {
        constructorPut(e.getKey(), e.getValue());
    }
}

該方法會(huì)把所有的 key 和 value 存儲(chǔ)起來,利用 constructorPut 方法

constructorPut()

/**
 * This method is just like put, except that it doesn't do things that
 * are inappropriate or unnecessary for constructors and pseudo-constructors
 * (i.e., clone, readObject). In particular, this method does not check to
 * ensure that capacity is sufficient, and does not increment modCount.
 */
private void constructorPut(K key, V value) {
    if (key == null) {
        HashMapEntry<K, V> entry = entryForNullKey;
        if (entry == null) {
            entryForNullKey = constructorNewEntry(null, value, 0, null);
            size++;
        } else {
            entry.value = value;
        }
        return;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    HashMapEntry<K, V> first = tab[index];
    for (HashMapEntry<K, V> e = first; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            e.value = value;
            return;
        }
    }

    // No entry for (non-null) key is present; create one
    tab[index] = constructorNewEntry(key, value, hash, first);
    size++;
}

該方法和 put 方法很類似,但是注釋也講明了二者的區(qū)別。

再看 putAll 方法

putAll()

/**
 * Copies all the mappings in the specified map to this map. These mappings
 * will replace all mappings that this map had for any of the keys currently
 * in the given map.
 *
 * @param map
 *            the map to copy mappings from.
 */
@Override public void putAll(Map<? extends K, ? extends V> map) {
    ensureCapacity(map.size());
    super.putAll(map);
}

調(diào)用了 ensureCapacity 方法

ensureCapacity()

/**
 * Ensures that the hash table has sufficient capacity to store the
 * specified number of mappings, with room to grow. If not, it increases the
 * capacity as appropriate. Like doubleCapacity, this method moves existing
 * entries to new buckets as appropriate. Unlike doubleCapacity, this method
 * can grow the table by factors of 2^n for n > 1. Hopefully, a single call
 * to this method will be faster than multiple calls to doubleCapacity.
 *
 *  <p>This method is called only by putAll.
 */
private void ensureCapacity(int numMappings) {
    int newCapacity = Collections.roundUpToPowerOfTwo(capacityForInitSize(numMappings));// 上面講過
    HashMapEntry<K, V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (newCapacity <= oldCapacity) {
        return;
    }
    if (newCapacity == oldCapacity * 2) {// 初始化的空間大小是原來大小2倍
        doubleCapacity();
        return;
    }

    // We're growing by at least 4x, rehash in the obvious way
    HashMapEntry<K, V>[] newTable = makeTable(newCapacity);// 根據(jù)newCapacity初始化新數(shù)組
    if (size != 0) {// 重新掛載
        int newMask = newCapacity - 1;
        for (int i = 0; i < oldCapacity; i++) {
            for (HashMapEntry<K, V> e = oldTable[i]; e != null;) {
                HashMapEntry<K, V> oldNext = e.next;
                int newIndex = e.hash & newMask;
                HashMapEntry<K, V> newNext = newTable[newIndex];
                newTable[newIndex] = e;
                e.next = newNext;
                e = oldNext;
            }
        }
    }
}

接下來就是 remove 方法

remove()

/**
 * Removes the mapping with the specified key from this map.
 *
 * @param key
 *            the key of the mapping to remove.
 * @return the value of the removed mapping or {@code null} if no mapping
 *         for the specified key was found.
 */
@Override public V remove(Object key) {
    if (key == null) {// key 為空就調(diào)用下面方法去除
        return removeNullKey();
    }
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index], prev = null;
            e != null; prev = e, e = e.next) {// 對(duì)鏈表的操作
        if (e.hash == hash && key.equals(e.key)) {
            if (prev == null) {
                tab[index] = e.next;
            } else {
                prev.next = e.next;
            }
            modCount++;// 修改次數(shù) + 1
            size--;// 大小 - 1
            postRemove(e);// 標(biāo)記去除的e
            return e.value;
        }
    }
    return null;
}

private V removeNullKey() {// 針對(duì)key 為null 進(jìn)行處理
    HashMapEntry<K, V> e = entryForNullKey;
    if (e == null) {
        return null;
    }
    entryForNullKey = null;
    modCount++;
    size--;
    postRemove(e);
    return e.value;
}

/**
 * Subclass overrides this method to unlink entry.
 */
void postRemove(HashMapEntry<K, V> e) { }// LinkedHashMap實(shí)現(xiàn)

休息一下

還記得 HashMap 的類聲明嗎,

public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable 

實(shí)現(xiàn)了 Cloneable 和 Serializable接口,都是兩個(gè)空接口,一個(gè)實(shí)現(xiàn)淺拷貝,一個(gè)實(shí)現(xiàn)序列化

clone()

/**
 * Returns a shallow copy of this map.
 *
 * @return a shallow copy of this map.
 */
@SuppressWarnings("unchecked")
@Override public Object clone() {
    /*
     * This could be made more efficient. It unnecessarily hashes all of
     * the elements in the map.
     */
    HashMap<K, V> result;
    try {
        result = (HashMap<K, V>) super.clone();// here
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(e);
    }

    // Restore clone to empty state, retaining our capacity and threshold
    result.makeTable(table.length);
    result.entryForNullKey = null;
    result.size = 0;
    result.keySet = null;
    result.entrySet = null;
    result.values = null;

    result.init(); // Give subclass a chance to initialize itself
    result.constructorPutAll(this); // Calls method overridden in subclass!!
    return result;
}

HashMap 的迭代器

 private abstract class HashIterator {
    int nextIndex;
    HashMapEntry<K, V> nextEntry = entryForNullKey;
    HashMapEntry<K, V> lastEntryReturned;
    int expectedModCount = modCount;

    HashIterator() {
        if (nextEntry == null) {
            HashMapEntry<K, V>[] tab = table;
            HashMapEntry<K, V> next = null;
            while (next == null && nextIndex < tab.length) {
                next = tab[nextIndex++];
            }
            nextEntry = next;
        }
    }

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

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

        HashMapEntry<K, V> entryToReturn = nextEntry;
        HashMapEntry<K, V>[] tab = table;
        HashMapEntry<K, V> next = entryToReturn.next;
        while (next == null && nextIndex < tab.length) {
            next = tab[nextIndex++];
        }
        nextEntry = next;
        return lastEntryReturned = entryToReturn;
    }

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

上面的是定義在 HashMap 內(nèi)部的迭代器類,在迭代的時(shí)候,外部可以通過調(diào)用 put 和 remove 的方法,來改變正在迭代的對(duì)象。但從設(shè)計(jì)之處,HashMap自身就不是線程安全的,因此HashMap在迭代的時(shí)候使用了一種Fast—Fail的實(shí)現(xiàn)方式,在 HashIterator 里面維持了一個(gè) expectedModCount 的變量,在每次調(diào)用的時(shí)候如果發(fā)現(xiàn) ModCount != expectedModCount,則拋出 ConcurrentModificationException 異常。但本身這種檢驗(yàn)不能保證在發(fā)生錯(cuò)誤的情況下,一定能拋出異常,所以我們需要在使用HashMap的時(shí)候,心里知道這是「非線程安全」的。

HashMap 的序列化

 private void writeObject(ObjectOutputStream stream) throws IOException {
    // Emulate loadFactor field for other implementations to read
    ObjectOutputStream.PutField fields = stream.putFields();
    fields.put("loadFactor", DEFAULT_LOAD_FACTOR);
    stream.writeFields();
        
    stream.writeInt(table.length); // Capacity 寫入容量
    stream.writeInt(size);// 寫入數(shù)量
    for (Entry<K, V> e : entrySet()) {// 迭代寫入key 和value
        stream.writeObject(e.getKey());
        stream.writeObject(e.getValue());
    }
}

private void readObject(ObjectInputStream stream) throws IOException,
        ClassNotFoundException {
    stream.defaultReadObject();
    int capacity = stream.readInt();
    /**下面的代碼和上面的很類似*/
    if (capacity < 0) {
        throw new InvalidObjectException("Capacity: " + capacity);
    }
    if (capacity < MINIMUM_CAPACITY) {
        capacity = MINIMUM_CAPACITY;
    } else if (capacity > MAXIMUM_CAPACITY) {
        capacity = MAXIMUM_CAPACITY;
    } else {
        capacity = Collections.roundUpToPowerOfTwo(capacity);
    }

    makeTable(capacity);// 根據(jù)容量創(chuàng)建

    int size = stream.readInt();// 獲得size大小
    if (size < 0) {
        throw new InvalidObjectException("Size: " + size);
    }

    init(); // Give subclass (LinkedHashMap) a chance to initialize itself
    for (int i = 0; i < size; i++) {
        @SuppressWarnings("unchecked") K key = (K) stream.readObject();
        @SuppressWarnings("unchecked") V val = (V) stream.readObject();
        constructorPut(key, val); // 前面講到的另一個(gè)類似put的方法
    }
}

涉及序列化,需要了解一個(gè) Java 的關(guān)鍵字 transient ,該關(guān)鍵字用來表示一個(gè)域不是該對(duì)象串行化的一部分。當(dāng)一個(gè)對(duì)象被串行化的時(shí)候,transient 型變量的值不包括在串行化的表示中,然而非 transient 型的變量是被包括進(jìn)去的。

總結(jié)

至此,HashMap 算是告一段落,可以看出谷歌處理 HashMap 時(shí)和 Java JDK 里面的方法的不同。雖然谷歌工程師大牛,但是也存在一些問題

  • HashMap的每一次擴(kuò)容都會(huì)重新構(gòu)建一個(gè)length是原來兩倍的Entry表,這個(gè)二倍擴(kuò)容的策略很容易造成空間浪費(fèi)。試想一下,假如我們總共有100萬條數(shù)據(jù)要存放,當(dāng)我put到第75萬條時(shí)達(dá)到閾值,Hash表會(huì)重新構(gòu)建一個(gè)200萬大小的數(shù)組,但是我們最后只放了100萬數(shù)據(jù),剩下的100萬個(gè)空間將被浪費(fèi)。
  • HashMap在存儲(chǔ)這些數(shù)據(jù)的過程中需要不斷擴(kuò)容,不斷的構(gòu)建Entry表,不斷的做hash運(yùn)算,會(huì)很慢。
  • 此外,HashMap獲取數(shù)據(jù)是通過遍歷Entry鏈表來實(shí)現(xiàn)的,在數(shù)據(jù)量很大時(shí)候會(huì)慢上加慢。

在 Android 項(xiàng)目中使用 HashMap,主要針對(duì)小數(shù)據(jù)量的任務(wù)比較ok。

參考鏈接

HashMap源碼分析

Android HashMap源碼詳解

Java HashMap 源碼解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,013評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,146評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,534評(píng)論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,767評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,318評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,074評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,258評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,486評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評(píng)論 1 290
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,993評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,234評(píng)論 2 375

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