Java7 中的 HashMap 的put(),get()簡單解析,remove()方法中保留的一個不是很複雜的問題

1. HashMap 關鍵名詞:  16和0.75 是設計者結合空間和時間考慮的;

     1. capacity     :  當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍;

     2. loadFactor :負載因子,默認爲 0.75;

     3. threshold   :擴容的閾值,或者叫擴容臨界值,等於 capacity * loadFactor;

     4. 一個數組,數組中每個元素組成一個單向鏈表。;

單向鏈表關鍵名詞:

     Entry 類,包含四個屬性:key, value, hash 值, next指向單向鏈表的下一個節點。

1.1 Entry 類:

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

 

2. put()方法簡單介紹:當在鏈表中已經存在相同的hash和key時,覆蓋原值,並將原值返回;

public V put(K key, V value) {
    // 當插入第一個元素的時候,需要先初始化數組大小,即hashmap的大小;
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 爲 null,會將 entry 放到 table[0] 的位置
    if (key == null)
        return putForNullKey(value);
    // 1. 對 key 求 hash 值
    int hash = hash(key);
    // 2. 找到hash值對應的數組下標
    int i = indexFor(hash, table.length);
    // 3. 遍歷 數組中對應下標處的鏈表,比較 key 如果有重複,直接覆蓋,put 方法返回舊值
    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;
        }
    }
    //hashmap在結構上被修改的次數(暫時不解釋)
    modCount++;
    // 4. key 不重複 ,將此 entry 添加到鏈表中
    addEntry(hash, key, value, i);
    return null;
}

 2.1. inflateTable(threshold);方法簡介new HashMap<>() 事實上基本什麼也沒有做,默認擴容的閾值爲16;

/**
     * Inflates the table. 創建數組空間
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize 必須是2的N次冪;
        int capacity = roundUpToPowerOf2(toSize);
        // 計算擴容閾值:capacity * loadFactor
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 初始化數組
        table = new Entry[capacity];
        //此方法不看也罷
        initHashSeedAsNeeded(capacity);
    }

2.2.addEntry(int hash, K key, V value, int bucketIndex) :四個參數:key的hash值,key值,value值,數組下標值;

    //添加Entry的過程
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果當前數組的大小達到或者超過擴容的閾值;且新值要插入的數組位置已有元素則需要擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //擴容按當前數組的2倍長度值處理
            resize(2 * table.length);
            //從新技術hash值
            hash = (null != key) ? hash(key) : 0;
            //計算hash值在新數組中的下標地址
            bucketIndex = indexFor(hash, table.length);
        }
        //將新值放到鏈表的表頭,然後 size++
        createEntry(hash, key, value, bucketIndex);
    }


   
    //利用下標獲取對應的數組
    void createEntry(int hash, K key, V value, int bucketIndex) {
        //利用下標獲取對應的數組
        Entry<K,V> e = table[bucketIndex];
        //將新值放到鏈表的表頭(這裏的代碼是將新建的Entry 給到數組,所以在放在鏈表的表頭位置)
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //數組的大小加一
        size++;
    }

2.3.resize(int newCapacity):擴容按當前數組的2倍長度值處理

    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, initHashSeedAsNeeded(newCapacity));
        //擴容結束,全局變量賦值
        table = newTable;
        //重置擴容閾值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

2.4.transfer(Entry[] newTable, boolean rehash):數據遷移

    // 由於是雙倍擴容,遷移過程中,會將原來 table[i] 中的鏈表的所有節點,分拆到新的數組的 newTable[i]
    // 和 newTable[i + oldLength] 位置上。如原來數組長度是 16,那麼擴容後,原來 table[0] 處的鏈表中
    // 的所有元素會被分配到新數組中 newTable[0] 和 newTable[16] 這兩個位置
 void transfer(Entry[] newTable, boolean rehash) {
        //新數組的大小
        int newCapacity = newTable.length;
        //將老數組中,每個單向鏈表上Entry全部讀出來
        for (Entry<K,V> e : table) {
            while(null != e) {
                //每個不爲null的節點 
                Entry<K,V> next = e.next;
                if (rehash) {
                    //重新計算 key 的hash值
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //算出新數字的下標位置,放入新的數組中
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

3.get()方法簡介:

public V get(Object key) {
        if (key == null)
            //key爲null,從數組table[0]中獲取key==null的entry中的value
            return getForNullKey();
        //通過key,獲取entry 
        Entry<K,V> entry = getEntry(key);
        //entry 不爲null,返回entry中的value
        return null == entry ? null : entry.getValue();
    }

3.1 Entry<K,V> getEntry(Object key):獲取Entry

    final Entry<K,V> getEntry(Object key) {
        //數組大小爲0,返回null
        if (size == 0) {
            return null;
        }
        //計算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        //獲取對應數組上的落表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            //hash值相同,且 key相同的情況下,返回對應Entry
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        //在鏈表中沒有找到,返回null
        return null;
    }

4.V remove(Object key):刪除單個key,注意返回是的K-V中的Value;

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

4.1 final Entry<K,V> removeEntryForKey(Object key) :


    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //計算hash
        int hash = (key == null) ? 0 : hash(key);
        //獲取數組下標
        int i = indexFor(hash, table.length);
        //在數組中的位置table[i],即鏈表的首節點做一次賦值
        Entry<K,V> prev = table[i];
        //該行代碼沒有具體含義
        Entry<K,V> e = prev;
        //遍歷鏈表
        while (e != null) {
            //next 可以是null
            Entry<K,V> next = e.next;
            Object k;
            //比較hash值和key值
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                //hashmap在結構上被修改的次數(暫時不解釋)
                modCount++;
                //匹配到了對應節點,數組的大小減一
                size--;
                //如果要刪除的節點是首節點,則將鏈表中的下一個節點設置爲首節點
                if (prev == e)
                    table[i] = next;
                else
                    //如果首節點不是要刪除的節點,將首節點prev的next存e的next節點
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

removeEntryForKey()提一個問題:while (e != null) {} 遍歷鏈表,假設鏈表的長度爲3,如果比較hash值和key值相同確認,一個entry 後,比如是第二個,那爲什麼此時可以直接做size--;這不是bug?

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