Hashmap经典高频问题,让面试成为你的主场

hashmap底层原理

jdk1.7和1.8有什么区别

1.7之前:数组+链表

1.8之后:数组+链表+红黑树

数组:对于查询效率较高,但是对于删除增加效率低

链表:对于删除增加效率高,但是对于查询效率低

计算Hash值

HashTable直接使用对象的hashCode。 HashMap的Hash值:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

三目运算 式子合理取?不合理取:

位运算符 ^ 相同0 不同1

HashMap和Hashtable的区别

继承:

Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

锁:

Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。

方法:

HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。 Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

是否可以为null:

Hashtable中,key和value都不允许出现null值。 HashMap中,null可以作为键,这样的键只有一个。 Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

容量:

HashTable在不指定容量的情况下的默认容量为11,而HashMap为16, Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。 Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

为什么 ConcurrentHashMap 比 HashTable 效率要高?

HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;

ConcurrentHashMap

JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment/segmant/,包含多个 HashEntry。

JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry<K,V>)。锁粒度降低了。

HashMap 的 table 的容量如何确loadFactor 是什么?该容量如何变化?这种变化会带来什么问题

table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;

loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75,比如 table 数组大小为 16,装载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时, table就需要动态扩容;

扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold)

如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。

Hashmap 与 ConcurrentHashMap

hashmap本质是数组+链表 根据key去获取hash值 然后计算出对应的下标,如果有多个key对应同一个下标,就用链表的形式存储

ConcurrentHashMap在hashmap的基础上 ConcurrentHashMap将数据分成了多个数据段(segment 默认是16) 主要是对segment去加锁

hashmap的键值允许null值,但是ConcurrentHashMap不允许

在jdk1.7 ConcurrentHashMap是由segment数组和hashentry数组结构组成

在jdk1.8 ConcurrentHashMap放弃了segment用node+cas+synchronize保证

ConcurrentHashMap是线程安全, hashmap线程不安全

负载因子为什么是0.75.为什么到8的时候转红黑树

如果你看过源代码,你会发现在初始条件下,HashMap在时间和空间两者间折中选择了0.75。

/  
  The load factor used when none specified in constructor.
 /
​
static final float DEFAULT_LOAD_FACTOR = 0.75f;

但是为什么一定是0.75?而不是0.8,0.6,这里有一个非常重要的概念:泊松分布。

相信大家都学过概率论,对这个大名鼎鼎的定律感觉应该是既熟悉又陌生。本篇文章的重点不是为大家普及概率论知识,这里就简单介绍下。

泊松分布是最重要的离散分布之一,它多出现在当X表示在一定的时间或空间内出现的事件个数这种场合。

举个简单的例子,假如你一个老板,新开张了一家酒店,这个时候应该如何准备一天所用的食材呢?

准备的太多,最后卖不掉这么多菜只能浪费扔掉;准备不够,又接不了生意。但是你有很多同行和朋友,他们会告诉你很多经验。

比如把一天分成几个时间段,上午、下午、晚上每个时间段大概会来多少个客人,每一桌大概会点几个菜。综合下来,就可以大致知道在一天的时间内,估计出需要准备的食材数量。

翻译过来说的是,在理想情况下,使用随机哈希码,节点出现的频率在 hash 桶中遵循泊松分布。

对照桶中元素个数和概率的表,可以看到当用 0.75 作为加载因子时,桶中元素到达 8 个的时候,概率已经变得非常小,因此每个碰撞位置的链表长度超过 8 个是几乎不可能的,因此在链表节点到达 8 时才开始转化为红黑树。

HashMap的线程不安全主要体现在下面两个方面

1、JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况

2、JDK1.8中,当并发执行put的时候会对数据造成覆盖的情况

hash code和eques之间有什么关系

1、equals方法用于比较对象的内容是否相等(覆盖以后)

2、hashcode方法只有在集合中用到

3、当覆盖了equals方法时,比较对象是否相等将通过覆盖后的equals方法进行比较(判断对象的内容是否相等)。

4、将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。

5、将元素放入集合的流程图: 在这里插入图片描述

concurrenthash map底层原理

1

如图,这是ConcurrentHashMap在jdk1.8中的存储结构,它是由数组 ,单项链表,红黑树来构成,当我们去初始化一个ConcurrentHashMap实例的时候,默认会初始化一个长度等于16的数组,由于ConcurrentHashMap它的核心仍然是Hash表,所以必然会存在Hash冲突的问题,所以ConcurrentHashMap采用链式寻址的方式,来解决Hash表的冲突,当Hash冲突比较多的时候,会造成链表长度较长的问题,这种会使得ConcurrentHashMap中的一个数组元素的查询复杂度会增加,所以在jdk1.8里面,引入了红黑树的机制,当数组长度大于64并且链表长度大于等于8的时候,单向链表会转化成红黑树,另外随着ConcurrentHashMap的一个动态扩容,一旦链表的长度小于8,红黑树会退化成单向链表

2. ConcurrentHashMap的基本功能

2

ConcurrentHashMap本质上是一个HashMap,因此功能和HashMap是一样的,但是ConcurrentHashMap在HashMap的基础上提供了并发安全的一个实现。并发安全的主要实现主要通过对于Node节点去加锁,来保证数据更新的安全性

3. ConcurrentHashMap在性能方面的优化

如何在并发性能和数据安全性之间去做好平衡,在很多地方都有类似的设计,比如说cpu的三级缓存,mysql的buffer_poor,Synchronized的一个锁升级等等,ConcurrentHashMap也做了类似一个优化,主要体现在几个方面

  1. 在jdk1.8里面ConcurrentHashMap锁的粒度,是数组中的某一个节点,而在jdk1.7里面。它锁定的是Segment,锁的范围要更大,所以性能上它会更低。

  2. 引入红黑树这样一个机制,去降低了数据查询的时间复杂度,红黑树的时间复杂度实是O(logn) 3 如图,当数组长度不够的时候,ConcurrentHashMap它需要对数组进行扩容,而在扩容时间上,ConcurrentHashMap引入了多线程并发扩容的一个实现,简单来说多个线程对原始数组进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而去整体的提升了扩容过程中的数据迁移的一个效率

  3. ConcurrentHashMap它有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下去实现元素个数的累加,性能是非常低的,所以ConcurrentHashMap这个方面做了两个优化

  • (1)4

如图,当线程竞争不激烈的时候,直接采用CAS的方式,来实现元素个数的一个递增

  • (2) 如果线程竞争化比较激烈的情况下,使用一个数组来维护元素个数,如果要增加总的元素个数的时候,直接从数组中随机选择一个,在通过CAS算法来实现原子递增,它的核心思想是引入数组来实现并发更新的一个负载

重载和重写有什么区别

一、定义上的区别:

1、重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。 2、 重写是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。

二、规则上的不同:

1、重载的规则: ①必须具有不同的参数列表。 ②可以有不同的访问修饰符。 ③可以抛出不同的异常。

2、重写方法的规则: ①参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。 ②返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。 ③访问修饰符的限制一定要大于被重写方法的访问修饰符。 ④重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。

三、类的关系上的区别:

重写是子类和父类之间的关系,是 垂直关系 ;重载是同一个类中方法之间的关系,是 水平关系 。

jdk1.8做了哪些改进

一、改进

1,jdk1.7底层采用entry数组+链表的数据结构,而1.8采用node数组+链表/红黑树的数据结构。

2,jdk1.7的HashMap插入新值时使用头插法,1.8使用尾插法。

使用头插法比较快,但在多线程扩容时会引起倒序和闭环的问题。所以1.8就采用了尾插法。

3,扩容后新表中的索引位置计算方式不同,jdk1.7扩容时是将旧表元素的所有数据重新进行哈希计算,即hashCode & (length-1)。而1.8中扩容时只需将hashCode和老数组长度做与运算判断是0还是1,是0的话索引不变,是1的话索引变为老索引位置+老数组长度。

Java8中HashMap为什么要用红黑树?

在jdk1.8版本后,Java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。

java8中HashMap数据结构 在这里插入图片描述

为什么选择红黑树?

红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树。

有部分图片是别的博主的感觉不错,拿来借用,如有冒犯请联系我

猜你喜欢

转载自blog.csdn.net/Fristm/article/details/125618502
今日推荐