hashMap

视频

1、引出问题

在这里插入图片描述

  • null是什么意思?
  • map.size为什么是3?
    在这里插入图片描述

数组大于64,或者链表大于8,才会变为红黑树

  • 1.主数组长度?
  • 2.主数组类型?
  • 3.哈希码怎么计算的?
  • 4.公式是什么?
  • 5.节点在主数组中的位置?
  • 6.节点的数据类型?

1.1 HashMap的底层数据结构?

在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。“头插法”

在JDK1.8 中,由“数组+链表+红黑树”组成。尾插法。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  • 当链表超过 8 且数据总量超过 64 才会转红黑树。
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

JDK1.8 后采用数组+链表+红黑树的数据结构。通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算,来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。

1.2 为什么引入红黑树?

JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当

HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。

针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。

而引入红黑树,就是为了提高查找效率的;因为红黑树是一种自平衡的二叉查找树;

这是HashMap解决查询效率低下的一种方式,还有一种方式是通过负载因子控制的扩容,每次Hash碰撞达到负载因子的时候,就会触发扩容,扩容为之前的两倍,从而大幅提高查找的效率;理论上扩容最高可以提升2倍的效率,但实际很难出现;

1.3 为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。

当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。

当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

1.4 jdk1.8的HashMap中的链表达到多少个时会生成红黑树?

HashMap用链地址法解决hash冲突,则当链表里的长度太长就会严重影响HashMap的性能。于是在jdk1.8里,对数据结构做了进一步优化,引入了红黑树,当链表长度大于8的时候,链表就会转成红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

1.5 HashMap 的put方法流程?

简要流程如下:

  • 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
  • 如果数组是空的,则调用 resize 进行初始化;
  • 如果没有哈希冲突直接放在对应的数组下标里;
  • 如果冲突了,且 key 已经存在,就覆盖掉 value;
  • 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
  • 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。

1.6 为什么HashMap内部的散列表数组的长度一定是2的次方数?

1.为了数据的均匀分布,减少哈希碰撞。
因为(2的次幂数 - 1)的二进制形式表示都是1,这样在和经过异或运算的h进行按位与运算的时候才可以最多地保留其特性,减少产生哈希碰撞的概率,让数组空间均匀分配。

2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算,得到的肯定是2的幂次数,并且是离那个数最近的数字。

1.7 哈希冲突及解决方法

如果两个不同对象的hashCode相同,这种现象称为hash冲突。有以下的方式可以解决哈希冲突:

  • 开放定址法 :就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

  • 链地址法: 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

  • 再哈希法 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。

  • 建立公共溢出区 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。

而HashMap就是采用链地址法进行解决hash冲突的。

1.8 HashMap初始值的大小和负载因子的大小?

hashMap初始长度就是16,负载因子是0.75。
HashMap所容纳的最大数据量为:长度*负载因子。即当长度达到这个值的时候就会发生扩容。

loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。

1.9 HashMap扩容机制

扩容(resize)就是重新计算容量,

向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

底层是resize方法中的transfer方法,将原有的Entry数组的元素拷贝到新的Entry数组里,扩容都是以2的N次幂进行扩容 一般是2倍。

1.10 HashMap线程安全问题 HashTable ConcurrentHashMap

HashMap是线程不安全的,多个线程同时写HashMap可能会导致数据的不一致。

如果需要满足线程安全可以用ConcurrentHashMap,还有一个HashTable

  • ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成,采用segment分段锁来保证线程安全。
  • HashTable是继承自Dictionary类,HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下,HashTable的效率非常低下,
  • HashTable无论key或value都不能为null,HashMap只能允许一个key为null,可以运行多个value为null。而且HashTable是线程安全的,HashMap是线程不安全的。

1.11 HashMap链表成环

由于HashMap线程不安全的,至于为何不安全,什么时候会出现问题,这里来讨论一下:

当有多个线程共同操作hashMap的put方法时,这个时候hashMap容量不够了,两个线程都去扩容执行resize方法,在这个时候cpu切换资源的话,会造成链表成环问题,死循环问题。

1.12 谈一下hashMap中get是如何实现的?

对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。

1.13 HashMap和HashTable的区别

相同点:都是存储key-value键值对的

不同点:

  • HashMap允许Key-value为null,hashTable不允许;
  • hashMap没有考虑同步,是线程不安全的。hashTable是线程安全的,给api套上了一层synchronized修饰;
  • HashMap继承于AbstractMap类,hashTable继承与Dictionary类。
  • 迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。
  • 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"。Hashtable默认的容量大小是11;增加容量时,每次将容量变为"原始容量x2 + 1";
  • 添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。

1.14 平时在使用HashMap时一般使用什么类型的元素作为Key?

选择Integer,String这种不可变的类型,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的,

2、源码解析

2.1 new HashMap<>(10)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 6.节点的数据类型?
    • node类型

2.2 map.put(“通话”,10)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.3 map.put(“通话”,30)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.4 map.put(“重地”,40)


在这里插入图片描述

在这里插入图片描述

2.5 为什么主数组长度是2的n次幂

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值