從源碼分析:Java中的Map(三)詳解Java中HashMap的常用方法(構造方法、get、put等)

在上一章中,我們看過了HashMap的結構,並瞭解了其用於儲存數據的兩個基本的數據結構,那麼這一篇文章中就可以來具體地看一看一些具體的方法了。

HashMap的構造方法

首先,我們來看一看HashMap的成員變量:

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

    private static final long serialVersionUID = 362498820763181265L;

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;

    transient Node<K,V>[] table;

    transient Set<Map.Entry<K,V>> entrySet;
    
    // size表示HashMap中存放KV的數量(爲鏈表和樹中的KV的總和)。
    transient int size;
    
    //對HashMap 內容的修改都將增加這個值,在迭代器初始化過程中會將這個值賦給迭代器的  expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map。
    //沒有使用volatile聲明,是爲了避免過度設計,因爲HashMap作爲一個非線程安全的類,並沒有提供任何保證,因此無需付出高昂的代價來保證線程安全。換句話說,在線程不安全的環境下使用HashMap,本身就是不合理的。
    transient int modCount;
    
    // threshold表示當HashMap的size大於threshold時會執行resize操作。 
    int threshold;
    
    final float loadFactor;

看到這裏有這麼多的變量,沒有關係,後面需要用到的時候會具體地講。

之後,來看一下第一個構造方法,也是最基礎的構造方法,因爲其它簽名的構造方法大多要調用這個方法:

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);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

// 返回大於輸入參數且最近的2的整數次冪的數
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

輸入的兩個參數分別爲初始容量與裝載因子,初始容量很好理解,而loadFactor爲裝載因子,用來衡量HashMap“滿”的程度。loadFactor的默認值爲0.75f。計算HashMap的實時裝載因子的方法爲:size/capacity,而不是佔用桶的數量去除以capacity。

構造函數中,首先判斷輸入的初始容量是否大於常量MAXIMUM_CAPACITY,即2的30次方,若大於,則取MAXIMUM_CAPACITY,若小於,則取輸入的參數。這裏爲什麼要取2的30次方呢?是因爲,如前面所說,HashMap的第一層爲數組,而數組是以int爲下標的,而int是32位的,去除一個符號位之後,剩餘31位,又因爲HashMap的容量必須爲2的整數次冪(後面會講到),因此能放入的最大的容量爲2的30次冪。

之後,判斷輸入的裝載因子是否符合要求,若符合要求,則將成員變量中的裝載因子賦爲輸入的值。

之後的操作爲對threshold進行賦值,threshold表示當HashMap的size大於threshold時會執行resize操作,即進行擴容。而對於threshold賦的值是通過方法tableSizeFor得到的,這個方法的作用爲找到大於或等於輸入的參數的最小的2的冪。因此,對於threshold的初值的設定爲不小於初始容量的最小的2的冪。

接下來的兩個構造方法就相對比較簡單了:

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

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

其中的操作較少,而下面的這個構造方法爲將其它Map的對象中的元素放入其中並構造出新的HashMap對象的方法:

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

可以看到,當putMapEntries用在構造函數中時,table肯定爲null,因此會首先用輸入的m的size除以設定的裝載因子再加1,再用上面所提到的tableSizeFor得到threshold,之後再通過遍歷將m的entrySet中的每個entry通過putVal方法放入本HashMap對象中,這個方法的操作我們後面會在講put的時候具體介紹。

HashMap中的get方法

在這一節中我們會講解get相關的方法

在這之前,把兩個比較簡單的實現先貼出來:

public int size() {
    return size;
}

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

接下來就是一個重頭戲了,就是HashMap中的get方法:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

可以看到,get(Object key)方法是依賴getNode(int hash, Object key)方法來實現的,因此,我們先來詳細分析一下getNode(int hash, Object key)方法。

getNode有兩個輸入的參數,分別是鍵的哈希值與鍵本身。在方法中首先取出類對象的成員變量table賦值給方法變量tab,判斷tab的長度(賦給n)是否大於0,之後將輸入的鍵的哈希值與(n - 1)的與運算的結果作爲數組中的下標取出這個位置所儲存的鏈表的頭節點。

這裏之所以用(n - 1)來計算,我們在前面提到過,HashMap裏使用2的整數次冪來作爲長度,因此在二進制下,n - 1中爲後k位(其中k = logn)全爲1的一個數字,與這樣一個數字求與運算,相當於取出哈希值的後k位,這也是前面所說取2的冪作爲長度的一個原因。

取出頭節點後,若頭節點不爲空,則先判斷頭節點是否是我們需要取的節點,判斷方法爲,使用我們輸入的第二個參數,即鍵本身來判斷兩者是否相等,若相等則返回,若不相等,則用遍歷鏈表的方式,不斷比較鏈表中的每個節點。當然,這裏判斷相等的方式有兩種,一種是直接用==來判斷,一種是用euqals()方法來判斷。

還有一點需要注意的是,在判斷完頭節點,要遍歷之後的節點時,先判斷頭節點的類型,是否爲一個TreeNode,這個TreeNode類我們在前一篇文章中已經將結果,是作爲紅黑樹的數據節點的類,因此,若節點的類型爲TreeNode,說明這個鏈表已經轉換爲了一棵紅黑樹,之後應該用紅黑樹中的方法getTreeNode來進行get的操作,而不是遍歷鏈表的方法。

在查找完之後(遍歷鏈表或者紅黑樹),若仍沒有找到所需的元素,則返回null,而get中,若通過getNode找到了所需元素的節點,則將這個節點的值作爲結果返回,否則返回null。

以上就是HashMap中的get的具體實現了。次外,還有一個方法是基於get來實現的,就是containsKey(Object key)方法:

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

HashMap中的put

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

與get方法類似,put是依賴putVal方法來實現的。

putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法的參數較多,分別是要插入元素的哈希值,鍵的值,元素的值,是否不改變已存在的值的標誌位,與是否刪除鏈表頭節點的標誌位(後兩者在put中調用時均置爲false)。

在putVal中,首先也是取出成員變量table,若爲空或長度爲0則調用resize()方法重置大小。之後判斷其所在的桶(找下標的方式與get中相同)是否爲空,若爲空,則新建一個節點。

若桶中不爲空,則先判斷要插入的元素的鍵與頭節點是否相等(哈希值相等,鍵相等-鍵相等即其值相同或調用equals方法判斷相等),則找到了該節點,賦給e等待進一步的處理,若不相等,再通過頭節點的類型是不是TreeNode來判斷當前桶內是一個鏈表還是一棵紅黑樹,若是紅黑樹,則通過紅黑樹的putTreeVal方法來找到應該插入的位置e,若是一個鏈表,則從頭節點不斷向後遍歷,若找到了一個相等(判斷方法同上)的節點,則將這個節點賦給e,若沒有找到一個相等的節點,則創建一個新的節點,將新的節點賦給e。這裏要注意的一句是:if (binCount >= TREEIFY_THRESHOLD - 1),即如果當前桶內的節點的數量達到了8(binCount從0開始計數,因此到7),則會將這個鏈表轉換成一棵紅黑樹,這也是Java8中的對於效率的一個改進。

通過以上的操作,我們找到了應該放入元素的位置e,接下來,首先將其原值取出,作爲oldValue用於返回,之後若標誌位onlyIfAbsent不爲真,則將其值賦爲要修改的新值。之後執行的afterNodeAccess方法是爲了繼承HashMap的LinkedHashMap類服務的,此處暫且不多加敘述。之後返回舊值。

在完成以上幾步之後,若找到的桶不爲空且找到了鍵所對應的原節點,則更新完原節點之後,便直接返回了,但是其它情況下,還要有一些繼續的操作,首先是將modCount自增一次,這個變量是用於HashMap的快速失敗機制的,對HashMap 內容的修改將增加這個值,在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map。

之後,將size自增一次,並與threshold比較,若大於threshold,則擴容一次,擴容的方法在下一節中會詳細講解。

HashMap的擴容方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

以上代碼就是HashMap中的擴容的方法了,首先將數組table取出,並取出其原先的容量oldCap與原先的擴容閾值oldThr,若原先的容量大於0,如果大於MAXIMUM_CAPACITY,則將閾值設爲MAXIMUM_CAPACITY,數組則不變,若小於MAXIMUM_CAPACITY,則設置新的容量newCap爲oldCap的兩倍(靠左移來實現),此時,若新的容量小於MAXIMUM_CAPACITY且老的容量大於等於設定的初始容量,則將新閾值newThr也擴大爲原閾值的兩倍。

若原容量爲0,但是原閾值大於0,則將新容量設爲原閾值。若原閾值也爲0,則將新容量與新閾值都進行初始化。

以上步驟爲設定新的容量與閾值大小的過程,也就是說,到目前,我們已經找到了新的容量與閾值,接下來就是根據這些數據來進行操作了。

首先將新的閾值newThr更新到成員變量threshold中,之後根據新的容量newCap爲長度創建出新的容器數組newTab,並將成員變量table指向這個新的數組。之後就是遍歷老的數組oldTab將所有值賦給newTab了。

HashMap中的remove方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

這篇文章的最後呢,就來一起看一下HashMap中的remove方法。

與前面幾個操作類似,這裏依然是把具體實現的方法拆分出來,然後調用這個具體的方法removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)來實現。

如果詳細地看過了前面幾個方法的講解,這裏應該也相對容易了,流程也是先取出數組table,然後用數組的長度減1去與鍵的哈希值作爲數組下標找到索要找的桶,在桶中取出頭節點,先判斷頭節點是否是我們要刪除的節點,若是,則放入node中作爲記錄以備後續處理,若不是,則判斷頭節點是不是紅黑樹節點,若屬於紅黑樹節點類型,則按照紅黑樹的方式去進行操作,將找到的節點放入node中,若不是紅黑樹節點,則是一個鏈表的節點,接下來就不斷地用do-while語句去取next,若找到了索要刪除的節點,則放入node中。

之後,若找到了node,如果node是一個紅黑樹的節點,則按照紅黑樹的方法來刪除這個節點,如果不是紅黑樹的節點,則屬於鏈表節點,如果node == p說明要刪除的節點爲桶中的頭節點,將node的next賦給桶作爲新的頭節點即可,否則,將前一個節點p的next指向node的next即可。

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