Java的学习: HashMap

141 阅读2分钟

上次面试中问到了HashMap的一系列问题,如底层实现,是否线程安全。中间也聊到扩容。回答不上来,特此学习记录。

底层实现

JDK 1.8版本中是数组+链表+红黑树, 而JDK 1.7版本中没有红黑树

  • 数组是存放对数据的引用

    源码是这样实现数组的。 1.8版本从Entry改为Node。

    version 1.7version 1.8
    transient Entry[] tabletransient Node<K, V>[] table

    数组的index为Key的hashcode。

  • 链表和红黑树是为了解决hashcode的冲突

    • 当链表长度大于8, 且数组大小大于64时,会把链表结构转为红黑树结构。

        static final int TREEIFY_THRESHOLD = 8;
        static final int MIN_TREEIFY_CAPACITY = 64;
      

      当数组大小小于64时,只会发生扩容不会转为红黑树

    • 当长度小于等于6时,红黑树结构会转为链表结构。

        static final int UNTREEIFY_THRESHOLD = 6;
      
    • 有了链表,为什么还会出现红黑树呢?

      是对查询的优化,链表的查找时间复杂度为O(n)。而红黑树具有快速增删查改的特点。这样可以解决链表过长时操作比较慢的问题。

刚刚提到链表结构和红黑树结构之间的转化中,说到了数组大小,链表长度,还有扩容的话题。那么HashMap中的扩容是否和数组的扩容类似呢?

扩容机制

如果需要扩容的话,说明原本的容量已经是不够用的了。那么HashMap初始化的容量到底是多少呢?首先HashMap是可以指定初始化容量大小的,接下来的内容就是对初始化自定义与否的情形。

HashMap的初始化容量(capacity)

  • 当不指定容量的情况下,初始化容量为16, 源码如下。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

  • 当指定容量的情况下,JDK 1.8和JDK 1.7设定的时机也是不同的。

    JDK 1.7中,要等到第一次PUT操作才进行容量的设定。 JDK 1.8中,调用构造函数时,就会进行容量的设定。

那么为什么在有默认初始化容量的情况下还可以自定义初始化容量呢?并且阿里巴巴Java开发手册中还推荐集合初始化时,制定集合初始化大小

主要原因就是初始化容量远远小于数据所需的容量时,HashMap需要不断扩容,而HashMap中扩容机制决定了每次扩容需要重建hash表,会严重影响性能。

那么如果指定初始化大小的话,多少合适呢?

当指定初始化容量是时,就会有一次对输入值的扩容,会取距离最近的2的幂次方。

具体参考 blog.csdn.net/moakun/arti… (如果不能引用我会删除)

这其中提到了负载因子(loader factor),默认值为0.75。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

这个负载因子,也是HashMap扩容机制中阈值的决定因素。我现在看到的博客中基本以0.75是基于空间利用率和时间效率的平衡决定的。另有Linn_cn在2020.03.18发表的文章中的一段解释,链接附在文末。

因为当扩容因子设置比较大的时候,相当于扩容的门槛就变高了,发生扩容的频率变低了。
但此时发生Hash冲突的几率就会提升,当冲突的元素过多的时候,变成链表或者红黑树都会增加了查找成本。
而扩容因子过小的时候,会频繁触发扩容,占用的空间变大,比如重新计算Hash等,使得操作性能会比较高。

阈值为负载因子*最大容量。当元素个数大于阈值时,触发扩容。扩容有两步

1. resize(): 创建一个新的空数组,长度为原来的两倍
2. rehash(): 遍历原数组,把所有node重新hash到新数组。

HashMap的优缺点

  • 优点:

    1. 查询:O(1) 的时间复杂度
    2. 动态的存储
  • 缺点:

    1. 扩容时,需要重建hash表,非常影响性能。
    2. 不是线程安全。

HashMap线程不安全

HashMap的实现里没有锁的机制, 所以它本身不是线程安全的。

  • 解决方式

    1. 有另一数据结构:ConcurrentHashMap。据我看网上大家的文章中,大多提到应用场景比较少。
    2. 替换成HashTable。不过HashTable大多已经弃用,性能比较低。
    3. 使用Collections类的synchronizedMap方法。

Put方法

  1. 检查数组是否还有位置

    • 有:2.
    • 无:扩容
  2. 根据Key计算Hashcode确定数组位置

  3. 检查该数组位置上有没有内容

    • 有:比较key和hashcode值 (equals, ==)

      • 一样: 替换原内容

      • 不一样:

        • 该数组位置对应的结构是红黑树还是链表

          • 红黑树: 4.
          • 链表:循环至next是null节点 或找到hash和key都相同的节点

          在放入后,检查链表长度是否到了与红黑树结构转换值,8,并比较数组长度与64.

    • 无:4.

  4. 放入键值对

Get 方法

  1. 判断数组是非为空

    • 是: return null
    • 否:2.
  2. 判断头节点是否为目标值,比较hashcode及key值

    • 是: return 头节点
    • 否: 3.
  3. 判断结构是否为红黑树

    • 是: getTreeNode
    • 否:即为链表,遍历即可

引用:juejin.cn/post/684490…