推荐学习

  • 刷透近200道数据结构与算法,成功加冕“题王”,挤进梦中的字节
  • 面试官杠上Spring是种什么体验?莫慌,送你一套面试/大纲/源码

前言

在介绍HashMap之前先了解一个别的东西:红黑树。

这边提前声明下,发布文章的时候没太注意,有点本末倒置,将源码放在了最上面,文字解析过程和图文放在了源码后面,还请见谅,以后小编多多注意。

01 什么是红黑树?

红黑树其实是一种自平衡二叉查找树。它的左右子树高度可能大于1,严格意义上来讲,红黑树并不是完全平衡的二叉树。那么又引入了另一个问题:什么是二叉查找树 ? 二叉查找树是有什么缺点呢 ? 为什么会衍生出红黑树呢 ?

如图所示:这就是一个二叉查找树的实例。每次数据插入的时候,都是先判断是否比根节点大 (当前数据 > 根节点 ? 从右边插入 : 从左边插入 ) 这是一个三元运算符 。而且插入都是从最下面的叶子节点做比较再选择是否在叶子节点的左边还是右边。这里便会产生一个缺点: 树的高度问题。数据越多,高度越大。这样导致查询最下面的叶子节点耗时过长。于是便出现了它其中的一种红黑树

如图所示:便是一个红黑树了。红黑树维护了两种不同的颜色。

每个节点要么是红色,要么是黑色

根节点必须为黑色

红色节点不可以连续 (红色节点的孩子不能为红色)

对于每个节点,从该节点到 null (树尾端)的任何路径,都含有相同个数的黑色节点。

直观上看红黑树的插入和二叉查找树的插入相似,只是维护了两种不同颜色而且,为什么会说是一个平衡二叉树呢? 于是为了解决这个问题引入了一些平衡操作:变色旋转。旋转又分为 左旋右旋

02 变色

变色可以分为很多种情况。这里只是说其中一个,因为不是本次重点内容。有兴趣的可以去了解。

从图中可以看出 B 的左节点孩子 也是红色。明显违背了红黑树的概念 (红色节点不可以连续) 那么经过调整后(图中的右侧) 。将 B 节点变为黑色。于是解决了不可以连续的问题。但是又会产生一个新的问题。该侧路径的黑色节点多了一个,导致两边黑色节点不一致。

于是我们又将 B 的父节点 A 变色 红色。虽然是解决了两侧黑色节点数量一致的问题,但是又会产生上一个 不可以连续的问题。 于是我们又将 A 的右节点孩子也改变为 黑色。如下图所示 这样这两个问题都得到解决。

了解了如何通过 变色 调整平衡后,那么下来就看看 旋转是如何操作的吧

03 旋转

上面我们提到过,旋转分为 左旋 和 右旋,那么就分别来展示下吧

3.1 左旋

左旋 : 自己的右节点成为自己的夫节点。取而代之的是 右节点的左节点成为了自己的右节点。

3.2 右旋

右旋 :自己的左节点成为自己的父节点。取而代之的是 左节点的右节点成为自己的左节点。

如果到这里你还没明白 旋转的话,给大家来一组动态展示图吧

至此,到这里就结束了。说是讲 HashMap 的 结果却说了这么就的 数据结构,但是没办法啊,如果你不了解上面的东西。你可能不太能看懂下面的源码部分。

04 简介

说了那么多,终于到了正文了。 HashMap 实现了 Map 接口,JDK1.7由 数组 + 链表实现, 1.8后由 数组 + 链表 + 红黑树实现。是以 key,value 为形式的存储容器,且允许key 为null ,也允许 value 为 null 。该容器线程不安全。

JDK 1.7 的 HashMap 结构图

JDK 1.8 的 HashMap 结构图

4.1 HashMap 的基本元素

   /**     *   默认的初始容量   1 << 4   ==   2^4 = 16     */    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16    /**     *    最大的容量   2^30     */    static final int MAXIMUM_CAPACITY = 1 << 30;    /**     *  默认的负载因子     *  负载因子表示了一个当前散列的使用程度     *  容器个数 size > 负载因子 * 数组长度  就需要进行扩容     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;     /**     *  JDK 1.8 新增      *     如果数组中某一个链表 >= 8 需要转化为红黑树     */    static final int TREEIFY_THRESHOLD = 8;    /**     *  JDK 1.8 新增     *     如果数组中某一个链表转化为红黑树后的节点 < 6 的时候 继续转为 链表     */    static final int UNTREEIFY_THRESHOLD = 6;    /**     *  JDk 1.8 新增     *  如果当链表元素 >= 8 并且数组 > 64 的时候转化红黑树     */    static final int MIN_TREEIFY_CAPACITY = 64;    /**     *   由之前的 Entry 该变为 Node 类型,其实Node为Map.Entry的接口实现类。     *   (Entry 为 Map 接口中的一个内部接口)     */transient Node[] table;   // 记录键值对的个数    transient int size;    /**     *  记录集合中元素修改的次数     */    transient int modCount;    /**     *  阈值 : 所能容纳的元素个数,当 size > threshold 的时候 就会扩容     * threshold = 负载因子 * 数组长度     */    int threshold;

4.2 Node

static class Node implements Map.Entry {    // hash 值        final int hash;        final K key;        V value;    // 下节点指针        Node next;        Node(int hash, K key, V value, Node next) {            this.hash = hash;            this.key = key;            this.value = value;            this.next = next;        }

4.3 构造方法

HashMap()

public HashMap() {    // 默认负载因子为 0.75        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted    }1234

HashMap(int initialCapacity)

 public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR); }// 指定初始容量值,默认负载因子 0.75public HashMap(int initialCapacity, float loadFactor) {    // 初始容量不 < 0        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);    // 初始容量 > 2^30 则容量为 2^30        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;    // 负载因子不能小0 || 没有值        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);    // 负载因子        this.loadFactor = loadFactor;    // 阈值        this.threshold = tableSizeFor(initialCapacity); }

HashMap(Map extends K, ? extends V> m)

// 基于Map 创建一个新的 HashMappublic HashMap(Map extends K, ? extends V> m) {        this.loadFactor = DEFAULT_LOAD_FACTOR;        putMapEntries(m, false);}

4.4 添加方法 put

 public V put(K key, V value) {        return putVal(hash(key), key, value, false, true); }final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {        Node[] tab; Node p; int n, i;    // table 为 null 新建一个table        if ((tab = table) == null || (n = tab.length) == 0)           // 采用 resize() 新建 table            n = (tab = resize()).length;    //根据 数据长度 和 hash 值 进行 与 运算得到一个数值下标,如果这个下标不存在元素则直接存储        if ((p = tab[i = (n - 1) & hash]) == null)            tab[i] = newNode(hash, key, value, null);        else {            // 已经存在元素            Node 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)p).putTreeVal(this, tab, hash, key, value);            // 若是链表存储            else {                // 循环链表                 for (int binCount = 0; ; ++binCount) {                    // 直到链表尾部还未有重复 key                    if ((e = p.next) == null) {                        // 新建节点存储                        p.next = newNode(hash, key, value, null);                        // 若链表长度 >= 指定值 8 - 1                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                            // 转化红黑树                            treeifyBin(tab, hash);                        break;                    }                    // 若发现相同 key 则结束循环                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = 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;  }

若代码层面你还在懵懂,那么就用文字在梳理一下流程吧

调用 hash 获取 该 key 的 hash 值

判断 table 是否为 null。为 null 则新建一个 table 数组

根据 数组长度 和 哈希值 获取下标位置。如果该下标没有数据 则直接存储

(1)有数据的情况下 判断是否存在冲突,冲突直接获取此节点

(2)判断是否用红黑树存储,是则利用红黑树中的方法存储键值对。

(3)链表方式存储,循环链表,是否存在与其中数据冲突

​ I. 若循环结束没有数据冲突,则在尾部直接插入,同时判断 是否 >= 8 -1 转化红黑树

​ II. 若存在冲突的key, 则立刻结束循环,同时获取该节点

将新值 替换 旧值

注意一个点 : 如果是自定义类型作为 HashMap 的 key 时候,这个时候需要覆盖 hashCode 和 equals 方法。

原则:

​ 覆盖hashCode方法 :须保证内容相同的元素返回相同的 hash 值,不同的元素,尽可能返回不同的hash值。

​ 覆盖 equls 方法 : 内容相同的对象返回 true。

说了这么多,再来一点图吧,这样 三者 结合起来或许可以更透彻一些哦。话不多说,就直接上图吧。

当插入 key 为 的这条数据的时候,会通过 key 的 hash 和 数组长度 计算出一个下标。比如此时这个这个下标为 0 ,0下标位置此时刚好没有数据。那么这个数据就会直接插入在这里。 当第二次插入一个数据为 key帅子 的时候。又通过 key 的 hash 和 数组长度计算出下标为 2。此时这里数据没有数据,则直接插入。如图 1 所示。之前我们提到了一个链表。那么链表在哪里体现的呢。那么看下图 2 吧。 这个时候我们要插入一个 key 为 子帅的数据。在某种程序上。子帅 和 帅子 的 hash可能会一致。那么就会导致数据冲突。此时就采用了 拉链法 来解决冲突问题。这里需要特别注意一个问题: 上图展示的是 JDK 1.7 的插入方法 采用的是 头插法,也就是说 子帅 和 帅子 产生冲突后。 会将子帅放在头节点。它的后指针指向 帅子 。

为什么会在上面特意强调 JDK 1.7 采用的是头插呢 ? 难道之后不是这种插入了吗

这里就不在画 JDK 1.8 的图了。大家自行脑补,有点不厚道了。 1.8 和 上面的图没太大的区别,上面也强调了很多次。 链表 >= 8-1 && 数组长度 > 64 转化为 红黑树。 图大家脑补下吧。 这里重点来说说 尾插法

思考一个问题: 为什么要用尾插 ? 难道头插有缺点吗?

这个问题, 大家先思考下。在 resize() 的时候会带来详细的讲解。

4.5 获取方法 get

  final Node getNode(int hash, Object key) {        Node[] tab; Node first, e; int n; K k;      // tab 不为 null 同时 table 长度 > 0 同时数组对应下对应下标内容不为 null        if ((tab = table) != null && (n = tab.length) > 0 &&            (first = tab[(n - 1) & hash]) != null) {            // 如果数组对应下标的第一个节点 key 和 查找的 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)                    // 通过 红黑树获取 key 对用的 Node                    return ((TreeNode)first).getTreeNode(hash, key);                // 链表形式, 遍历所有节点。                do {                    // 如果查找到相同的 直接返回                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        return e;                } while ((e = e.next) != null);            }        }      // 没有则返回 null        return null;    }

如果 table 不为 null,且对用对应的存储下标不为 null判断第一个节点是否相同。相同则返回第一个节点不是所寻找数据。是否为红黑树存储, 是则利用红黑树获取对应的 Node链表存储,遍历链表,寻找相同的 key 返回 Node如果 table 为 null , 或是没有对应的数据,返回 null

4.6 扩容 resize

JDK1.8 扩容

final Node[] resize() {        Node[] oldTab = table;        int oldCap = (oldTab == null) ? 0 : oldTab.length;        int oldThr = threshold;        int newCap, newThr = 0;        //  table 长度是否 > 0        if (oldCap > 0) {            // table 长度 >= 2^30 则无法扩容            if (oldCap >= MAXIMUM_CAPACITY) {                threshold = Integer.MAX_VALUE;                // 返回原有数组                return oldTab;            }            // 没有超过最大 空间,则扩大原有的 2 倍            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;    // 基于新的容量 创建新的 Node 数组 和 哈希表        @SuppressWarnings({"rawtypes","unchecked"})            Node[] newTab = (Node[])new Node[newCap];        table = newTab;        if (oldTab != null) {            // 遍历原有 table 重新计算每一个元素的位置            for (int j = 0; j < oldCap; ++j) {                Node e;                // 对应的下标元素不为null                if ((e = oldTab[j]) != null) {                    oldTab[j] = null;                    // 该数组下只有一个元素                    if (e.next == null)                        // 存储在新的数组上                        newTab[e.hash & (newCap - 1)] = e;                    // 红黑树存储                     else if (e instanceof TreeNode)                        // 将红黑树分离                        ((TreeNode)e).split(this, newTab, j, oldCap);                    else { // preserve order                        // 链表存储                        Node loHead = null, loTail = null;                        Node hiHead = null, hiTail = null;                        Node 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;    }

JDK 1.7 扩容

  void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        Entry[] newTable = new Entry[newCapacity];        transfer(newTable, initHashSeedAsNeeded(newCapacity));        table = newTable;        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);    }  void transfer(Entry[] newTable, boolean rehash) {        int newCapacity = newTable.length;      // 扩容后的数组赋值        for (Entry e : table) {            while(null != e) {                Entry next = e.next;                // 这里会重新计算 key 的值 。 Jdk 1.8 在这里做了改变                if (rehash) {                    e.hash = null == e.key ? 0 : hash(e.key);                }                int i = indexFor(e.hash, newCapacity);                e.next = newTable[i];                newTable[i] = e;                e = next;            }        }  }

看来这么多源码, 是不是觉得这也太枯燥了吧,好像也就是似懂非懂的样子吧。很奇怪。没关系,还是用代码结合图文来看一看吧。在这里也会对之前的提到了为什么采用 尾插法 做出了解析

先看下单线程下的扩容吧

这是一个单线程下扩容。 如代码所讲,会将 数组扩容到原来的 2 倍,然后再遍历所有的节点,往新的数组赋值。这个时候有人会说了。为什么 C 跑到其他下标了 A 和 B 还在原来的下标呢? 因为 下标是取决了 keyhash 和 数组长度的。数组都扩容了那么下标就会导致某种情况下的改动。 那是不是每一个Key都会进行一次哈希呢 原则上是这样的,但是在 1.8 后 做出了调整。不会导致去 rehash 因为每次都 hash 会消耗掉很多的性能。

多线程的扩容

先提前说明下上图中三个分别代表的意义

  • 第一个是 线程 1 在执行完扩容后 被 CPU 调度挂起,开始在执行 线程2
  • 第二个是 线程2 开始执行扩容并进行扩容后的数组赋值 执行完后 再去继续执行 线程1
  • 第三个是 线程1的继续执行过程

那就来一一解释下吧

先提前说一个不愉快的事情,在看这段文字的时候,请先大致看一下扩容的源码还有上文中的 单线程以及多线程的扩容图。有个宏观的了解后再来品这段文字。某种程度上,小编不愿意将相同的事情重复很多次。请理解!!!

线程1执行完扩容方法后(这里是指执行完扩容那行代码,并不是整个扩容方法)被挂起,这里不多说自行看上面代码弥补。紧接着开始执行了线程2 。经过扩容,并且重新计算 下标的操作后。此时的状态变为了图2 右边所示的样子。原本的状态是 B 的 next 是指向 A 的经过线程2 的扩容导致 A 的next 指向了 B 此时。线程2 被挂起了,开始去执行线程1 此刻线程1Anext 已经不指向 null 了。经过 线程2 的处理 ,此刻 指向的是B 这个时候就产生了一个特别严重的问题 A 指向 B , B 指向A 形成了死循环。

所以就抛弃了头插法,引入了尾插法。为什么尾插法,不会引起这些问题呢。我想答案已经在你的脑海中了。如果你还没想明白。那么你就跟着小编这边文章的 添加 put 章节开始一步步的跟着用尾插法 插入值 再到扩容试试。所以这里我就不再过多介绍,想知道的小伙伴,已经跟着小编的思路在画图了哦。

作者: 一个在路上奔跑的程序猿

原文链接:https://blog.csdn.net/weixin_45465895/article/details/106854581

hashmap为什么用红黑树_关于HashMap的实现,一篇文章带你彻底搞懂,再也不用担心被欺负相关推荐

  1. 遍历HashMap源码——红黑树原理、HashMap红黑树实现与反树型化(三)

    本章将是HashMap源码的最后一章,将介绍红黑树及其实现,HashMap的remove方法与反树型化.长文预警~~ 遍历HashMap源码--红黑树原理.HashMap红黑树实现与反树型化 什么是红 ...

  2. hashmap为什么用红黑树_全网最全,面试常问的HashMap知识点

    引言 其实我很早以前就想写一篇关于HashMap的面试专题.对于JAVA求职者来说,HashMap可谓是集合类的重中之重,甚至你在复习的时候,其他集合类都不用看,专攻HashMap即可. 然而,鉴于网 ...

  3. hashmap为什么用红黑树_要看HashMap源码,先来看看它的设计思想

    HashMap 是日常开发中,用的最多的集合类之一,也是面试中经常被问到的 Java 类之一.同时,HashMap 在实现方式上面又有十分典型的范例.不管是从哪一方面来看,学习 HashMap 都可以 ...

  4. 红黑树效率为甚恶魔是log_一文带你彻底读懂红黑树(附详细图解)

    红黑树简介 红黑树是一种自平衡的二叉查找树,是一种高效的查找树.它是由 Rudolf Bayer 于1972年发明,在当时被称为对称二叉 B 树(symmetric binary B-trees).后 ...

  5. 请列举你了解的分布式锁_这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排!...

    什么是分布式锁? 大家好,我是jack xu,今天跟大家聊一聊分布式锁.首先说下什么是分布式锁,当我们在进行下订单减库存,抢票,选课,抢红包这些业务场景时,如果在此处没有锁的控制,会导致很严重的问题. ...

  6. js等待 callback 执行完毕_前端开发,一篇文章让你彻底搞懂,什么是JavaScript执行机制!...

    不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序.因为javascript是一门单线程语言,所以我们可 ...

  7. 用html5做一个介绍自己家乡的页面_(近万字)一篇文章带你了解HTML5和CSS3开发基础与应用-适合前端面试必备...

    作者 | Jeskson来源 | 达达前端小酒馆 HTML5和CSS3开发基础与应用,详细说明HTML5的新特性和新增加元素,CSS3的新特性,新增加的选择器,新的布局,盒子模型,文本,边框,渐变,变 ...

  8. python论文排版格式_一张图总结科研必备的软件清单,妈妈再也不用担心我的工作了...

    好的工具可以让工作事半功倍,那么哪些软件有助于提高学习效率,促使成果尽快产出? 废话不多说,先上一张图,它归纳了科研必备软件工具,具体说明见下文. 1.入门配置三件套 Word, excel, ppt ...

  9. [Java 8 HashMap 详解系列]7.HashMap 中的红黑树原理

    [Java 8 HashMap 详解系列] 文章目录 1.HashMap 的存储数据结构 2.HashMap 中 Key 的 index 是怎样计算的? 3.HashMap 的 put() 方法执行原 ...

最新文章

  1. Java反射机制分析指南
  2. python名称由来_Python的由来与使用介绍
  3. ubuntu/mint 恢复模式 报read-only file system 的解决方法
  4. css中哪些属性与创建多列相关,css3中的新增属性有哪些
  5. ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接
  6. Scala数组的基本操作,数组进阶操作,多维数组
  7. python 多个列表合并_Python对两个有序列表进行合并和排序的例子
  8. 数学家的浪漫,你想都想不到!
  9. 用sum函数求三个数和C语言,C语言用函数写两数之和.doc
  10. 纯手写实现HashMap
  11. Arbin数据导出---cellpy库的安装与使用
  12. poj 3735 Training little cats (矩阵快速幂)
  13. 漫网漫画APP源码包含后台完整版
  14. 如何查询Linux软件安装源,Zypper——suse软件查询 安装 升级 与 软件源编辑
  15. 如何申请免费SSL证书?宝塔面板SSL证书安装部署完整教程
  16. No Assembler service found - please make sure that the right jars are in your classpath
  17. 很多次游戏的最后取胜实际上都有很强的偶然性
  18. 把 Win 8.1 升级成 Windows 2012 R2 (再续)
  19. 利用OpenCV与Sklearn实现的Logistic Regression
  20. php sequelize,Sequelize 中文文档 v4 - Querying - 查询

热门文章

  1. GitHub 2021年度报告发布:中国755万开发者排名全球第二!
  2. 全球最大规模学术不端调查显示,53%的博士生会从事有问题的研究
  3. 北大博士干了半年外卖骑手,写出 AI 伦理论文登上顶刊,“系统知道一切”
  4. 导师:寒假复现几篇顶会论文?答:3天1篇!
  5. YOLO算法史上最全综述:从YOLOv1到YOLOv5
  6. 图神经网络越深,表现就一定越好吗?
  7. 用 Pytorch 理解卷积网络
  8. 互掐了半辈子的两个数学巨头,到最后连单身问题都没解决
  9. 美多商城之购物车(购物车管理1)
  10. celery中间件:broker