java集合類三-聊聊HashMap

目錄

 

1 HashMap數據結構

2 構造函數

3 put方法

4 鏈表和紅黑樹互相轉換

5 hashmap擴容


1 HashMap數據結構

HashMap底層是通過數組和單向鏈表來實現的。HashMap底層是一個數組,數組的每一項又是一個鏈表節點或者紅黑樹(jdk1.8),如圖所示

2 構造函數

這裏看一個參數比較完整的構造函數
    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);
    }
initialCapacity:初始容量
loadFactor:負載因子,默認0.75
threshold:初始化table的時候數組大小。計算值是剛剛超過initialCapacity的2的n次冪
    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

感興趣的話可以瞭解一下移位運算。數組大小是2的n次冪可以減小哈希碰撞,具體原因下文解釋

3 put方法

首先看方法入口

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

首先是計算key的hash()

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

hashcode是int類型32位,這裏讓高位和低位做異或運算使得重新生成的hash更具有隨機性

putVal核心方法

    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;
            // (n - 1) & hash,計算存放數組下標
        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))))
                // 如果待插入位置node的key和需要插入的key一致則覆蓋
                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);
                        // 鏈表長度爲8的時候嘗試轉紅黑樹
                        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)
            // 元素個數超過閾值(table數組大小*負載因子)則進行擴容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

這裏說一下計算存放數組下標的方法:i = (n - 1) & hash

首先說明這裏是進行按位與運算而不是對數組長度取模,原因是對於計算機來說位運算比取模運算要快

選擇與運算的原因:與運算的結果是小於等於參與運算的較小元素。如果採用或運算則計算結果有可能數組越界

爲什麼table數組長度要是2的n次冪:讓table中每個位置都可以存放到元素。數組下標是0到n-1,只有n是2的n次冪的時候,n-1的二進制每一位都是1,這樣可以儘可能的填滿整個table。如果n不是2的n次冪,則n-1的二進制存在位數爲0的情況,則與運算後該位置還是0,所以該位置爲1的table位置存放不了元素。比如n=15,n-1=14,14的二進制結果是1110,則1001,1011,1101這三個數組下標即9,11,13存放不了數據

4 鏈表和紅黑樹互相轉換

鏈表->紅黑樹:鏈表長度>=8的時候嘗試轉成紅黑樹,TREEIFY_THRESHOLD默認值爲8。如果table大小>=64開始真正的轉紅黑樹

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 如果table數組長度<64則擴容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                // 如果table數組長度>=64則轉紅黑樹
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

紅黑樹->鏈表

hashmap中有兩個地方會把紅黑樹轉成鏈表

  • remove元素

這裏是remove元素其中的一段代碼,通過紅黑樹根節點及其子節點是否爲空來判斷是否轉紅黑樹

            if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
  • resize方法

resize方法會對紅黑樹進行拆分(split),拆分方法有轉鏈表操作

            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    // 紅黑樹節點個數<=6時轉換成鏈表
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }

 

5 hashmap擴容

當元素個數超過table數組大小*負載因子的時候開始擴容。每次擴容後table容量變爲原來的2倍

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