面试一次问一次,HashMap是该拿下了(二)

文章目录

  • 前言
  • 一、HashMap类图
  • 二、源码剖析
    • 1. HashMap(jdk1.8版本) - 此篇详解
      • ⑴. 底层结构(数组+单向链表+红黑树)
      • ⑵. 构造函数
      • ⑶. put() - 添加元素方法
      • ⑷. get() - 获取元素方法
      • ⑸. remove() - 删除元素方法
    • 2. HashMap(jdk1.7版本)
    • 3. ConcurrentHashMap
  • ~~   码上福利


前言

业精于勤荒于嬉,行成于思毁于随;

在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,开始思考…

今天一起来聊一聊 HashMap集合,看到这里,笔者懂,大家莫慌,先来宝图镇楼 ~
在这里插入图片描述

咳咳… 对于屏幕前帅气的猿友们来说,HashMap… 张口就来,闭眼能写,但是呢,面试官大大一问立马慌,自己阅读源码时,又隐隐觉得知其然不知其所以然,但直面高薪诱惑,又不得不研究,如此甚难;

那么…此时,笔者帅气的脸庞似有似无洋溢起一抹微笑,毕竟是查看过源码的猿,就是那么的豪横,话不多说,来吧,展示…



一、HashMap类图

在这里插入图片描述



二、源码剖析


1. HashMap(jdk1.8版本) - 此篇详解


大家都知道,jdk1.8版本底层数组+链表(单向链表)+红黑树,结合笔者的经验之谈,我觉得在分析HashMap集合具体操作源码前,有必要先了解下其底层链表结构以及红黑树,话不多说,上源码…

⑴. 底层结构(数组+单向链表+红黑树)


  • 链表结构 - 单向链表

    /**
     * HashMap1.8中定义- 单向链表
     */
    static class Node<K, V> implements Map.Entry<K, V> {
     
        // 当期key对应hash值
        final int hash;
        // key值
        final K key;
        // value值
        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; }

        // 重写toString方法
        public final String toString() {
     
            return key + "=" + value;
        }

        // 重写hashCode方法
        public final int hashCode() {
     
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

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

        // 重写equals方法
        public final boolean equals(Object o) {
     
            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;
        }
    }
    

我们发现,HashMap1.8版本定义的链表结构之单向链表除了类名从Entry改为Node以外,代码方面与1.7版本的HashMap是一致的…

每一个Node节点也包含四个属性:key表示当前节点key值;value表示当前节点value值,next节点表示当前节点下一个节点,如当前节点为链表末尾节点,则当前节点的next节点为null;hash表示当前节点key值通过算法计算出来的hash值;

抽象图解如下(其实笔者并不是很认同此图能形象的代表链表结构,但抽象理解还是可以的):

      单个Node节点:
面试一次问一次,HashMap是该拿下了(二)_第1张图片

      单向链表图解:
面试一次问一次,HashMap是该拿下了(二)_第2张图片

  • 红黑树

    /**
     * HashMap1.8中定义- 红黑树
     */
    static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
     
        TreeNode<K, V> parent;  // red-black tree links
        TreeNode<K, V> left;
        TreeNode<K, V> right;
        TreeNode<K, V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K, V> next) {
     
            super(hash, key, val, next);
        }
        
        // ...省略诸多代码,详情请参考源码

    }

	/**
     * 红黑树节点继承与LinkedHashMap定义的Entry节点
     * 
     * **重点:我们发现Enty节点又继承与 HashMap中的Node节点
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
     
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
     
            super(hash, key, value, next);
        }
    }


    

我们发现,红黑树节点继承于LinkedHashMap定义的Entry节点,而Entry节点又继承于HashMap中的Node节点…

      红黑树图解:
面试一次问一次,HashMap是该拿下了(二)_第3张图片

  • 底层结构:数组 + 单向链表 + 红黑树

      HashMap1.8版本底层 数组 + 单向链表 图解:

面试一次问一次,HashMap是该拿下了(二)_第4张图片

⑵. 构造函数


    // 加载因子
    final float loadFactor;

    // 加载因子 - 默认值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 无参构造
     */
    public HashMap() {
     
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * 有参构造
     * @param initialCapacity :用于计算threshold(扩容阈值)
     */
    public HashMap(int initialCapacity) {
     
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 有参构造
     * @param initialCapacity:用于计算threshold(扩容阈值)
     * @param 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);
    }

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

从源码中可以看出,构造只为相关参数(加载因子、扩容阈值)进行初始化;

⑶. put() - 添加元素方法


     // 底层数组
    transient Node<K, V>[] table;

    // 转红黑树 - 阈值
    static final int TREEIFY_THRESHOLD = 8;

    // 对HashMap操作次数
    transient int modCount;

    // HashMap存放元素个数
    transient int size;

    // 底层数组扩容阈值(也可以理解为HashMap底层数组实际存放元素大小)
    int threshold;

    // 扩容最大值
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 底层数组默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 标识链表转数组要求数组最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 入口
     */
    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);
    }

    /**
     * 添加元素
     *
     * @param hash:添加元素key值对应hash值
     * @param key:添加元素key值
     * @param value:添加元素value值
     * @param onlyIfAbsent:内容覆盖标识(翻译:如果为真,不要改变现有的值,系统默认为false)
     * @param evict:个人理解:标识插入节点后是否进行其他操作,默认为true
     * @return 返回前一个值,如果为空则返回null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
     

        // 1-1.tab为当前全局table数组
        Node<K, V>[] tab;
        // 2-2.p为当前添加元素key对应的Node节点/链表
        Node<K, V> p;
        // 1-2.tab数组长度
        int n;
        // 2-1.i为当前添加元素key存放tab数组对应的index值[与jdk1.7中计算index值一样,(hash & table.length - 1)]
        int i;

        // 1.判断当前数组是否为空 是 -> 表示当前第一次put元素
        if ((tab = table) == null || (n = tab.length) == 0) {
     
            // 进行对数组及参数初始化
            n = (tab = resize()).length;
        }

        // 2.(未发生index冲突)判断当前添加元素在tab数组对应位置下是否为第一个Node节点 是-> 此位置第一次添加Node节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建Node节点赋值到tab数组对应i位置下
            tab[i] = newNode(hash, key, value, null);

            // 3.(发生index冲突)表示当前tab数组对应位置下为链表/红黑树
        else {
     
            // 3.2-1.e为p节点,也就是当前添加元素key对应的Node节点
            Node<K, V> e;
            // 3.1-1.k为当前节点对应key,也就是当前添加元素key值
            K k;

            // 3.1.判断当前添加元素对应节点hash一致且key相等内容覆盖(先赋值,后进行新增) ->可知:当前节点为第一个Node节点
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            // 3.2.判断当前节点是否为红黑树类型 ->可知:当前tab对应的位置已为 红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 已红黑树方式添加元素

            // 3.3.可知:当前tab对应位置为链表
            else {
     
                // 遍历链表
                for (int binCount = 0; ; ++binCount) {
     
                    // 判断当前节点下个节点是否为空 ->可知:当前节点为对应链表最后一个Node节点
                    if ((e = p.next) == null) {
     
                        // 直接在此节点的next节点添加 ->可知:HashMap1.8为尾插法
                        p.next = newNode(hash, key, value, null);
                        // 判断链表长度是否大于8 是->链表转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            // 链表转红黑树操作
                            treeifyBin(tab, hash);

                        break; // 因源码作者这里定义为死循环,故增加节点跳出
                    }

                    // 遍历中判断对应节点hash一致且key相等内容覆盖(先赋值,后进行新增)
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break; // 因源码作者这里定义为死循环,故增加节点跳出

                    // 赋值p,继续遍历
                    p = e;
                }
            }

            // 3.4.判断当前e的Node节点不为空,对其内容进行覆盖(经过步骤3进入此判断表示这里为覆盖)
            if (e != null) {
     
                // 获取就值
                V oldValue = e.value;
                // onlyIfAbsent翻译为:如果为真,不要改变现有的值,系统默认为false,所以表示默认为覆盖
                if (!onlyIfAbsent || oldValue == null) {
     
                    // 内容覆盖
                    e.value = value;
                }
                // 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
                afterNodeAccess(e);
                // 返回删除元素旧值
                return oldValue;
            }
        }

        // 操作次数++
        ++modCount;
        // HashMap元素个数 > 扩容阈值
        if (++size > threshold) {
     
            resize(); // 扩容
        }
        // 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
        afterNodeInsertion(evict);
        return null;
    }

    // 具体扩容方法
    final Node<K, V>[] resize() {
     
        // 获取全局table数组 (便于理解,定义为旧数组)
        Node<K, V>[] oldTab = table;
        // 获取数组容量 (便于理解,定义为旧数组容量)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 获取扩容阈值 (便于理解,定义为旧数组扩容阈值)
        int oldThr = threshold;
        // (便于理解,newCap定义为新数组容量、newThr定义为新数组扩容阈值)
        int newCap, newThr = 0;

        // 如旧数组容量>0 ->可知:数组已初始化过了
        if (oldCap > 0) {
     
            // 旧数组容量>=数组最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
     
                // 扩容阈值为Integer最大值
                threshold = Integer.MAX_VALUE;
                // 返回旧数组
                return oldTab;
            }
            // 新数组容量(旧数组容量*2) < 扩容最大值 且 旧数组容量 >= 数组默认容量(16) -> 新数组容量:2倍扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
     
                // 新数组扩容阈值:2倍扩容
                newThr = oldThr << 1; // double threshold
            }
        }

        // 旧数组扩容阈值>0(当前情况:table数组未初始化,threshold>0) ->可知:此种情况为调用有参构造方法时会进入此判断
        else if (oldThr > 0) {
     
            // 新数组容量设置为旧数组扩容阈值,HashMap使用threshold变量暂时保存initialCapacity值
            newCap = oldThr;
        }

        // (当前情况:table数组未初始化,threshold=0) ->可知:当前为第一次put元素,数组及参数进行初始化
        else {
     
            // 新数组容量=16
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新数组扩容阈值=16*0.75=12
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 新数组扩容阈值为0
        if (newThr == 0) {
     
            // ft=新数组容量*加载因子
            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数组
        table = newTab;

        // 旧数组不为空,进行遍历分组重新映射到新数组中
        if (oldTab != null) {
     
            // 遍历数组,并将元素映射到新数组中
            for (int j = 0; j < oldCap; ++j) {
     
                // e为当前数组位置对应节点
                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 {
     
                        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;
    }

    // 创建链表Node节点
    Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
     
        return new Node<>(hash, key, value, next);
    }

    // 链表转红黑树
    final void treeifyBin(Node<K, V>[] tab, int hash) {
     
        // n为当前全局tab数组长度;index为当前添加元素hash对应数组下标位置
        int n, index;
        // 用于循环的迭代变量,代表当前节点
        Node<K, V> e;
        // tab数组为空进行初始化;tab不为空且长度<64未达到转红黑树条件,需继续进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

        // 数组index位置对应链表不为空,进行链表转红黑树
        else if ((e = tab[index = (n - 1) & hash]) != null) {
     
            // hd为head头节点,tl为tail尾节点
            TreeNode<K, V> hd = null, tl = null;
            // 遍历链表
            do {
     
                // 创建树节点
                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);

            // hd树不为空赋值给tab数组对应位置
            if ((tab[index] = hd) != null) {
     
                // 具体树化操作 - 此知识点(红黑树变色及旋转)笔者之后会另起一篇详解
                hd.treeify(tab);
            }
        }
    }

    // 创建树TreeNode节点
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
     
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }
    

相信任谁跟如此一大坨代码硬杠之下,结局也只有放弃抵抗,包括笔者(同样是硬着头皮看着一根根掉落在键盘上的头发,再放弃与挣扎中承受着,一种欣喜又苦涩的心情),或许,这就是程序猿吧。

不知过了许久…一口叹气声,打破了空气中凝固的气氛…

庆幸的是,帅气的笔者最终还是成功拿下了它,笔者帅气的脸庞似有似无洋溢起一抹微笑,也许,这才是一名合格的程序猿吧。

咳咳…猝不及防的一波感慨人生,看看天花板,生活依旧继续,我们言归正传…

其实呢,相信看过上篇(HashMap1.7版本源码分析)的猿友已或多或少掌握了笔者查看源码的技巧,其实说白了,研究源码过后,就会明白源码作者当时在写代码时的思路以及逻辑,作为暖男的笔者再按照源码作者的思路逻辑进行步骤标记及注释,屏幕前的猿友再去结合笔者的步骤标记以及注释去阅读,是可以起到事半功倍的作用的。

接下来,我们重新审阅put()方法就会发现,其内部其实只做了四件事,下面我们拆解分析下:


  • 第一次put元素时(数组及重要参数进行初始化):

这里需注意一点,数组的扩容及初始化都定义在resize(),方法内部通过参数判断进行区分,在这里我们先阐述初始化部分相关代码,最后再进行整体分析,按照暖男笔者的思路来吧…

    // tab为当前全局table数组
    Node<K, V>[] tab;
    // tab数组长度
    int n;
    
   // 1.判断当前数组是否为空 是 -> 表示当前第一次put元素
   if ((tab = table) == null || (n = tab.length) == 0) {
     
       // 进行对数组及参数初始化
       n = (tab = resize()).length;
   }
  1. 初始化底层数组:
   // 新数组容量=16
   newCap = DEFAULT_INITIAL_CAPACITY;
   // 创建新数组
   Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];

   // 将新数组赋值给全局table数组
   table = newTab;
  1. 计算底层数组扩容阈值:
   // 新数组扩容阈值=16*0.75=12
   newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

   // 底层数组扩容阈值=16
   threshold = newThr;

  • 未发生index冲突时添加元素:
    // tab为当前全局table数组
    Node<K, V>[] tab;
    // p为当前添加元素key对应的Node节点/链表
    Node<K, V> p;
    // tab数组长度
    int n;
    // i为当前添加元素key存放tab数组对应的index值[与jdk1.7中计算index值一样,(hash & table.length - 1)]
    int i;

    // 2.(未发生index冲突)
    // 判断当前添加元素在tab数组对应位置下是否为第一个Node节点 是-> 此位置第一次添加Node节点
    if ((p = tab[i = (n - 1) & hash]) == null) {
     
        // 创建Node节点赋值到tab数组对应i位置下
        tab[i] = newNode(hash, key, value, null);
    }

i = (n - 1) & hash:计算当前key对应数组存放的下标位置,我们发现,jdk1.8版本与1.7版本计算下标方式是一样的;

所谓的未发生冲突,也就是当前添加元素的key值,通过一定算法计算出来的index值,对应在数组其位置上还未存在节点,也表示此位置第一次添加Node节点。


  • 发生index冲突时添加元素 - 分为3种情况:
    // tab为当前全局table数组
    Node<K, V>[] tab;
    // p为当前添加元素key对应的Node节点/链表
    Node<K, V> p;
    
    // 3.(发生index冲突)表示当前tab数组对应位置下为链表/红黑树
    else {
     
        // e为p节点,也就是当前添加元素key对应的Node节点
        Node<K, V> e;
        // k为当前节点对应key,也就是当前添加元素key值
        K k;

        // 3.1.判断当前添加元素对应节点hash一致且key相等内容覆盖(先赋值,后进行新增) ->可知:当前节点为第一个Node节点
        // ...

        // 3.2.判断当前节点是否为红黑树类型 ->可知:当前tab对应的位置已为 红黑树
        else if (p instanceof TreeNode)
        // ...

        // 3.3.可知:当前tab对应位置为链表
        else 
        // ...

        // 3.4.判断当前e的Node节点不为空,对其内容进行覆盖(主要针对3.1 / 3.3中,执行覆盖操作)
        // ...
    }
  • 3.1:当前对应数组冲突位置,头节点与添加元素hash值一致以及key值相等,进行覆盖;
	// 3.1.判断当前添加元素对应节点hash一致且key相等内容覆盖 ->可知:当前节点为头节点
	if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
     
        e = p; // (先赋值,最后通过 3.4步骤进行覆盖操作)
    }

  • 3.2:当前对应数组冲突位置,为红黑树节点,已红黑树方式添加元素;
    // 3.2.判断当前节点是否为红黑树类型 ->可知:数组对应的位置为 红黑树
    else if (p instanceof TreeNode) {
     
        e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 已红黑树方式添加元素
    }

  • 3.3:当前对应数组冲突位置,为链表,遍历进行对比:
    // 3.3.可知:当前tab对应位置为链表
    else {
     
        // 遍历链表
        for (int binCount = 0; ; ++binCount) {
     
            // 判断当前节点下个节点是否为空 ->可知:当前节点为对应链表最后一个Node节点
            if ((e = p.next) == null) {
     
            
                // 3.3.2.直接在此节点的next节点添加 ->可知:HashMap1.8为尾插法
                p.next = newNode(hash, key, value, null);
                
                // 3.3.3判断链表长度是否大于8 是->链表转红黑树
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    // 链表转红黑树操作
                    treeifyBin(tab, hash);

                break; // 因源码作者这里定义为死循环,故增加节点跳出
            }

            // 3.3.1.遍历中判断对应节点hash一致且key相等内容覆盖(先赋值,后进行新增)
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                break; // 因源码作者这里定义为死循环,故增加节点跳出

            // 赋值p,继续遍历
            p = e;
        }
    }
  1. 如遍历过程中,存在与当前添加元素hash值一致且key值相等情况,进行覆盖;

  2. 遍历至结束未发现(hash值一致且key值相等的节点),添加当前元素;

  3. 接着第2点,添加元素节点后,判断当前链表长度是否大于等于8,如满足,进行树化操作;

        注意:执行树化操作需满足两个条件,其上述链表长度大于等于8为其一,再进入treeifyBin()方法内部,真正执行树化操作还需满足 数组长度大于等于64,只有同时满足两者才会真正执行树化操作,也就是我们常说的链表转红黑树。

  • 3.4:主要针对 3.1 / 3.3.1步骤,执行具体覆盖操作;
   // 3.4.判断当前e的Node节点不为空,对其内容进行覆盖(经过步骤3进入此判断表示这里为覆盖)
   if (e != null) {
     
       // 获取就值
       V oldValue = e.value;
       // onlyIfAbsent翻译为:如果为真,不要改变现有的值,系统默认为false,所以表示默认为覆盖
       if (!onlyIfAbsent || oldValue == null) {
     
           // 内容覆盖
           e.value = value;
       }
       // 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
       afterNodeAccess(e);
       // 返回删除元素旧值
       return oldValue;
   }

到此,HashMap整个put()方法的框架思路已讲解完毕,相信对于屏幕前的猿友们来说,是不是很 so easy…

此时,笔者嘴角若隐若现一丝弧度微起,源码么,也不过如此…

言归正传,虽然跟着笔者的思路,熟悉了put()方法的整体框架思路,但是凭此去面对面试官的话,还是略显单薄…

在我们Java面试界有一个不成文的规定,面试官只要提到HashMap,肯定会问其底层结构实现以及重中之重的扩容,不过对于有经验的开发猿来说,面试官只需询问:“你了解HashMap么?”,那么好,从hashMap1.7版本底层结构到扩容,再从头插法引出扩容死循环的问题以及时间/空间复杂度导致查询效率低的问题,行云流水般的过渡到hashMap的jdk1.8版本,相似的套路,从底层结构到其扩容,巧妙的运用尾插法解决扩容死循环问题,及加入红黑树,大大提升查询效率,最后再从线程安全问题过渡到ConcurrentHashMap,一套进可攻退可守的闪电五连鞭打在面试官一脸懵B的脸上,可想而知,此时此刻,You就是整个会议室最靓的仔。 细心的猿友是不是已经默默掏出小本本开始记知识点了呢,学知识就是要不讲武德!

那么好,接下来就来谈谈其 扩容及树化操作…

  • resize() - 扩容方法
    // 具体扩容方法
    final Node<K, V>[] resize() {
     
        // 获取全局table数组 (便于理解,定义为旧数组)
        Node<K, V>[] oldTab = table;
        // 获取数组容量 (便于理解,定义为旧数组容量)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 获取扩容阈值 (便于理解,定义为旧数组扩容阈值)
        int oldThr = threshold;
        // (便于理解,newCap定义为新数组容量、newThr定义为新数组扩容阈值)
        int newCap, newThr = 0;

        // 2.如旧数组容量>0 ->可知:数组已初始化过了
        if (oldCap > 0) {
     
            // 旧数组容量>=数组最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
     
                // 扩容阈值为Integer最大值
                threshold = Integer.MAX_VALUE;
                // 返回旧数组
                return oldTab;
            }
            // 新数组容量(旧数组容量*2) < 扩容最大值 且 旧数组容量 >= 数组默认容量(16) -> 新数组容量:2倍扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
     
                // 新数组扩容阈值:2倍扩容
                newThr = oldThr << 1; // double threshold
            }
        }

        // 3.旧数组扩容阈值>0(当前情况:table数组未初始化,threshold>0) ->可知:此种情况为调用有参构造方法时会进入此判断
        else if (oldThr > 0) {
     
            // 新数组容量设置为旧数组扩容阈值,HashMap使用threshold变量暂时保存initialCapacity值
            newCap = oldThr;
        }

        // 1.(当前情况:table数组未初始化,threshold=0) ->可知:当前为第一次put元素,数组及参数进行初始化
        else {
     
            // 新数组容量=16
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新数组扩容阈值=16*0.75=12
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // (针对上述情况3)新数组扩容阈值为0
        if (newThr == 0) {
     
            // ft=新数组容量*加载因子
            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数组
        table = newTab;

        // 旧数组不为空,进行遍历分组重新映射到新数组中
        if (oldTab != null) {
     
            // 遍历数组,并将元素映射到新数组中
            for (int j = 0; j < oldCap; ++j) {
     
                // e为当前数组位置对应节点
                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 {
     
                        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;
    }

还是一样,按照上述的步骤标记及注释,跟随笔者的思路一起来分析下:

  • 0:便于理解,变量定义名称;
        // 获取全局table数组 (便于理解,定义为旧数组)
        Node<K, V>[] oldTab = table;
        // 获取数组容量 (便于理解,定义为旧数组容量)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 获取扩容阈值 (便于理解,定义为旧数组扩容阈值)
        int oldThr = threshold;
        // (便于理解,newCap定义为新数组容量、newThr定义为新数组扩容阈值)
        int newCap, newThr = 0;

  • 1:table数组未初始化,threshold=0,表示当前为第一次put元素;
	// (当前情况:table数组未初始化,threshold=0) ->可知:当前为第一次put元素,数组及参数进行初始化
    else {
     
         // 新数组容量=16
         newCap = DEFAULT_INITIAL_CAPACITY;
         // 新数组扩容阈值=16*0.75=12
         newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     }

可知:第一次put元素,数组进行初始化,容量为16,扩容阈值为12;

  • 2:table数组已初始化过,表示数组已初始化过;
     // 如旧数组容量>0 ->可知:数组已初始化过了
     if (oldCap > 0) {
     
         // 旧数组容量>=数组最大容量
         if (oldCap >= MAXIMUM_CAPACITY) {
     
             // 扩容阈值为Integer最大值
             threshold = Integer.MAX_VALUE;
             // 返回旧数组
             return oldTab;
         }
         // 新数组容量(旧数组容量*2) < 扩容最大值 且 旧数组容量 >= 数组默认容量(16) -> 新数组容量:2倍扩容
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
     
             // 新数组扩容阈值:2倍扩容
             newThr = oldThr << 1; // double threshold
         }
     }

可知:数组扩容最大值为1073741824(1 << 30),扩容阈值最大值为Integer最大值2147483647(2^31-1),扩容后为为之前数组长度的2倍;

  • 3:table数组未初始化,threshold>0,此种情况为调用有参构造方法时会进入此判断;
	// 旧数组扩容阈值>0(当前情况:table数组未初始化,threshold>0) ->可知:此种情况为调用有参构造方法时会进入此判断
    else if (oldThr > 0) {
     
         // 新数组容量设置为旧数组扩容阈值,HashMap使用threshold变量暂时保存initialCapacity值
         newCap = oldThr;
     }
     
     // (此判断针对 当前情况 3)新数组扩容阈值为0,进入此判断
     if (newThr == 0) {
     
         // ft=新数组容量*加载因子
         float ft = (float) newCap * loadFactor;
         // 计算新数组扩容阈值
         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
     }

此种情况为调用有参构造实现,内部通过tableSizeFor(initialCapacity)方法计算扩容阈值;

可知:此情况数组容量值为扩容阈值,当数组容量小于数组最大值 且 (ft = 数组容量*加载因子)小于数组最大值,则扩容阈值为 ft,反之为Integer最大值2147483647(2^31-1),其中加载因子支持用户自定义,默认值为0.75;

  • 4:通过1/2/3步计算出的参数值,创建容量为newCap的数组并赋值给全局table,threshold 为newThr;
     // 扩容阈值设置为新数组扩容阈值
     threshold = newThr;
     // 创建新数组,长度为新数组容量
     @SuppressWarnings({
     "rawtypes", "unchecked"})
     Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
     // 将新数组赋值给全局table数组
     table = newTab;

  • 5:将旧数组的值重新映射到新数组中,最后返回新数组;
    // 旧数组不为空,进行遍历分组重新映射到新数组中
    if (oldTab != null) {
     
        // 遍历数组,并将元素映射到新数组中
        for (int j = 0; j < oldCap; ++j) {
     
            // e为当前数组位置对应节点
            Node<K, V> e;
            // 当前节点不为空
            if ((e = oldTab[j]) != null) {
     
                // 将循环节点设置为空
                oldTab[j] = null;

                // 1.当前节点下个节点为空 ->可知:当前数组位置对应只有一个节点
                if (e.next == null) {
     
                    // 重新计算当前节点对应新数组下标位置,并赋值
                    newTab[e.hash & (newCap - 1)] = e;
                }

                // 2.可知:当前数组位置对应为红黑树
                else if (e instanceof TreeNode) {
     
                    // 红黑树拆分
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                }

                // 3.可知:当前数组位置对应为链表
                else {
     
                    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;

通过笔者代码标记注释,可以看到映射值分为三种方式:

  1. 当前数组位置对应只有一个节点,直接重新计算对应新数组的位置并赋值;

  2. 当前数组位置对应为红黑树节点,进行红黑树拆分并赋值新数组中;

  3. 当前数组位置对应为链表,遍历链表重新计算每一个节点对应新数组的位置并赋值;

到这里,put()方法就结束了,相信屏幕前的猿友们或多或少受益匪浅;

学习亦是如此,当你翻过代码中最高的的一座山之后,剩下的只是一码平川;

猿友们此时此刻是不是正干劲十足呢,拿下HashMap近在咫尺喽。


⑷. get() - 获取元素方法


    /**
     * 入口
     */
    public V get(Object key) {
     
        Node<K,V> e;
        // 不得不说,1.8版本的作者代码功底很nice,个人很喜欢
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    // 获取对应节点方法
    final Node<K, V> getNode(int hash, Object key) {
     
        // 当前全局table
        Node<K, V>[] tab;
        // first为当前第一个节点;e用于循环的迭代变量,代表当前节点
        Node<K, V> first, e;
        // tab数组长度
        int n;
        // first节点的key值
        K k;
        
        // 如果当前tab不为空 && tab长度>0 && 当前数组对应下标位置节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
     
        
            // 1.与first节点hash值相等 && 与first节点key值相等,则返回first节点
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            // first.next不为空,继续遍历查找
            if ((e = first.next) != null) {
     

                //2. 红黑树方式获取
                if (first instanceof TreeNode)
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);

                // 3.遍历链表获取
                do {
     
                    // 与当前节点hash值相等 && 与当前节点key值相等,则返回当前节点
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null; // 未找到返回null
    }
 

相信对于屏幕前已拿下put()方法的你来说,获取元素方法,简直very so easy;

从源码中可以看出,获取元素时分为四种方式:

  1. 通过key计算对应数组下标从而获取对应节点(first节点),当获取元素与first节点hash值相等 且 key值相等,则返回first节点,最终返回其value值;

  2. 当与first节点对比不一致且first节点的next节点不为空时,并且为红黑树节点时,已红黑树方式获取节点并返回,最终返回其value值;

  3. 当与first节点对比不一致且first节点的next节点不为空时并且不为红黑树节点时,表示当前为链表,遍历链表进行对比,如存在与当前节点hash值相等 且 key值相等,则返回当前节点,最终返回其value值:

  4. 如未找到,则返回null;


⑸. remove() - 删除元素方法


    /**
     * 入口
     */
    public V remove(Object key) {
     
        Node<K,V> e;
        // removeNode()为具体删除节点方法
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }

    // 删除元素方法
    final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
     
        // 当前全局table
        Node<K, V>[] tab;
        // 当前数组对应位置的节点/链表/红黑树
        Node<K, V> p;
        // n为tab数组长度;index当前删除元素所在数组下标位置
        int n, index;

        // 当前tab(全局数组)不为空 && tab数组长度>0 && 当前数组对应下标位置节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
     
            // node为返回的删除节点;e用于循环的迭代变量,代表当前节点
            Node<K, V> node = null, e;
            // 当前节点对应key值
            K k;
            // 当前节点对应value值
            V v;

            /**
             * 1.获取删除节点
             */
            // (当前p为头结点,node节点也为头结点)与头节点hash值相等 && 与头节点key值相等 ->可知:删除节点为头节点
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;

            // p.next不为空,继续遍历查找
            else if ((e = p.next) != null) {
     

                // 红黑树 - 获取对应删除节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K, V>) p).getTreeNode(hash, key);

                // 遍历链表 - 获取对应删除节点
                else {
     
                    do {
     
                        // 与当前节点hash值相等 && 与当前节点key值相等,则为删除节点
                        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
     
                            // **如进入此判断 ->可知:node节点为p.next节点
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }

            /**
             * 2.删除节点
             */
            // 查找的删除节点不为空 && (matchValue默认为false,!matchValue为true || value相等)
            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);

                // node==p表示删除为链表头结点,p与node都为头结点
                else if (node == p)
                    // 将头节点的next节点赋值给tab数组index下标位置处
                    tab[index] = node.next;

                // 当前node节点为p.next节点
                else
                    // 将p的下个节点赋值为node节点的下个节点,因删除节点为node,需改变其前后节点对应引用关系
                    p.next = node.next;

                // 操作次数++
                ++modCount;
                // HashMap元素个数--
                --size;
                // 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
                afterNodeRemoval(node);
                // 返回删除节点
                return node;
            }
        }
        // 未找到返回null
        return null;
    }
    

从笔者源码标记中可以看出,删除元素主要分为2步:

  • 1:先获取要删除的元素节点;
     /**
      * 1.获取删除节点
      */
     // (当前p为头结点,node节点也为头结点)与头节点hash值相等 && 与头节点key值相等 ->可知:删除节点为头节点
     if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
         node = p;

     // p.next不为空,继续遍历查找
     else if ((e = p.next) != null) {
     

         // 红黑树 - 获取对应删除节点
         if (p instanceof TreeNode)
             node = ((TreeNode<K, V>) p).getTreeNode(hash, key);

         // 遍历链表 - 获取对应删除节点
         else {
     
             do {
     
                 // 与当前节点hash值相等 && 与当前节点key值相等,则为删除节点
                 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
     
                     // **如进入此判断 ->可知:node节点为p.next节点
                     node = e;
                     break;
                 }
                 p = e;
             } while ((e = e.next) != null);
         }
     }

此代码与get()获取元素方法一致,在此不再赘述;

  • 2:删除获取的元素节点;
    /**
     * 2.删除节点
     */
    // 查找的删除节点不为空 && (matchValue默认为false,!matchValue为true || value相等)
    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);

        // node==p表示删除为链表头结点,p与node都为头结点
        else if (node == p)
            // 将头节点的next节点赋值给tab数组index下标位置处
            tab[index] = node.next;

        // 当前node节点为p.next节点
        else
            // 将p的下个节点赋值为node节点的下个节点,因删除节点为node,需改变其前后节点对应引用关系
            p.next = node.next;

        // 操作次数++
        ++modCount;
        // HashMap元素个数--
        --size;
        // 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
        afterNodeRemoval(node);
        // 返回删除节点
        return node;
    }

此代码与get()获取元素方法一致,在此不再赘述;

  • 红黑树方式删除节点:
    // 红黑树方式删除节点
    if (node instanceof TreeNode) {
     
		((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
	}
  • 删除头结点:
    // node==p表示删除为链表头结点,p与node都为头结点
    else if (node == p) {
     
		// 将头节点的next节点赋值给tab数组index下标位置处
        tab[index] = node.next;
	}
  • 删除非头结点:
    // 当前node节点为p.next节点(这里不理解的猿友,请结合上述步骤1中-遍历链表获取对应删除节点中p节点与node节点的关系)
    else {
     
        // 将p的下个节点赋值为node节点的下个节点,因删除节点为node,需改变其前后节点对应引用关系
        p.next = node.next;
	}
  • 参数变更:
    // 操作次数++
    ++modCount;
    // HashMap元素个数--
    --size;
    // 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
    afterNodeRemoval(node);
    // 返回删除节点 - 如未找到->返回null
    return node;

到这里… 恭黑雷(恭喜你),顺利拿下HashMap;

此时此刻,屏幕前拥有盛世美颜的你,给也同样拥有盛世美颜的暖男笔者,赏脸来个三连吧…笔者已迫不及待准备好么么哒,亲在…;



  • HashMap(jdk1.8版本)总结:
  1. 底层为数组 + 链表(单向链表) + 红黑树 (Red-Black Trees 笔者之后会另起一篇详解);
  2. 线程不安全;
  3. 数组初始容量为16,最大值为1073741824(1 << 30),数组扩容为之前数组的2倍;
  4. 扩容加载因子为0.75,用户也可通过有参构造指定加载因子;
  5. 扩容阈值为数组容量*加载因子,默认初始扩容阈值为16 * 0.75 = 12;
  6. 尾插法 - 有效解决扩容死循环问题(1.7版本为头插法);
  7. 有modCount;



2. HashMap(jdk1.7版本)

面试一次问一次,HashMap是该拿下了之 HashMap1.7版本



3. ConcurrentHashMap

面试一次问一次,HashMap是该拿下了之 ConcurrentHashMap



~~   码上福利


大家好,我是猿医生:

在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,扫码一起学习吧…
https://marketing.csdn.net/poster/145?utm_source=765669642

你可能感兴趣的:(集合源码系列,java)