探究 jdk1.8 中 HashMap 由链表转化成二叉树的条件

HashMap 基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。另外,HashMap是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而Hashtable是线程安全的。

HashMap 是 Java 中最常使用的一个 Map 类,也是面试题中出现的高频考察点。HashMap 继承了 AbstractMap,实现了 Map、Cloneable、Serializable 接口,UML 图如下:

HashMap 的结构如下图所示:

HashMap 有四个构造方法,分别是:

  • HashMap():仅仅只是设置了负载因子(loadFactor)为默认值 0.75(此时极限值(threshold)仍然为0,真正设置该值是在第一次调用 put 方法,会进行一次 resize() 的过程,此过程发生后,极限值(threshold)会变成 capacity * loadFactor 即 16 * 0.75 = 12)
  • HashMap(int initialCapacity) :调用 HashMap(int initialCapacity, float loadFactor) 方法。
  • HashMap(int initialCapacity, float loadFactor) :校验两个参数之后,设置了负载因子(loadFactor)以及极限值(threshold),此时极限值就是最大容量值 capacity,capacity 总是 2 的 n 次方。同样的,在第一次 put 时,会进行一次 resize() 的过程,就会改变极限值(threshold)为 capacity * loadFactor。
  • HashMap(Map<? extends K, ? extends V> m):相当于把 m 中的值都复制到一个新的 HashMap 对象(该过程复制的是引用地址,也就是说,一旦改变该对象,两个 HashMap 对应的值都会改变)

HashMap 常量值:

    /**
     * 默认初始容量值 16,必须为 2 的 n 次方
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 即 16

    /**
     * 最大容量 2 ^ 30,如果构造方法传入的最大容量值大于该值,则将最大容量设置为该值,同样必须为 2 的 n 次方
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认负载因子 0.75,当构造方法中没有指定的时候,将使用该值
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 将链表转化二叉树的阈值 8
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 将二叉树转化回链表的阈值 6
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 将链表转化成二叉树时,最小容量值 64(注意:不是 map 中的元素个数)
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

HashMap 的内部字段及作用:

    /**
     * 用来存放键值对的 Node 数组,也就是哈希表,在 resize() 方法中会初始化这个数组的大小,当分配空间时它的长度总是 2 的 n 次方,也就是 capacity
     * 注意该字段被 transient 修饰,意味着序列化 HashMap 的时候将忽略该字段,反序列化的时候该字段将为 null
     */
    transient Node<K,V>[] table;

    /**
     * 该字段维护了一个键值对的集合 entrySet,而 HashMap 的父类 AbstractMap 维护了键集合 keySet 和值集合 values
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * 当前 HashMap 对象的键值对数量
     */
    transient int size;

    /**
     * 该字段记录了当前 HashMap 对象结构修改的次数(包括数量的改变、结构调整,例如调整哈希值)
     * 作用是在遍历 Map 的时候,如果此时有其他操作修改了该 Map(增删改元素)就会造成该字段值的改变,
     * 通过比较该值的前后是否相等,如果不相等,则会抛出异常
     */
    transient int modCount;

    /**
     * 极限值,在每次 resize 的时候将值设置为 (capacity * loadFactor),当键值对的数量达到该值的时候将会进行 resize() 扩容操作
     */
    int threshold;

    /**
     * 负载因子
     */
    final float loadFactor;

接下来我们来看 HashMap 的 put 方法,进而研究触发 HashMap 由链表转化成二叉树的条件:

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

put 方法实际上是调用了 putVal 方法,接下来看 putVal 方法的源码:

    /**
     * 实现 Map 接口的 put 方法
     *
     * @param hash         key 的哈希值
     * @param key          key 值
     * @param value        需要 put 的值
     * @param onlyIfAbsent 是否保留已存在的值
     * @param evict        非创建模式(HashMap 中没有使用该值).
     * @return 返回之前存在的值,如果不存在则为空
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        // 第一次赋值时,执行 resize() 方法,初始化 table 数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果命中的 table 数组(哈希表)的位置没有元素,则将当前值放入((n - 1) & hash 实际就是取模的位运算)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            // 发生 哈希碰撞,即命中的 table 数组(哈希表)的位置已存在元素
        else {
            Node<K, V> e;
            K k;
            // 如果命中的元素哈希值和当前哈希值相等,并且 key 值也相等,则把table 数组(哈希表)中的元素赋值给变量 e
            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);
                        // 判断链表的节点数是否已经达到需要转二叉树的阈值,由此可以看出当链表的结点数达到 8 时(binCount 从 0 开始),会将链表转成二叉树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果发现结点的 key 值和要 put 的元素的哈希值以及key值都相等,则直接结束循环
                    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;
                // 如果 onlyIfAbsent 为 false 说明不保留原来的值,而是用新值替换原来的值,如果原来的值为 null,也会用新值替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 在 HashMap 中这是一个空方法,在其子类 LinkedHashMap 中实现了该方法,作用是将修改结点移到最后,保证 LinkedHashMap 元素的顺序
                afterNodeAccess(e);
                // 返回原值
                return oldValue;
            }
        }
        // 修改计数器自增1
        ++modCount;
        // 判断当前元素个数是否达到需要扩容的极限值
        if (++size > threshold)
            resize();
        //  在 HashMap 中同样是一个空方法
        afterNodeInsertion(evict);
        return null;
    }

可以看到一个很重要的判断,if (binCount >= TREEIFY_THRESHOLD - 1) 从这里也可以看出树化的第一个条件是:当 bitCount 达到 TREEIFY_THRESHOLD - 1 时,将会对 table 进行二叉树化(treeifyBin),接下来我们来看看 treeifyBin() 方法的源码,探究链表树化的第二个条件。

    /**
     * 用二叉树替换给定 hash 位置的所有连接的结点,除非 table(哈希表)空间太小,这种情况下只进行 resize 操作
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 当 tab 为空或者 tab 的 length 小于 MIN_TREEIFY_CAPACITY 也就是 64 时,只是对 tab 进行 resize 而不进行树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 否则将 hash 对应位置非空的链表进行树化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                // 遍历 hash 对应位置的链表,将 Node 类型的节点转化成 TreeNode 类型,并且保留链表的前驱后继结点的关系
                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);
            // 将 TreeNode 类型的结点数组转化成二叉树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

可以看到 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 这句条件判断,当 tab 为空或者其长度小于 64 时,只是进行 resize 而没有进行树化。这就是链表树化的第二个条件。综合上面提到的,可以看出,要将 hash 对应位置的链表转化成二叉树,要满足以下两个条件:

  1. 当前 hash 位置的链表长度(结点数)大于等于 8 
  2. table(哈希表)的容量大于等于 64

另外还有一个值得注意的方法就是前面多次提到的 resize(),其源码如下:

/**
     * 初始化或者扩容 table 的空间,如果 table 为空,则用默认值初始化 capacity,threshold 与 capacity 的值相等. 
     * 否则,由于使用 2 的 n 次方来扩容,因此 table 中每一个位置的元素在新 table(哈希表)中要么落在原位置,要么按照 2 的 n 次方偏移。
     *
     * @return 返回 table(哈希表)
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果原容量大于 0
        if (oldCap > 0) {
            // 如果容量超过最大值,则不再扩容,将极限值(threshold)设为 Integer 可表示的最大正整数
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 如果扩容后的容量没有超过最大容量并且原来的容量大于默认容量,则将极限值 threshold 翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // threshold 翻倍
        }
        else if (oldThr > 0) // 用极限值(threshold)替换初始容量
            newCap = oldThr;
        // 首次调用 resize(),即 capacity 和 threshold 都为 0
        else {
            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;
        // 初始化 table 长度为 newCap
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // oldTab 不为空,说明原 table 已经存在元素,需要将这些元素移到新的 table
        if (oldTab != null) {
            // 遍历 oldTab 
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 不为空的位置
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 如果当前位置的后继元素为空,说明链表只有一个元素,则直接重新计算当前元素的哈希值命中的位置,然后将改元素插入新 table 计算的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果当前位置已经是一个二叉树,则调用 split() 方法拆分二叉树
                    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;
                            // hash 值落在了新扩展空间之外,或者说扩展 table 后 hash 的位置不变(例如 table 的 length 由 16 变为 32,则这个判断表示 hash 值在 [0,15] 和 [32,∞) 区间)
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 否则说明 hash 落在了新扩展空间内,或者说 hash 的位置需要移动(上面的例子就是[16,31] 区间),这样做是因为原表中发生了 hash 碰撞的元素,可能在新表中并不会发生碰撞,所以需要把这些元素位置做调整,即重新放入新的 table 对应位置中
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 将不需要调整的元素放入新 table 对应位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 将需要调整的元素放入新 table 重新计算后的位置(对原位置偏移 oldCap 长度)
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        // 返回新的 table
        return newTab;
    }

在 resize 方法中,如果 hash 对应位置的元素是一个二叉树,则调用 TreeNode 类的 split 方法拆分二叉树,接下来看看 split 方法的源码:

    /**
     * 将二叉树中的节点拆分为上下二叉树,如果树太小则去树化操作,只进行 resize 操作,逻辑可以参考 resize 方法
     *
     * @param map   the map
     * @param tab   记录二叉树父节点的 table(哈希表)
     * @param index 需要被拆分的位置
     * @param bit   需要被拆分的大小
     */
    final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {
        TreeNode<K, V> b = this;
        // Relink into lo and hi lists, preserving order
        TreeNode<K, V> loHead = null, loTail = null;
        TreeNode<K, V> hiHead = null, hiTail = null;
        int lc = 0, hc = 0;
        // 遍历结点,逻辑和 resize 方法类似,注意:遍历后,获得的是一个结点类型为 TreeNode 的链表
        for (TreeNode<K, V> e = b, next; e != null; e = next) {
            next = (TreeNode<K, V>) e.next;
            e.next = null;
            if ((e.hash & bit) == 0) {
                if ((e.prev = loTail) == null)
                    loHead = e;
                else
                    loTail.next = e;
                loTail = e;
                ++lc;
            } else {
                if ((e.prev = hiTail) == null)
                    hiHead = e;
                else
                    hiTail.next = e;
                hiTail = e;
                ++hc;
            }
        }
        // hash 位置不变
        if (loHead != null) {
            // 如果当前位置链表结点数降低到 UNTREEIFY_THRESHOLD ,即 6,则将二叉树去树化,即将二叉树转化成结点类型为 Node 的链表
            if (lc <= UNTREEIFY_THRESHOLD)
                tab[index] = loHead.untreeify(map);
            else {
                // 否则将该链表放入 tab 对应位置,然后转化二叉树
                tab[index] = loHead;
                if (hiHead != null) // (else is already treeified)
                    loHead.treeify(tab);
            }
        }
        // hash 位置需要改变
        if (hiHead != null) {
            if (hc <= UNTREEIFY_THRESHOLD)
                tab[index + bit] = hiHead.untreeify(map);
            else {
                tab[index + bit] = hiHead;
                if (loHead != null)
                    hiHead.treeify(tab);
            }
        }
    }

接下来我们看看去树化操作 untreeify() 方法的源码:

        /**
         * 返回一个每个元素用 Node 类型替换 TreeNode 类型后的链表,保持前驱后继关系
         */
        final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

由上可以得出结论:当对应位置(哈希桶)二叉树的结点数小于等于 6 时,会将二叉树转化回链表。

为了验证上面的结论,我们可以用代码去调试源码,调试源码如下:

package com.java.util;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author zhoushixiong
 * @Description HashMap 源码调试
 * @date 2020/7/13 14:59
 **/
public class HashMapDebug {

    /**
     * 模拟 Hash 碰撞 Key 实体
     */
    static class TestKey {
        String key;

        public TestKey(String key) {
            this.key = key;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            return Objects.equals(key, ((TestKey) o).key);
        }

        @Override
        public int hashCode() {
            // 将 hash 设置为固定值,为了将元素放在同一个哈希桶内(Hash 碰撞)
            return 1;
        }
    }

    public static void main(String[] args) {
        Map map = new HashMap();
        // 调试 HashMap 由链表转红黑树的过程
        for (int i = 0; i <= 64; i++) {
            TestKey key = new TestKey(i + "");
            map.put(key, i);
        }

    }

}

 

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值