avatar

HashMap在JDK1.7和1.8的区别

HashMap

JDK1.7中,底层使用数组+链表
JDK1.8中,底层使用数组+链表+红黑树

共同点:

容量(capacity):HashMap中数组的长度

  1. 容量范围:必须是2的幂 & <最大容量(2的30次方)
  2. 初始容量 = 哈希表创建时的容量
  3. 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  4. 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换)
    static final int MAXIMUM_CAPACITY = 1 << 30;

加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度

  1. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
  2. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
  3. 默认加载因子 = 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

扩容机制

  1. 扩容时resize(2 * table.length),扩容到原数组长度的2倍。

  2. 若key == null,则hash(key) = 0,则将该键-值 存放到数组table 中的第1个位置,即table [0]

异同点

JDK1.7中

  • 使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。

  • 在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表

  • ,也就是说时间复杂度在最差情况下会退化到O(n)

  • 发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,就元素移动到链表中。

  • 在扩容resize()过程中,采用单链表的头插法,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 。

  • HashMap线程不安全的一个重要原因就是:多线程下resize()容易出现死循环,此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出
    现 环形链表,从而在get数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。

JDK1.8中

  • 使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构

  • 如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。

  • 如果同一个格子里的key不超过8个,使用链表结构存储。

  • 如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。

  • 那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销

  • 也就是说put/get的操作的时间复杂度最差只有O(log n)

  • 由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入(尾插法),所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况 ,但jdk1.8仍是线程不安全的,因为没有加同步锁保护。

  • 发生hash冲突后,会优先判断该节点的数据结构式是红黑树还是链表,如果是红黑树,则在红黑树中插入数据,如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8,如果大于8要转成红黑树,另一还要判断数组长度是否超过阀值

文章作者: Hobo
文章链接: https://hobo-clh.github.io/2020/05/04/HashMap%E5%9C%A8JDK1-7%E5%92%8C1-8%E7%9A%84%E5%8C%BA%E5%88%AB/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hobo's blog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论