java-EnumMap、IdentityHashMap、WeakHashMap源碼分析

  • EnumMap源碼分析
    1、介紹
    EnumMap是與枚舉類相結合的Map類。跟hash沒有多大關係,雖然本文中另外兩種與HashMap有關,但是EnumMap與HashMap關係並不大。
    EnumMap就是專門與枚舉類結合形成Map的key-value對結構。值的注意的是,EnumMap中雖然也存儲的是key-value對的數據,但是內存實現上卻採用的是數組結構。key存儲一個數組結構,value也用一個對應的數組結構。
    2、構造器
public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        //保存枚舉類的所有枚舉值到數組中
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }

    public EnumMap(EnumMap<K, ? extends V> m) {
        keyType = m.keyType;
        keyUniverse = m.keyUniverse;
        vals = m.vals.clone();
        size = m.size;
    }

    public EnumMap(Map<K, ? extends V> m) {
        //如果m類是EnumMap類,與上面的構造器一樣
        if (m instanceof EnumMap) {
            EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m;
            keyType = em.keyType;
            keyUniverse = em.keyUniverse;
            vals = em.vals.clone();
            size = em.size;
        } else {
            //如果不是EnumMap類,則不允許vals數組長度爲空
            if (m.isEmpty())
                throw new IllegalArgumentException("Specified map is empty");
            keyType = m.keySet().iterator().next().getDeclaringClass();
            keyUniverse = getKeyUniverse(keyType);
            vals = new Object[keyUniverse.length];
            putAll(m);
        }
    }

上面是EnumMap的三個構造器,有構造器知道,該類存儲的key-value對,key值類型必須是枚舉類型。枚舉類型的值通過該方法getKeyUniverse(keyType)獲取枚舉類數組,keyUniverse 數組存放key集合。vals數組存放value數據。
2、put方法

public V put(K key, V value) {
        typeCheck(key);
        int index = key.ordinal();
        Object oldValue = vals[index];
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }
    private void typeCheck(K key) {
        Class keyClass = key.getClass();
        if (keyClass != keyType && keyClass.getSuperclass() != keyType)
            throw new ClassCastException(keyClass + " != " + keyType);
    }

put方法存入key-value對。由於key值是固定的枚舉類的常量值,並且在構造器初始化的過程中,已經獲取到了,這裏put方法中在此傳入key值,是爲了vals數組中保存value值。
首先檢查key類型是否是構造器初始化過程中綁定的枚舉類型,如果不是,拋出異常ClassCastException。
然後又key值,計算value存儲在vals數組中的索引值,然後將value值存入。如果原來已有value值,則替換原來的value值。
由此可見,vals長度最多隻能是枚舉類常量值的個數。key值只能是枚舉類常量值。
通過put方法我們已經基本瞭解了EnumMap類的特點了,其他方法都是按照這個思想進行的。

public void putAll(Map<? extends K, ? extends V> m) {
        if (m instanceof EnumMap) {
            EnumMap<? extends K, ? extends V> em =
                (EnumMap<? extends K, ? extends V>)m;
            if (em.keyType != keyType) {
                if (em.isEmpty())
                    return;
                throw new ClassCastException(em.keyType + " != " + keyType);
            }

            for (int i = 0; i < keyUniverse.length; i++) {
                Object emValue = em.vals[i];
                if (emValue != null) {
                    if (vals[i] == null)
                        size++;
                    vals[i] = emValue;
                }
            }
        } else {
            /**
            * 這裏的putAll(m)會調用父類的方法
            * 父類該方法的實現上,均採用一一放入的put方法進行
            * 數據的存入。
            * 此時會調用put(e.getKey(), e.getValue());
            * 子類put方法會被調用,所以回到了該類中的put方法
            * put方法中會檢驗key值的類型,如果不匹配就不能放入,
            * 拋出異常
            */
            super.putAll(m);
        }
    }

keyUniverse就是初始化時保存的枚舉類常量值數組。通過上面的方法,非常簡單保存集合中的數據。並且EnumMap採用數組保存key-value對,管理起來很方便,邏輯不復雜。

3、get方法

public V get(Object key) {
        return (isValidKey(key) ?
                unmaskNull(vals[((Enum)key).ordinal()]) : null);
    }
    /**
     * Returns true if key is of the proper type to be a key in this
     * enum map.
     * 檢查key值是否是有效值
     * key值不能爲null
     * 同時key的類型必須是初始化時的枚舉類類型或者
     * 其子類,否則就是無效值
     */
    private boolean isValidKey(Object key) {
        if (key == null)
            return false;
        // Cheaper than instanceof Enum followed by getDeclaringClass
        Class keyClass = key.getClass();
        return keyClass == keyType || keyClass.getSuperclass() == keyType;
    }

get方法很簡單,只要key傳入的合理的參數,很快就能得到結果。O(1)的時間複雜度。通過上面的put方法和get方法,結合起來看,EnumMap類進行key-value保存的時候,key專門用一個數組進行保存,然後vaule值存放在數組vals的相對應的位置上。當需要獲取指定key的value時,直接通過key即可得到數組vals對應位置上的值。並且不會出現衝突的問題。因爲Enum類的各個枚舉值不相同。

4、刪除方法

public V remove(Object key) {
        if (!isValidKey(key))
            return null;
        int index = ((Enum)key).ordinal();
        Object oldValue = vals[index];
        vals[index] = null;
        if (oldValue != null)
            size--;
        return unmaskNull(oldValue);
    }

    private boolean removeMapping(Object key, Object value) {
        if (!isValidKey(key))
            return false;
        int index = ((Enum)key).ordinal();
        if (maskNull(value).equals(vals[index])) {
            vals[index] = null;
            size--;
            return true;
        }
        return false;
    }

    /**
     * Returns true if key is of the proper type to be a key in this
     * enum map.
     * 檢查key值是否是有效值
     * key值不能爲null
     * 同時key的類型必須是初始化時的枚舉類類型或者
     * 其子類,否則就是無效值
     */
    private boolean isValidKey(Object key) {
        if (key == null)
            return false;
        // Cheaper than instanceof Enum followed by getDeclaringClass
        Class keyClass = key.getClass();
        return keyClass == keyType || keyClass.getSuperclass() == keyType;
    }

刪除方法很簡單,key值是不會刪除的,因爲key值是枚舉類常量值,只會把vals數組中對應位置上的value值刪除。
remove(key)是把key值對應位置上vals數組中的值刪除。
removeMapping(key,value)是把對應位置上與valus相等的vals數組中的數據刪除。如果不等,不刪除。

5 是否存在指定的數據方法

/**
     * @param key the key whose presence in this map is to be tested
     * @return <tt>true</tt> if this map contains a mapping for the specified
     * 首先檢驗key值是否是有效值
     * 然後獲取對應的key值的value值是否爲null
     * 這裏需要注意的是,put方法存放key-value對的時候,
     * 如果value值爲null,則會通過maskNull()方法將null值,轉換成NULL對象
     *所以實際存入vals數組中的value值不爲null值。
     */
    public boolean containsKey(Object key) {
        return isValidKey(key) && vals[((Enum)key).ordinal()] != null;
    }

    private boolean containsMapping(Object key, Object value) {
        return isValidKey(key) &&
            maskNull(value).equals(vals[((Enum)key).ordinal()]);
    }

上面兩個方法看明白的話,就很容易明白EnumMap類是如何進行key-value對的管理的。key值初始化的時候已經獲取,保存在了數組中,value值是通過put或者putAll方法添加到數組中的,containsKey(Object key)方法不是通過key值數組判斷的,而是通過key值對應位置上的value值是否存在進行判斷的。也就是說key值對應的vals數組中是否有值,決定了EnumMap類對象是否包含key值。
EnumMap類的其他方法就沒什麼說的了,大家看看就可以了,只要弄懂了HashMap類,其他的類就不在話下了。

  • IdentityHashMap類源碼分析
    該類特點是:只有全等的key值,該類纔會認爲兩個key值相等。比如new String(“11”) 與new String(“11”),這兩個對象就不是全等,而一般的HashMap則認爲上面兩個對象是相等的。
    並且該類非常有意思的是,在key-value數據的存儲上,類似於HashMap,採用map數組進行存儲,但是key-value不是利用鏈表解決衝突,而是繼續計算下一個索引,把數據計算在下一個有效索引的數組中,也就是數據全部存儲map數組中,並且table[i]=key 則table[i+1]=value。key-value緊挨着存儲在map數組中。
    1、構造器
/**
     * Constructs a new, empty identity hash map with a default expected
     * maximum size (21).
     * 採用默認容量的構造器
     */
    public IdentityHashMap() {
        init(DEFAULT_CAPACITY);
    }

    /**
     * Constructs a new, empty map with the specified expected maximum size.
     * Putting more than the expected number of key-value mappings into
     * the map may cause the internal data structure to grow, which may be
     * somewhat time-consuming.
     *
     * @param expectedMaxSize the expected maximum size of the map
     * @throws IllegalArgumentException if <tt>expectedMaxSize</tt> is negative
     * 設定容量大小的構造器
     * 參數期望的最大容量expectedMaxSize不能爲負數,否則異常。
     * 參數expectedMaxSize並不代表實際的容量大小。
     * 通過capacity(expectedMaxSize)方法我們會發現,
     * 實際的容量要大一些。
     */
    public IdentityHashMap(int expectedMaxSize) {
        if (expectedMaxSize < 0)
            throw new IllegalArgumentException("expectedMaxSize is negative: "
                                               + expectedMaxSize);
        init(capacity(expectedMaxSize));
    }

    /**
     * Returns the appropriate capacity for the specified expected maximum
     * size.  Returns the smallest power of two between MINIMUM_CAPACITY
     * and MAXIMUM_CAPACITY, inclusive, that is greater than
     * (3 * expectedMaxSize)/2, if such a number exists.  Otherwise
     * returns MAXIMUM_CAPACITY.  If (3 * expectedMaxSize)/2 is negative, it
     * is assumed that overflow has occurred, and MAXIMUM_CAPACITY is returned.
     * 將參數期望的容量大小expectedMaxSize擴大1.5倍
     * 之後按照比改制大的最小2進制數設定容量大小。
     * 返回的result值是實際的容量大小。
     */
    private int capacity(int expectedMaxSize) {
        // Compute min capacity for expectedMaxSize given a load factor of 2/3
        int minCapacity = (3 * expectedMaxSize)/2;

        // Compute the appropriate capacity
        int result;
        if (minCapacity > MAXIMUM_CAPACITY || minCapacity < 0) {
            result = MAXIMUM_CAPACITY;
        } else {
            result = MINIMUM_CAPACITY;
            while (result < minCapacity)
                result <<= 1;
        }
        return result;
    }
    /**
     * Initializes object to be an empty map with the specified initial
     * capacity, which is assumed to be a power of two between
     * MINIMUM_CAPACITY and MAXIMUM_CAPACITY inclusive.
     * 極限容量是initCapacity值的三分之二
     * 而數組長度確實initCapacity的兩倍!
     * 由此可見IdentityHashMap比HashMap耗費內存空間。
     * 等於說,當用到數組長度的三分之一的時候就要進行擴容操作。
     * 可見內存空間消耗有多大。
     */
    private void init(int initCapacity) {
        // assert (initCapacity & -initCapacity) == initCapacity; // power of 2
        // assert initCapacity >= MINIMUM_CAPACITY;
        // assert initCapacity <= MAXIMUM_CAPACITY;

        threshold = (initCapacity * 2)/3;
        table = new Object[2 * initCapacity];
    }

    /**
     * @param m the map whose mappings are to be placed into this map
     * @throws NullPointerException if the specified map is null
     */
    public IdentityHashMap(Map<? extends K, ? extends V> m) {
        // Allow for a bit of growth
        this((int) ((1 + m.size()) * 1.1));
        putAll(m);
    }

通過構造器我們知道,IdentityHashMap在初始化的時候就已經構造比較大的map數組以解決可能的衝突問題,以便將數據都存儲在數組中。同時爲了提高查詢效率,極限容量設置的比較小,只有數組長度的三分之一。但是問題也來了,佔用了太大的內存空間。也就是有效利用的空間不足數組總長度的三分之一。

2 put方法

/**
     * 該方法最爲重要。通過該方法我們知道,IdentityHashMap類
     * 在存放key-value對時,不採用鏈表解決衝突,而是通過
     * nextKeyIndex(i,len)方法找到下一個存放數據的索引值,如果該索引
     * 處沒有值則存放數據,如果有值,繼續nextKeyIndex(i,len)進行查找
     * 直到在數組中找到合適的存放位置。
     * 同時,如果找到全等的key值,說明已經存放過key,
     * 則用新value值替換舊value值。
     * put方法最後,進行resize檢查。
     * 如果存放下一個數據的長度>=極限容量,則進行擴容。
     * 通過put方法,我們可以瞭解到爲什麼在初始化table數組的時候,
     * 把數組長度定義爲設計容量的兩倍了。
     * 在init()方法中,我們知道,極限容量設計爲數組長度的三分之一
     * 說明,當存放的數組達到數組長度的三分之一的時候就要進行擴容
     * 可見IdentityHashMap在內存空間中的消耗有多大。
     */
    public V put(K key, V value) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        Object item;
        while ( (item = tab[i]) != null) {
            if (item == k) {
                V oldValue = (V) tab[i + 1];
                tab[i + 1] = value;
                return oldValue;
            }
            i = nextKeyIndex(i, len);
        }
        modCount++;
        tab[i] = k;
        tab[i + 1] = value;
        if (++size >= threshold)
            resize(len); // len == 2 * current capacity.
        return null;
    }

    /**
     * @param newCapacity the new capacity, must be a power of two.
     * 擴容方法
     * 擴展爲原來數組長度的兩倍。
     * 擴容之後進行數據的轉移,拷貝到新數組當中。
     * 循環內部手動進行了數據的清除,設置舊數組中的無用引用爲null.
     * 個人理解原因是:數組長度較大,佔用內存空間比較大,
     * 及時釋放內存空間是王道!
     * 由於擴容之後空間一定夠用,所以直接將原來數組中的數據
     * 存放到新數組對應的位置即可。
     * 並且數組存放不存在鏈表,只是數組中,所以管理起來比較方便。
     */
    private void resize(int newCapacity) {
        // assert (newCapacity & -newCapacity) == newCapacity; // power of 2
        int newLength = newCapacity * 2;
        Object[] oldTable = table;
        int oldLength = oldTable.length;
        if (oldLength == 2*MAXIMUM_CAPACITY) { // can't expand any further
            if (threshold == MAXIMUM_CAPACITY-1)
                throw new IllegalStateException("Capacity exhausted.");
            threshold = MAXIMUM_CAPACITY-1;  // Gigantic map!
            return;
        }
        if (oldLength >= newLength)
            return;

        Object[] newTable = new Object[newLength];
        threshold = newLength / 3;

        for (int j = 0; j < oldLength; j += 2) {
            Object key = oldTable[j];
            if (key != null) {
                Object value = oldTable[j+1];
                oldTable[j] = null;
                oldTable[j+1] = null;
                int i = hash(key, newLength);
                while (newTable[i] != null)
                    i = nextKeyIndex(i, newLength);
                newTable[i] = key;
                newTable[i + 1] = value;
            }
        }
        table = newTable;
    }

    /**
     * @param m mappings to be stored in this map
     * @throws NullPointerException if the specified map is null
     * putAll方法,使用put方法進行數據的複製。
     * 沒什麼說的。原理同上。
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        int n = m.size();
        if (n == 0)
            return;
        if (n > threshold) // conservatively pre-expand
            resize(capacity(n));

        for (Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }
    /**
     * Circularly traverses table of size len.
     * 計算下一個key值出現在數組處的索引值
     */
    private static int nextKeyIndex(int i, int len) {
        return (i + 2 < len ? i + 2 : 0);
    }

通過上面的方法我們知道,該類在完成key-value對的存放時,是挨着存放key-value對到數組中。以步進2爲間隔進行數據填充。

3、查詢獲取數據方法

/**
     * 通過key值獲取value對象。
     * 通過程序會發現,item==k,說明只有當全等的時候
     * 纔會返回對象,否則找不到value值返回null。
     * 同時,item==k的情況下,tab[i+1]爲value值。
     * 說明數組i處存放key值,i+1處存放value值。
     * 這也解釋了初始化數組時候2倍長度的原因了。
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        while (true) {
            Object item = tab[i];
            if (item == k)
                return (V) tab[i + 1];
            if (item == null)
                return null;
            i = nextKeyIndex(i, len);
        }
    }

    /**
     * @param   key   possible key
     * @return  <code>true</code> if the specified object reference is a key
     *          in this map
     * @see     #containsValue(Object)
     * 是否包含指定的key值。與上面的get(key)方法類似
     * 原理同上。
     */
    public boolean containsKey(Object key) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        while (true) {
            Object item = tab[i];
            if (item == k)
                return true;
            if (item == null)
                return false;
            i = nextKeyIndex(i, len);
        }
    }

    /**
     * @param value value whose presence in this map is to be tested
     * @return <tt>true</tt> if this map maps one or more keys to the
     *         specified object reference
     * @see     #containsKey(Object)
     * 是否包含指定的value值。
     * 循環中i以2位步長進行循環遍歷
     * 說明數組偶數處存放value值。
     *      IdentityHashMap<String, String> iden =
                            new IdentityHashMap<>();
            iden.put(null, null);
            iden.put(null, null);
            System.out.println(iden);
            System.out.println(iden.containsKey(null));
            System.out.println(iden.containsValue(null));
            上面的運行結果是:
            {null=null}
            true
            true
     * 說明存放的全等的key值會替換原來的value值。
     * 存放的key==null的情況下,會通過maskKey(key)方法
     * 將key=null的值,替換爲NULL_KEY對象。
     * 請結合put方法查看代碼。
     */
    public boolean containsValue(Object value) {
        Object[] tab = table;
        for (int i = 1; i < tab.length; i += 2)
            if (tab[i] == value && tab[i - 1] != null)
                return true;
        return false;
    }

    /**
     * @param   key   possible key
     * @param   value possible value
     * @return  <code>true</code> if and only if the specified key-value
     *          mapping is in the map
     * 是否包含指定的key-value對。
     * 原理和上面的containsKey containsValue類似
     */
    private boolean containsMapping(Object key, Object value) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        while (true) {
            Object item = tab[i];
            if (item == k)
                return tab[i + 1] == value;
            if (item == null)
                return false;
            i = nextKeyIndex(i, len);
        }
    }

上面三個是查詢map數組中數據的方法,上面的方法依次通過獲取key在map數組中的索引進行查詢,知道查詢到結果爲止。

4、刪除數據

/**
     * 使用全等的方式比較key值。
     * 通過key值,得到hash值,然後得到索引值。
     * 如果存在key值,則刪除對應位置上的數據。同時size-1.
     * 並且使用closeDeletion(i)將數組後面的數據重新remap
     * 存放在數組的相應位置當中。
     */
    public V remove(Object key) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        while (true) {
            Object item = tab[i];
            if (item == k) {
                modCount++;
                size--;
                V oldValue = (V) tab[i + 1];
                tab[i + 1] = null;
                tab[i] = null;
                closeDeletion(i);
                return oldValue;
            }
            if (item == null)
                return null;
            i = nextKeyIndex(i, len);
        }
    }

    /**
     * 刪除指定的key-value對。
     * 原理同上面的remove類似。
     * 通過key值得到hash值,然後得到key值在數組中的索引值
     * 有了索引值一一比較key值,只有和目標key值==時,纔會刪除
     * 該索引處的數據。
     * 刪除之後,通過closeDeletion(i)方法將後面的數據重新rehash
     * 重新在數組中進行存放。
     * 如果沒有找到==的key值,則返回false,說明刪除不成功。
     */
    private boolean removeMapping(Object key, Object value) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);

        while (true) {
            Object item = tab[i];
            if (item == k) {
                if (tab[i + 1] != value)
                    return false;
                modCount++;
                size--;
                tab[i] = null;
                tab[i + 1] = null;
                closeDeletion(i);
                return true;
            }
            if (item == null)
                return false;
            i = nextKeyIndex(i, len);
        }
    }

    /**
     * 該方法是在刪除數組中的數據的時候重新rehash數組中的元素
     * 重新存放在數組中。
     * 關於if語句中的判斷有點小麻煩。
     * 這個判斷需要了解hash值的計算。
     * 不過不影響我們大概知道是怎麼回事。
     * 同時我們也可以知道的是,刪除元素並不會使數組的長度減小。
     * 這一點比較重要。
     */
    private void closeDeletion(int d) {
        Object[] tab = table;
        int len = tab.length;
        Object item;
        for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
             i = nextKeyIndex(i, len) ) {
            int r = hash(item, len);
            if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
                tab[d] = item;
                tab[d + 1] = tab[i + 1];
                tab[i] = null;
                tab[i + 1] = null;
                d = i;
            }
        }
    }

由於IdentityHashMap採用在數組中保存key-value數據,並以加長的數組來解決可能引起的衝突,所以數據刪除起來比較方便,只不過只有全等的情況下,纔會刪除key值所對應的value。同時,由於刪除一對數據之後導致後面的數據遍歷不到,所以當刪除一對數據之後,需要對後面的數據重寫在map數組上面定位。

5 相等方法和hashcode方法

/**
     * 比較兩個IdentityHashMap對象是否相等
     * equals方法可以比較Map類對象,只要entrySet().equals(m.entrySet())返回
     * true就可以了。
     * 1、如果參數o屬於IdentityHashMap類對象,直接使用containsMapping方法逐一
     *比較每個key-value對數據是否相等即可。
     * 2、如果參數o不屬於containsMapping類對象,則使用entrySet()方法得到的
     * EntrySet集合進行比較。
     * 通過查找該類中的EntrySet內部類,沒有equals方法,說明覆用父類AbstractSet
     * 中的equals方法,通過查找父類的equals方法,我們發現,
     * AbstractSet類又調用了AbstractCollection類的
     * containsAll(Collection<?> c)方法。經過一系列的調用
     * 最終通過比較的是參數o集合的每個數據的(o.equals(it.next()))方法
     * 進行判斷的。
     * 也就是說,如果參數o不是IdentityHashMap類對象,則使用參數o的equals方法
     * 進行比較,不要求==全等。
     */
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof IdentityHashMap) {
            IdentityHashMap m = (IdentityHashMap) o;
            if (m.size() != size)
                return false;
            Object[] tab = m.table;
            for (int i = 0; i < tab.length; i+=2) {
                Object k = tab[i];
                if (k != null && !containsMapping(k, tab[i + 1]))
                    return false;
            }
            return true;
        } else if (o instanceof Map) {
            Map m = (Map)o;
            return entrySet().equals(m.entrySet());
        } else {
            return false;  // o is not a Map
        }
    }

    /**
     * 計算hash值。
     * 最終的hash值與key和value都有關
     * 保證每個key-value對的hash值是唯一的。
     */
    public int hashCode() {
        int result = 0;
        Object[] tab = table;
        for (int i = 0; i < tab.length; i +=2) {
            Object key = tab[i];
            if (key != null) {
                Object k = unmaskNull(key);
                result += System.identityHashCode(k) ^
                          System.identityHashCode(tab[i + 1]);
            }
        }
        return result;
    }

這兩個方法是該類的關鍵所在,hashcode的計算不僅僅和key值有關,而且和value值有關,這樣就保證了key-value對具備唯一的hash值。同時通過重寫equals方法,判定只有key值全等情況下才會判斷key值相等。這就是IdentityHashMap與普通HashMap不同的關鍵所在。

  • WeakHashMap源碼分析
    1、介紹
    WeakHashMap採用弱引用隊列關聯map數組中存儲的數據,該類與普通HashMap類似,解決衝突一樣採用鏈表解決。瞭解了HashMap再來了解WeakHashMap會很容易上手。之所以採用採用WeakHashMap該類,是因爲通過該類可是實現緩存,在內存空間很緊張情況下,使用該類,避免強引用佔用大量的內存,銷燬掉不用或者過時的對象,較早的釋放空間。
    該類主要的特點就是使用引用隊列,將Entry對象與引用隊列關聯起來,使得每個Entry對象都是弱引用,先看內部類Entry的定義
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        int hash;
        Entry<K,V> next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            /**
            * 這句super(key,queue);
            * 就把Entry對象引用隊列關聯了起來。
            * 此時的Entry對象是弱引用對象,弱引用WeakReference
            * 的構造器new WeakReference(o,q)
            * 意思就是WeakReference對象引用o對象,當弱引用被垃圾回收
            * 則o對象就沒有引用了,也會被垃圾回收,此時WeakReference對象
            * 會被加入到引用隊列queue當中去。
            * 在這裏,我們可以吧Entry對象看作是增強了參數的WeakReference對象。
            */
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }

        @SuppressWarnings("unchecked")
        public K getKey() {
            return (K) WeakHashMap.unmaskNull(get());
        }

        public V getValue() {
            return value;
        }

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

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

        public int hashCode() {
            K k = getKey();
            V v = getValue();
            return ((k==null ? 0 : k.hashCode()) ^
                    (v==null ? 0 : v.hashCode()));
        }

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

這是WeakHashMap類中內部類Entry的定義,這也就是它與普通HashMap不同的關鍵所在。Entry構造器中,將每個Entry與引用隊列關聯,而每個Entry對象就是一個弱引用!!!這個弱引用指向key值。
2、WeakHashMap構造器

 /**
     * 這裏構造器的方法與HashMap類的構造器一樣
     */
    public WeakHashMap(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);
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        table = newTable(capacity);
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    }

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

    public WeakHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public WeakHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY),
             DEFAULT_LOAD_FACTOR);
        putAll(m);
    }

通過與HashMap比較,我們發現,兩者的構造器上並沒有什麼區別,可以說一樣,這就說明WeakHashMap與普通HashMap在存儲數據上是一樣的。

3 put方法

public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);

        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
            if (h == e.hash && eq(k, e.get())) {
                V oldValue = e.value;
                if (value != oldValue)
                    e.value = value;
                return oldValue;
            }
        }

        modCount++;
        Entry<K,V> e = tab[i];
        tab[i] = new Entry<>(k, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }
    /**
     * Expunges stale entries from the table.
     * 該方法是WeakHashMap類的核心方法
     * 每次在進行getSize() getTable()方法是都要調用該方法
     * 該方法實現的功能是:
     * 通過遍歷引用隊列當中保存的已經回收的弱引用對象
     * 將原map 數組中的引用清除,map數組中只保留還沒有回收的弱引用對象。
     * queue.poll()彈出的是弱引用對象,該類中的Entry集成了WeakReference類
     * 方法中利用兩層循環:一層循環遍歷引用隊列中的值,另一層循環遍歷
     * map數組中的值,當在map數組中發現由於引用隊列中相同的引用
     * 則把應用變量從map數組中刪除。更新map數組長度
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

    /**
     * 獲取map數組
     * 利用expungeStaleEntries()方法刷新map數組
     */
    private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
    }

put方法存儲數據,key值如果已經存在,則替換原來的value,如果不存在,則找到合適的位置填充。可以看出出現衝突的情況下,是使用鏈表解決的。這和一般HashMap一致。不同的地方是,在存放數據之前,使用getTable()方法首先獲取一次map數組,調用了expungeStaleEntries()方法,上面的註釋部分我儘量給出了自己的理解。
可以說expungeStaleEntries()方法是保證WeakHashMap正常運行的關鍵方法。

4、獲取數據方法

/**
     * @see #put(Object, Object)
     * 這裏爲什麼沒有使用getEntry(Object key)方法
     * 來獲取value值呢??代碼一樣的呀
     */
    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }

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

    Entry<K,V> getEntry(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null && !(e.hash == h && eq(k, e.get())))
            e = e.next;
        return e;
    }

上面是獲取數據的方法,與普通HashMap區別不大,主要在於每次進行數據獲取前,先獲取map數組。

5、刪除數據

public V remove(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && eq(k, e.get())) {
                modCount++;
                size--;
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return e.value;
            }
            prev = e;
            e = next;
        }

        return null;
    }

    boolean removeMapping(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Entry<K,V>[] tab = getTable();
        Map.Entry<?,?> entry = (Map.Entry<?,?>)o;
        Object k = maskNull(entry.getKey());
        int h = hash(k);
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return true;
            }
            prev = e;
            e = next;
        }

        return false;
    }

刪除也比較簡單,與普通的HashMap沒有什麼特殊的地方.就不多說了。

總結一下:
上面的三個類中的迭代器我都沒有給出,不像之前的兩篇博文源碼全部給出,只要你看懂HashMap類的源碼部分,其他的類可以說觸類旁通!不在話下!重要的區別就在於各個類的特點上面。

1、EnumMap類必須和枚舉類相關聯,key值只能是枚舉類的常量值。value使用數組存儲,長度就是枚舉類的常量值的個數。
2、IdentityHashMap類使用全等判斷key值,不全等的key值,就認爲是兩個對象,可以放入map數組中。並且IdentityHashMap不用鏈表解決衝突,而是使用較大的map數組存儲全部的key-value對。同時key-value對挨着存儲在map數組的奇數位和偶數位上。因此,IdentityHash佔用較大的內存空間。真正使用的map數組的有限長度最多佔總長度的三分之一。
3、WeakHashMap類使用引用隊列存儲回收的弱引用。主要用於內存空間緊張的情況。使用此類時需要小心應對。因爲說不定什麼時候,map數組中的數據已經被回收掉了。
該類的定義是:

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> 

這就說明該類不能被序列化、反序列化和克隆!!!
想來也挺好理解的,既然存儲的是弱引用,如果克隆的話,必須是深拷貝纔行。如果要序列化,可能存在的情況是,map數組已空,沒有必要,還有就是既然用到了WeakHashMap,說明內存空間已經緊張,沒必要進行克隆或者序列化反序列化。
不過,在該類的entrySet()方法中返回的Set

public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }

    private class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            Entry<K,V> candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);
        }

        public boolean remove(Object o) {
            return removeMapping(o);
        }

        public int size() {
            return WeakHashMap.this.size();
        }

        public void clear() {
            WeakHashMap.this.clear();
        }
        /**
        *深拷貝
        * 這裏很容易理解
        * 如果是淺拷貝,目標數組中還是弱引用對象那麼可能會被垃圾回收
        * 深拷貝才能保證拷貝到目標數組的對象是強引用
        */
        private List<Map.Entry<K,V>> deepCopy() {
            List<Map.Entry<K,V>> list = new ArrayList<>(size());
            for (Map.Entry<K,V> e : this)
                list.add(new AbstractMap.SimpleEntry<>(e));
            return list;
        }

        public Object[] toArray() {
            return deepCopy().toArray();
        }

        public <T> T[] toArray(T[] a) {
            return deepCopy().toArray(a);
        }
    }

這裏面的toArray()方法就是進行深拷貝獲取map數組中的值的。想想也是對的,如果是淺拷貝,可能已經被回收了。獲取值並沒有多大用處了。基本也解釋了爲什麼不實現克隆和序列化機制了。

以上都是個人的見解。如果有什麼不對或者問題,歡迎大家留言評論!多多交流~!共同進步!!^_^【握手~】

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