深入理解HashMap第一篇

HashMap

1.描述

  • HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现。设计目标是尽量实现哈希表O(1)级别的增删改查效果,与HashTable主要区别为不支持同步和允许null作为key和value

    各项默认值

    1. 初始容量(capacity):16(1<<4),即2^4

    2. 最大容量:(1<<30),即2^30

    3. 默认扩容因子(loadFactor):0.75

    4. 自动扩容阈值:当前HashMap元素个数 > capacity * loadFactor,会自动扩容,并重新hash。

    5. 扩容机制:扩容至当前HashMap容量*2.0,且每次扩容后的容量必须为2的n次方

    6. 容量必须为2的n次方的原因:

      HashMap对元素进行读、写操作时,需要将Map元素的Key的哈希值对数组长度(HashMap 的容量)进行取模运算,结果作为该元素在数组中的索引(index),在计算机中,取模运算的代价远高于位运算的代价,当数组长度为 2^n 时,可以将Map元素的Key的hashcode对 (2^n)-1 进行 与运算(&),效果与对数组长度进行取模运算相等,所以为了提高 HashMap 的操作效率,规定 HashMap 的容量必须为2的n次方,即2^n。

2.底层实现 1.7与1.8及以后

2.1JDK1.8之前

​ 采用 数组+链表 实现,形成一个数组带着多个桶的结构,每个数组元素就是一个桶,而数组索引就是每个桶中链表的表头,每一个Map元素的数据结构是一个 Entry。

    • 数组存储区间连续,占用内存较多,寻址容易,插入和删除困难。
    • 链表存储区间离散,占用内存较少,寻址困难,插入和删除容易。
  • 遗留问题

    • 在某些极端情况下,会导致大量元素都存放在同一个桶(数组索引是链表的表头)的链表中,此时的HashMap 就相当于一个单链表,假设链表中的元素个数为n个,则其操作时间复杂度就变成了O(n),完全失去了哈希表的优势。

2.2JDK1.8及以后:

  • 采用 数组+链表+红黑树 实现。每个Map元素的数据结构是 Node(链表)TreeNode(红黑树)

  • 解决遗留问题:

    • jdk1.8时默认还是使用 数组+链表 实现,只是元素的数据结构从 Entry 变为了 Node,当存储在同一个桶中的元素过多时,才会将链表转换为红黑树实现,保证其操作时间复杂度为 O(logn)。
  • 链表与红黑树的转换条件

    • 链表->红黑树:整个 HashMap 中的元素个数 >= 64 且 同一个桶下的链表中的元素个数 > 8;此时Map元素的数据结构从 Node 转为 TreeNode ;
    • 红黑树->链表:同一个桶下的链表中的元素个数 < 6;此时元素的数据结构为 Node;

3.HashMap Q/A start

1.链表转红黑树的阈值为什么是8?

  • 因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

  • 还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

2.为什么是要转为红黑树,二叉平衡树不行吗?

  • 因为平衡二叉是高度平衡的树, 当平衡被破坏时,需要要 rebalance(再平衡), 开销会比红黑树大.

  • 原因:

    • 插入引起的不平衡:

      插入一个node引起了树的不平衡,平衡二叉树和红黑树都是最多只需要2次旋转操作,即两者都是O(1);

    • 删除引起的不平衡:

      最坏情况下,平衡二叉树需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级是O(logN)此,而红黑树最多只需3次旋转,只需要O(1)的复杂度。

3.是否线程安全?不安全的场景

  • HashMap线程不安全,主要表现在:

​ 多线程同时put时可能会丢失值(前面的put被后面的覆盖)。

​ 多线程扩容时会出现环状结构,造成死循环。(1.8之前)

​ 多线程使用迭代器时会触发fast-fail机制。

  • 如何解决?

​ 使用 CollectionssynchronizedMap() 对其进行包装,使其线程安全。(锁整个表,效率差)

​ 直接使用 线程安全的ConcurrentHashMap。(分段锁/CAS,效率相对较高)

  • 线程安全的map有哪些?

​ hashtable,concurrentHashMap

4.说下hashmap put的过程

  • key的hashcode高16位与低16位异或运算得到新的hash,为了让数据(特别是在当前容量不大的情况下)散列更均匀,然后把异或计算出的新hash与此时的hashmap容量-1做&运算,得到插入下标。

5.为什么要做&运算,还有什么方式

  • :二进制运算速度快,还可以取模。之后如果下标位数组无数据则直接插入,如果有数据则链表往下逐个进行hash比较,如果产生hash碰撞再进行==或者equals比较key是否一样,一样则覆盖原数据,否则添加到链表后面。(jdk7是新元素插入至链表头部

6.rehash后的元素具体去向问题(1.7和1.8元素rehash后元素去向是不同的)

7.扩容机制jdk7及以后

当hashmap中数据量超过当前容量*扩容因子(默认0.75)则扩容为原来的2倍,问:当前插入的位置上没有元素就不扩容(jdk7)

8.为什么是2次幂扩容

  • 2进制运算快;
  • hash与当前容量-1做&运算很快且很巧妙地获得元素下标;
  • 扩容后能巧妙地重新分配元素位置)说下扩容的rehash,扩容后的部分节点数据会重新定位,具体规则是hash&原容量,得到无非两个结果:0和1,如果是0则该元素所在下标位置不动,如果是1则将该元素放置原位置扩容后的对应位置(假如原先容量为16,元素位置在数组下标14的位置,则扩容后容量为32,该元素移动到数组下标30的位置(即原索引+原容量位置)。

为什么看hashmap源码,你觉得看了后对你有什么好处

  • 比较喜欢探究(其实是近期面试才认真看的...)。知道了计算机位运算速度会比其它数学运算快;学习了它的思想对我思考问题方式有提升(能吹多少尽量吹多少)扩容是一个费性能的事,如果知道集合中大致会存多少元素最好给它一个初始容量
posted @ 2020-08-21 14:17  MrOldx  阅读(256)  评论(0编辑  收藏  举报