1. 程式人生 > >jdk原始碼閱讀之HashMap(一)

jdk原始碼閱讀之HashMap(一)

HashMap是一個散列表,這個散列表包含的雜湊桶的格式為2^n個,而且必須滿足這個條件,原因在getNode(int hash, Object key)這個方法的分析中給出。HashMap允許null的鍵值對,null的鍵值對總會被對映到第一個雜湊桶中。HashMap裡面的鍵值對並沒有順序之分,裡面的結構是使用陣列+連結串列或者是紅黑樹,是否將連結串列變成紅黑樹的條件是,一個桶裡面節點的個數大於MIN_TREEIFY_CAPACITY

static final int MIN_TREEIFY_CAPACITY = 64;

HashMap的容量是指雜湊桶的個數,在建構函式中傳入,如果構造時傳入的引數不滿足這個條件,內部會做相應的調整,以滿足,HashMap的建構函式如下:

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);
    }
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

預設的HashMap大小為16

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

HashMap是一個散列表,散列表的節點是Node這個類

//Node實現了Map類裡面的Entry介面
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;	//節點的雜湊碼
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
		/* 節點的雜湊碼,注意這個並不是用來定位key的位置的雜湊碼方法,
		* 獲得key的雜湊碼的方法是HashMap.hash方法,使用這個雜湊碼來決
		* 定該key存放在哪個雜湊桶內
		*/
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

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

        public final boolean equals(Object o) {
        //相等的依據是兩者引用相等,或者兩者的key、value分別相等
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

每個節點的雜湊碼是以key的雜湊碼來計算的,具體計算方式如下:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

從上面的雜湊碼計算方式可知
1.如果key為null,則雜湊碼為0,因為HashMap的鍵值對可以為null-null,因此,key為null的節點都會被對映到第一個雜湊桶裡面。
2.如果key不為null,則呼叫key的雜湊碼生成方法,則返回key本身的雜湊碼異或高16位,這樣做的原因是當散列表比較小的時候,讓高位一塊進行運算,更能有效的防止大多數節點對映到同一個雜湊桶.

解決了雜湊碼的生成之後,如何將雜湊碼和儲存位置聯絡起來呢?讓我們先看一下獲取節點的方法

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代表的是雜湊桶的第一個節點,從獲取節點的方式可以知道,雜湊桶的位置
        其實就是節點的雜湊碼位與雜湊桶個數-1,**(注意一個桶裡面的key的雜湊碼並不一樣)**,為什麼要與上雜湊桶個數-1,原因在於
        雜湊桶的個數為2^n,即1<<n,(1<<n)-1可以保證低位全部為1,這樣的話,也不會
        造成陣列的越界,我想這也應該是雜湊桶個數為什麼必須是2^n個的原因
        */
            (first = tab[(n - 1) & hash]) != null) {
            //首先檢查第一個節點是否滿足要求,碰到一個key相等的,即返回這個節點
            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 {
                //遍歷這個雜湊桶,知道獲取到正確的節點,否則返回null
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;

put操作

put操作是向HashMap新增一個鍵值對,如果該鍵已經存在,則更新舊的值

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果HashMap裡面還沒有任何鍵值對,則進行resize操作進行擴容
        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;
            //檢查第一個,如果第一個節點的key已經和需要插入的key相同,則e=p
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            //如果p為紅黑樹的節點,這裡是為了提高查詢效能,當節點衝突太高的時候,使用紅黑樹而不是連結串列,執行完這一句,e仍然指向需要更新的節點或者null.
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //既不是雜湊桶的第一個,又不是紅黑樹,則遍歷連結串列
            //下面的for迴圈只是象徵性的,因為不知道連結串列裡面元素的個數,當滿足一定條件就break
                for (int binCount = 0; ; ++binCount) {
                //已經遍歷到連結串列尾,說明沒有相同的key存在,則插入新的節點
                    if ((e = p.next) == null) {
                    //插入新的節點,這時,e引用的是新節點
                        p.next = newNode(hash, key, value, null);
                        //將連結串列轉化成紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果找到相同的key節點,e引用這個節點
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果執行到這裡,那麼e已經是引用了新的節點,或者舊的相同key的節點
            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;
    }

resize操作

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //oldTab為null的時候,雜湊桶裡面沒有任何元素
    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
    	//舊的容量為0
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        //threshold=capacity*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;
    //如果舊的table不為空,則重新雜湊
    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;
                 //如果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;
}

對於上面舊雜湊桶中元素重新雜湊的部分,進行說明,因為程式碼比較難懂,首先要宣告的是舊的一個雜湊桶中的元素重新雜湊的可能只有兩種:
假如舊雜湊桶個數oldCap為2^5=32個,那麼table的定址範圍為:0~31,對第20個雜湊桶中的元素進行分析,雜湊碼位h,那麼先來看h的可能取值:
h^(32-1)=20 二進位制表示:h ^ 11111=10100
可以得出h的可能:xxx10100
現在重新雜湊,雜湊桶個數翻倍newCap=2^6=64,雜湊桶的位置:
z=h^(64-1)=xxx10100 ^ 111111=x10100 ^ 111111
當x為1時:z=10100,重新雜湊在原位置
當x=0時,z=110100=10100+100000=10100+oldCap,重新雜湊在20+oldCap這個位置,對應程式碼:

newTab[j + oldCap] = hiHead;

這樣上面的程式碼就好理解了,再解釋這幾個變數

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;

loHead 、loTail 分別代表舊雜湊桶重新雜湊之後,仍然待在舊的雜湊桶的元素組成的連結串列首尾;相應的hiHead 、hiTail 分別代表舊雜湊桶重新雜湊之後,被對映到新的雜湊桶的元素組成的連結串列首尾.

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 {
                	//遍歷當前雜湊桶所有的節點,退出時總是有e=p.next,node=e,e指向需要刪除的節點
                    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
                	//根據上面的關係,node為需刪除的節點,去掉其引用,由垃圾回收器回收
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

關於HashMap還有一個很重要的操作,就是遍歷,這一部分放到另一篇