天天看點

Java集合類架構學習 4.1 —— HashMap(JDK1.6)

這篇開始看HashMap,先從1.6的開始,它是基礎。了解了1.6的之後,再看下1.7以及1.8的改進。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 一、基本性質 1、基于哈希表的Map接口實作,使用鍊位址法處理hash沖突。如果hash函數絕對随機均勻,那麼基本操作(get和put)的時間性能基本是恒定的。疊代操作所需的時間大緻與HashMap的容量(hash桶的個數,table.length)和K-V對的數量(size)的 和 成正比,是以,如果疊代性能很重要,不要将初始容量設定得太高(或負載系數太低)。 2、HashMap有兩個影響其性能的參數:初始容量initCapacity,和負載因子loadFactor。容量是哈希表中的hash桶的個數,initCapacity隻是建立哈希表時的容量,loadFactor是衡量哈希表在擴容之前允許達到多少的量度。當哈希表中的條目數量超過loadFactor和目前容量capcity的乘積threshold時,哈希表會擴容為兩倍的大小,并且進行重新散列(重建内部資料結構,各個K-V對重新存儲到新的哈希表中)。 預設負載因子0.75在時間成本和空間成本之間提供了良好的平衡。較高的值loadFactor會減少空間開銷,但會增加查找成本(反映在HashMap類的大多數操作中,包括get和put)。在設定其初始容量時,應考慮映射中的預期條目數(size)及其負載因子(loadFactor),提前設定好。這樣能盡量節省空間,并且減少擴容次數,提高HashMap整體存儲效率。 3、允許null key和null value,null key總是放在第一個hash桶中。 4、非同步,可以使用Collections.synchronizedMap包裝下進行同步,這樣具體實作還是使用HashMap的實作;也可以使用Hashtable,它的方法是同步的,但是實作上可能和HashMap有差別;多數場景下,可以使用ConcurrentHashMap。 5、跟ArrayList一樣,HashMap的疊代器是fail-fast疊代器。 6、實作Cloneable接口,可clone。 7、實作Serializable接口,可序列化/反序列化。 8、HashMap中,Key的hash值(hashCode)會優先于 == 和 equals,這一點後面有解釋。

基本結構的簡單示意圖,可以看下。

Java集合類架構學習 4.1 —— HashMap(JDK1.6)

二、常量和變量 1、常量

/** The default initial capacity - MUST be a power of two. */
static final int DEFAULT_INITIAL_CAPACITY = 16; // 數組table的預設初始化大小,容量必須是2^n形式的數

/**
 * 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; // hash桶最大數量(table數組的最大長度),size超過此數量之後無法再擴容了

/**  The load factor used when none specified in constructor. */ 
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 預設加載因子
           

2、變量

/** The table, resized as necessary. Length MUST Always be a power of two. */
transient Entry[] table; // 底層的hash桶數組,長度必須是2^n,容量不足時可以擴容

/** The number of key-value mappings contained in this map. */
transient int size; // K-V對的數量。注意,為了相容size方法才使用int,HashMap的實際size可能會大于Integer.MAX_VALUE,理論上long類型才是比較好的值,實際中大多數int型也夠用

/** The next size value at which to resize (capacity * load factor). */
int threshold; // 擴容門檻值,一般值為table.length * loadFactor,不能擴容時使用Integer.MAX_VALUE來表示後續永遠不會擴容

/** The load factor for the hash table. */
final float loadFactor; // 加載因子,注意,此值可以大于1

/**
 * 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 volatile int modCount; // 大多數實作類都有的modCount

private transient Set<Map.Entry<K,V>> entrySet = null;
// keySet values繼承使用AbstractMap的父類的屬性
           

三、基本類 也就是每個K-V對的包裝類,也叫作節點,比較基礎的類。

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash; // final的,擴容時hash值還是使用的舊值,隻是重新計算索引再散列

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

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

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

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

    public final String toString() {
        return getKey() + "=" + getValue();
    }

    // 提供給子類實作的方法,在LinkedHashMap中有實作
    void recordAccess(HashMap<K,V> m) {}
    void recordRemoval(HashMap<K,V> m) {}
}
           

四、構造方法與初始化

// 1.6的構造方法是會真正初始化數組的,到了1.7就開始使用懶初始化,在第一次進行put/putAll等操作時才會真正初始化table數組
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) // 用循環找出滿足的2^n
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity]; // 真正初始化table數組
    init(); // 這個方法裡面什麼都沒做
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 預設構造方法,相當于new HashMap(16, 0.75f)
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 真正初始化數組
    init();
}

// loadFactor使用預設值0.75f,因為m是接口類型,可能沒有loadFactor這個屬性
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); // 因為m是一個空的map
}

void init() {
}

// 特化的一個put,使用createEntry而不是addEntry,不會觸發擴容(容量已經設定好了),也不會修改modCount
private void putForCreate(K key, V value) {
    int hash = (key == null) ? 0 : hash(key.hashCode());
    int i = indexFor(hash, table.length);

    /**
     * Look for preexisting entry for key.  This will never happen forclone or deserialize.
     * It will only happen for construction if the input Map is a sorted map whose ordering is inconsistent w/ equals.
     */
    // 因為不同的Map實作中判别“相等”的方式可能不一樣,是以HashMap這裡需要用自己的方式再比較下
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }

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

private void putAllForCreate(Map<? extends K, ? extends V> m) {
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext();) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        putForCreate(e.getKey(), e.getValue());
    }
}

// 在初始化時使用的一個特化的添加節點的方法
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++;
}
           

五、一些内部方法 jdk1.6的主要有兩個,一個hash函數,一個hash桶定位。

/**  Returns index for hash code h. */
// hash桶定位方法,利用length = 2^n的特性,使用位運算加快速度
static int indexFor(int h, int length) {
    return h & (length-1);
}
           

這個方法就是用來把hash值散列到table數組某個位置的方法。 HashMap是利用哈希表來加速查找的集合類。它當中使用的hash值是一個32bit的整數,而HashMap的hash桶的初始數目為16,是無法跟全部整數一一對應的,是以需要根據hash值進行散列,使得不同Entry能均勻存儲到所有hash桶中。最常見的散列方式就是用hash值對hash桶的數目進行取模。十進制中常用的取模方法是%,是用除法實作的。對于2^n這種數,可以利用位運算取模,具體的做法就是 & (2^n-1)。因為除以2^n相當于右移n位,%2^n相當于保留最低的n位,而(2^n-1)這種數的最低的n位1,%2^n就相當于 &(2^n-1)。(2^n-1)這種二進制中有效的1都是從最低位開始連續的1,跟網絡中的子網路遮罩很像(子網路遮罩是從高位開始),有個比較高大上的說法叫做"低位hash掩碼"。 Hashtable是利用取模運算散列定位到hash桶的,雖然通用,但是效率比這HashMap低。 這個方法也是HashMap的容量一定要是2的整數次幂的一個原因。length = capacity,length為2^n的話,h&(length-1)就相當于對length取模。同時(2^n - 1)這種數的所有bit為1的位都是連續的,這樣進行 & 運算能夠利用hash值中最低的n位中的所有位,也就是[0, 2^n - 1]所有值都能取到。& 運算的結果是這個hash桶在table數組的索引,是以也就能夠利用table的所有空間 。如果不是2^n,那麼hash掩碼中最低n位就不全為1,會有0出現,這樣進行 & 運算後這個0對應的位永遠是0,就不能利用這一位的值,造成hash值散列到table中時不夠均勻,table中會有無法被利用的空間。比如length為15,是個奇數,(length-1)為偶數14,最後一位為0,進行&運算後一定是偶數,造成所有table中所有奇數下标的位置無法被利用,浪費15 >> 1 = 7個空間,基本浪費了一半。

/**
 * Applies a supplemental hash function to a given hashCode, which
 * defends against poor quality hash functions.  This is critical
 * because HashMap uses power-of-two length hash tables, that
 * otherwise encounter collisions for hashCodes that do not differ
 * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 */
// HashMap自己的hash函數,是一個擾動函數,主要是為了避免hashCode方法設計的不夠好導緻hash沖突過多
// indexFor方法隻能利用h的最低的n位的資訊,是以使用移位來讓低位能夠附帶一些高位的資訊,充分利用hashCode的所有位的資訊
static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
           

為什麼HashMap不直接使用hashCode,非要自己寫個hash函數呢? 因為hashCode是個32bit數,存放到table數組中時,根據上面的table數組索引方法,可以知道隻有最低n位(HashMap的容量為2^n)被利用到了,高位部分的資訊都丢失了。假設直接使用hashCode,在節點很多,并且hashCode設計得比較好的情況下,低n位也會是随機且均勻分布的。但是在元素不太多、hashCode設計得很爛的情況下,低n位就不夠随機均勻了,這讓hash沖突變多,降低了各種方法的時間效率。 HashMap中的hash算法基本就是把hashCode的高位與低位進行異或運算,讓低位能夠夾帶一些高位的資訊,盡量利用hashCode本身所有位的資訊,來讓indexFor方法的結果盡量随機均勻。多次進行這種運算,hashCode本身的影響就減少了,這也降低了hashCode設計得太差導緻的不良影響 。 這種函數一般叫作擾動函數,就是為了讓數值本身的二進制資訊變亂,某些位能夠夾帶一部分别的位的資訊,得到一個bit位分布盡量随機均勻的新值,減少後續的hash散列沖突。 如果是直接用%操作,并且除數盡量使用大的素數,就基本上能夠利用hashCode的所有位了,讓根據hash值散列到table數組時盡量均勻,這時候就不太依賴hash擾動函數了。Hashtable基本是就是這樣做的(直接使用hashCode,中間多一個變符号操作),不過這樣效率低,其他的一些使用length = 2^n特性的地方也會比HashMap慢不少。

六、擴容 jdk1.6的HashMap的擴容很簡單,實作得很直接。兩個步驟,先建立一個兩倍長度的數組,然後把節點一個個重新散列定位一次。要說的都寫注釋了,其餘的沒什麼單獨好說的。

// table數組擴容
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); // 重設門檻值,注意這裡有點問題。loadFactor可以大于1,newCapacity*loadFactor是個浮點數,
                                                 // 它可能大于Integer.MAX_VALUE,此時強轉後變為Integer.MAX_VALUE,造成後續再也無法擴容。1.7開始修複了這一點
}

// 基本思路是把舊數組的所有節點全都重新“添加”到新數組對應的hash桶中
// 1.6的實作很簡單、直接、直覺,後續版本有改良的實作
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;
            // 這裡是把原來的Entry鍊從頭到尾再“put”到新數組裡面
            // jdk1.6的put是把新節點添加到Entry鍊的最前面,是以transfer執行後,還在同一條Entry鍊(隻有兩條可選,可以看下jdk1.8的注釋,後面我也會說)上的節點的相對順序會颠倒
            // 舉個例子(數字為hash值,非真實值),擴容transfer前,table[0] = 16 -> 32 -> 48 -> 64 -> 80 -> 96,
            //     擴容新數組中變成兩條了,一條是table[0] = 80 -> 48 -> 16,另一條是table[16] = 96 -> 64 -> 32
            // 16, 48, 80(32, 64, 96)還在同一條上,但是它們的相對順序颠倒了,HashMap的整體的疊代順序當然也變了,當然本身它ye不保證疊代順序
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity); // 沒有重新計hash值,隻是重新計算索引
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
           

七、常用方法 1、get get實作比較簡單比較好了解,兩個步驟,先indexFor定位到hash桶 -> 再進行連結清單周遊查找。

public V get(Object key) {
    if (key == null) // key == null 的情況
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];  e != null;  e = e.next) { // indexFor定位hash桶
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) // 周遊連結清單查找
            return e.value;
    }
    return null;
}

// 處理 key == null 的情況
// 根據putForNullKey方法(後面說)可以知道,key == null的節點,一定放在index = 0的hash桶中,判斷null要使用 "=="
private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}
           

這裡專門說下get方法的一個疑問。那就是for循環中的這句代碼:     1.6的:if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 後續版本也有:     1.7的:if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))     1.8的:if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 這裡一起說,後面說1.7,1.8時再貼一份。 1.6中已經在putForNullKey中先行處理了null,明确了到這裡key不可能是null。那麼1.6中為什麼還要加上(k = e.key) == key?合理原因是用 == 能加快比較,比較奇葩的原因是,雖然equals(null)一般都是傳回false,不排除有極個别的惡意的實作是傳回true。

個人關注的疑問是這個: e.hash == hash 這句是否多餘? Java中equals和hashCode的 通正常定:==為true ---> equals為true,equals為true ---> hashCode相等,==為true ---> hashCode相等(具體看api docs中Object類的說明)。後面的一個判斷 ((k = e.key) == key || key.equals(k)) 就是判斷key和e.key是否equals(就是通常意義上的“相等”,null使用==,非null使用equals,已經說了這裡的key不可能為null)。那麼如果後面的條件傳回true,則有 == 或者 equals必定有一個傳回true。再按照上面的通正常定,可以知道hashCode也是一樣的,運算得到的hash也一樣,那麼e.hash == hash就不用比較了一定是true。 這個e.hash == hash存在的比較合理的解釋就是突出hashCode的作用,明确表示: 在HashMap(以及其他的HashXXX)中,Key的hash值(hash值是根據hashCode算出來的,這裡也可以了解為hashCode)的優先于==和equals。HashXXX中在查找key是否”相等“時,先使用hash值(可以了解為hashCode)判斷一次,hash值相等時,再才使用==或者equals判斷。如果一開始比較hash值就不相等,那麼就是認為是不“相等”的對象,不再去管 == 或者equals。如果hash值相等,但是equals/==判斷為不等,這種也視為“不相等”。下面的demo可以展示這一點。

// jdk1.8,請使用1.8運作,1.8的hash函數比較簡單,容易構造資料
// 需要用調試器才看得出來在同一條Entry鍊上,請使用調試器
public class TestHashCode {
    public static void main(String[] args) {
        Key k = new Key();
        Map<Key, String> map = new HashMap<>();
        map.put(k, "1");
        k.i = 2; // 修改hashCode
        map.put(k, "2"); // 現在put了兩個key "equals 且 ==" 的K-V對,hashCode不一樣,實際hash值
        k.i = 16; // 修改hashCode
        map.put(k, "16"); // 現在put了三個key "equals 且 ==" 的K-V對,并且第三個跟第一個在同一條Entry鍊(index = 0)上,hashCode不一樣,實際hash值也不一樣
        System.err.println(map); // 現在這個HashMap有三個K-V對,它們的key都是 "equals 且 ==" 的 ,但是它們的hashCode各不同,算出來的hash值不一樣,在HashMap中這"三個"key是不"相等"的

        Key newK = new Key();
        newK.i = 16;
        map.put(newK, "new16");
        System.err.println(map); // 又添加了一個,并且也在index = 0的Entry鍊上,它的hash值和第三個相等,但是equals判斷不相等,是以在HashMap看來它跟第三個是不"相等"的
        // 因為Key的toString是直接使用Object.toString(),會用到hashCode,是以列印出來的結果中,四個K-V的key看上去都是一樣的
    }

    static class Key {
        int i = 0;
        public int hashCode() {
            return i;
        }
    }
}
           

雖然HashXXX中hashCode優先,但是平時還是不要用這一點,非常迷惑人。而在其他的大多數情況下,==和equals是優先于hashCode的,判斷對象相等基本上都是直接使用 ==或者equals,根本不使用hashCode。 是以大家還是要盡量遵守equals和hashCode的通正常定,不要寫出奇怪的equals和hashCode方法,同時盡量避免修改已經放到HashXXX中的對象中會改變hashCode和equals結果的field。大多數情況,使用不變類,比如String、Integer等,充當key是一個很好的選擇。

2、put方法 實作比較簡單。四個步驟,先indexFor定位到hash桶 -> 再進行連結清單周遊查找,确定是否添加 -> 如果添加就添加在連結清單頭 -> 擴容判斷。要說的都寫注釋上面了。

public V put(K key, V value) {
    if (key == null) // 處理 key == null 的情況
        return putForNullKey(value);
    int hash = hash(key.hashCode()); // indexFor定位hash桶
    int i = indexFor(hash, table.length);
    // 先确認是否添加了“相等”的key
    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))) { // “相等”是指滿足此條件,上面的hash方法中說了
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this); // 此方法HashMap中是空方法,留給子類實作
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i); // 執行真正的添加操作
    return null; // 新添加的key,沒有舊的value,傳回null
}

// 處理 key == null 的情況,總是把它放在index = 0的hash桶中
private V putForNullKey(V value) {
    // 先确認是否已經添加了null key
    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;
}

// 在Entry鍊的頭部插入新的節點,并檢查是否需要擴容
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) // 然後判斷是否要擴容,在把size加1
        resize(2 * table.length); // 把第(threshold + 1)個添加了再擴容為2倍大小(例如,預設構造的HashMap時,在執行put第13個key互不“相等”的K-V時擴容)
}
           

下面簡單畫了個put的示意圖,可以看下。

Java集合類架構學習 4.1 —— HashMap(JDK1.6)

3、remove方法

兩個步驟,先indexFor定位hash桶 -> 然後周遊連結清單,找到“相等的就删除”。

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

// 就是連結清單中節點的删除,很簡單
final Entry<K,V> removeEntryForKey(Object key) {
    int hash = (key == null) ? 0 : hash(key.hashCode()); // 計算hash值
    int i = indexFor(hash, table.length); // 定位hash桶
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    while (e != null) { // 周遊連結清單尋找key“相等”的節點
        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;
}
           

4、其他的一些基本方法 都比較簡單,也沒什麼好說的。

public int size() {
    return size;
}

public boolean isEmpty() {
    return size == 0;
}

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

public void clear() {
    modCount++;
    Entry[] tab = table;
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    size = 0;
}

// 分null、非null兩種情況判斷,也很好了解
public boolean containsValue(Object value) {
    if (value == null)
        return containsNullValue();

    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))
                return true;
    return false;
}

// 處理null value的情況
private boolean containsNullValue() {
    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (e.value == null)
                return true;
    return false;
}

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

    // 這裡使用保守的政策,一點小小的優化完善
    // 直覺的政策(m.size() + size) >= threshold不一定準确,因為兩個map中可能會存在許多K-V重疊,可能會白白地擴容一次
    // numKeysToBeAdded <= threshold 時本身也隻擴容一次,就把這次可能的擴容交給put去進行準确的判斷
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); // 加1是為了有預留白間,避免下一次put就立即擴容
        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());
    }
}
           

八、視圖以及疊代器 這個沒什麼好說的了,本身了解起來比較簡單。

HashMap是重要的基礎,HashSet/LingkedHashMap/LinkedHashSet/ConcurrentHashMap等等基本的集合類,都直接或者間接用到了HashMap。 之是以過來這麼久,還要說1.6的,因為它簡單清楚,把該說的都用盡量直接的方式說出來了。另外,也可以學習一下hash表這種資料結構,離開書本後hash表的學習的第一站,用HashMap是個很好的選擇。

接下來的一篇說下1.7的HashMap,改動并不多,有了1.6的作基礎,了解1.7的也很簡單。