從源碼分析java集合【HashMap】

來源:互聯網
上載者:User

標籤:java集合類   java   java編程   java集合源碼解析   源碼   

Map如我們所知,儲存的是鍵值對,它的基本單位是實現了Map.Entry<K,V>的Node<K,V>,Node 的屬性如下:

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

看定義就能知道它的作用了,能夠看到它儲存了一個指向下一個節點的對象next,由此我們是大概能夠想到它的儲存方式由鏈表進行儲存,其實,通過翻閱資料我們也能夠清楚的知道,HashMap其實是一個鏈表數組。它的結構如:


看到這之後,再看源碼,應該能夠清楚好多。

public class HashMap<K,V> extends AbstractMap<K,V>    implements Map<K,V>, Cloneable, Serializable {       /*        * HashMap 的預設初識容量        * */        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16        /**         * HashMap的最大容量,假如儲存1 << 30,需要在棧中存在2 ^ 30個指向指定堆         * 空間的對象空間,在32位機下,每個這樣的對象空間為4位元組;也就是說所有的         * 記憶體空間都會被用來儲存Map,而jvm的堆大小和棧大小都是有上限的所以,一般         * 很難達到這麼大         */        static final int MAXIMUM_CAPACITY = 1 << 30;        /**         * 預設的載入因子         */        static final float DEFAULT_LOAD_FACTOR = 0.75f;        /**         * 鏈表數組,它的索引稍後會詳細說明         */        transient Node<K,V>[] table;        /**         * 使用keySet()和values()時會用到         */        transient Set<Map.Entry<K,V>> entrySet;        /**         * 儲存了索引值對的數量         */        transient int size;        /**         * 當前Map的修改次數,保持一致性         */        transient int modCount;        /**         * 數值為(capacity * load factor),是否進行resize的判斷闕值         *         */        int threshold;        /**         * Map的載入因子         *         * @serial         */        final float loadFactor;    }
它有四個建構函式,如下:

        HashMap()        HashMap(int initialCapacity)        HashMap(int initialCapacity, float loadFactor)        HashMap(Map<? extends K,? extends V> m)
假如沒有傳入initialCapacity 或 loadFactor時會使用預設的 1<< 4也就是16 和 0.75兩個參數。

當傳入了initialCapacity 時,不會直接使用該值,而是計算大於他的最小2的 冪值。在jdk1.8相對於之前版本是由改進的:

//jdk 1.8    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;    }//jdk 1.7    while (capacity < initialCapacity)        capacity <<= 1;
每次想到這兩個代碼都很激動,代碼很美,但也會產生一個疑問,我們在學資料結構的時候,書上教我們雜湊表的大小應該設為大於指定值得最小質數,而這明顯不符合說明,其實,這是為後續好多操作提供了方便。

這樣操作之後n 其實是一個大於指定值得最小2的冪值再少1的值,假如傳入的是9,n的值便是15,單看十進位好像沒什麼,但是化為2進位是1111,全部為1的話,能夠很好的進行很多的操作。

計算出大小,分配空間之後我們便可以進行儲存工作了。

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;}

我們拋開TreeXXX的先不管,只看對table的操作,可以知道,節點在table中的索引其實是通過 hash & (n - 1) ,其中n 其實就是table[]的長度。而hash的計算是通過下列方法的調用得到:
static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
它能夠保證在計算時,hashCode 高位的可用,使存入元素儘可能分散開來,同也可以觀察到,元素存在的索引值之和key有關。

從整體來看,當傳入值時,通過傳入的hash值與capacity-1進行 & 操作得出索引i,通過訪問p = table[i],判斷p是否為空白或者p.key 是否與key相等,當為空白時,直接插入元素;當p.key == key時,更新value,不相等時,在鏈表最後面插入指定結點。

newNode代碼如下:

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {        return new Node<>(hash, key, value, next);    }
有一個操作 在我們去面試的時候,面試官可能會高度興趣,resize();

在上代碼之前,我們先來聊聊它的主要操作。

在不考慮極端的情況下,當之前的capacity大於0時,新的capacity會變成之前的兩倍,而闕值也會同樣增長;當之前的capacity小於0,但是闕值大於0,新的capacity就賦值為闕值;capacity和闕值等於0時,會被賦值為他們的預設值。容量規定好了,就需要把之前Map裡的鍵值對存入到新的容器table裡面,因為是通過計算雜湊值確定索引,所以註定不能像List一樣直接進行copy,於是只有重新根據每個Entry中儲存的hash與capacity-1來計算索引。這是我們都會想到的。但是通過翻閱源碼,我們又可以發現它的精妙之處。先附上代碼吧。

    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;    }

看完代碼,我們來說說它的精妙,當一個數擴大成之前的兩倍之後例如 8 -> 16,它們減一的值得二進位碼 111 -> 1111;可以看到增加了一個1000,也就是原來容量的大小。當有一個hash值與增大之後的容量減一也就是15進行與運算時,實際上與之前容量為8時的索引也就是變了與1000與的位,將容量擴大成之前的兩倍時,可以看成是增加了一個與之前容器一樣大的容器。很明顯索引數值小的會放在容器的前面,大的放在之後。所以我們只需要計算hash 和 之前容量的值進行與運算的結果是否為1,就能知道原來的值是放在原處還是放在新增容器相對應的位置。只看文字有點難以理解,我們來看個圖片:


假如需要將某個節點重新分配,它只有如上兩種位置可以存放,當它的hash & oldCapacity == 1時,存在New地區;否則在Old地區。同時他也說明,鍵值對仍在固定位置或者New區相對應的固定位置,不可能Old地區中的簡直到跑到Old地區的其他位置。

再看看具體代碼:

首先先看 if(e.next == null),當它的next為null時,說明在這個位置上就只有一個鍵值對,所以,可以直接插入,當不為null時,為了提高效率,每個地區都存在tail 和head 節點,當tail != null時,直接將新節點插入在tail之後,否則加入到 head中。它的hi 和 lo地區就像我們圖中的New和Old地區。

從上可以看到,每次講容量擴大成2的冪級數,是非常漂亮的設計,其中很大的提高了效率。








從源碼分析java集合【HashMap】

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.