HashMap常见面试题

发布时间 2023-08-16 16:48:06作者: WeChat——E云

HashMap的底层数据结构?
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用。

HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的 长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值8时,将链表转化为红黑树,以减少搜索时间。

HashMap的存取原理?
1)存方法:put(Object key, Object value)

执行流程如下:

对 key 进行 hash 操作,计算存储 index; 判断是否有哈希碰撞,如果没碰撞直接放到哈希桶里,如果有碰撞则以链表的形式存储; 判断已有元素的类型,决定是追加树还是追加链表,当链表大于等于 8 时,把链表转换成红黑树; 如果节点已经存在就替换旧值; 判断是否超过阀值,如果超过就要扩容。
2)取方法:get(Object key)

执行流程如下:

首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点; 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对。
Java7和Java8中HashMap的区别
HashMap 在 JDK 7 和 JDK 8 的主要区别如下。

存储结构:JDK 7 使用的是数组 + 链表;JDK 8 使用的是数组 + 链表 + 红黑树。 存放数据的规则:JDK 7 无冲突时,存放数组;冲突时,存放链表;JDK 8 在没有冲突的情况下直接存放数组,有冲突时,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。 插入数据方式:JDK 7 使用的是头插法(先将原位置的数据移到后 1 位,再插入数据到该位置);JDK 8 使用的是尾插法(直接插入到链表尾部/红黑树)。
为什么HashMap线程不安全
多线程put的时候导致数据不一致 可能会因为扩容引起死循环
HashMap 在并发场景中可能出现死循环的问题,这是因为 HashMap 在扩容的时候会对链表进行一次倒序处理,假设两个线程同时执行扩容操作,第一个线程正在执行 B→A 的时候,第二个线程又执行了 A→B ,这个时候就会出现 B→A→B 的问题,造成死循环。

有什么线程安全的类代替吗
Hashtable或ConcurrentHashMap

默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?
默认初始大小是16 这样是为了位运算的方便,位与运算比算数计算的效率高了很多,之所以选择16,是为了服务将Key映射到index的算法。 为了存取高效,减少碰撞,把数据分配均匀
HashMap的扩容方式?负载因子是多少?为什么是这么多?
当hashmap中的元素个数超过负载因子×当前大小时,就会进行数组扩容,负载因子的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过12的时候,就把数组的大小扩展为32,即扩大一倍,然后重新计算每个元素在数组中的位置。

HashMap的主要参数都有哪些?
HashMap 有两个重要的参数:容量(Capacity)和负载因子(LoadFactor)。

容量(Capacity):是指 HashMap 中桶的数量,默认的初始值为 16。 负载因子(LoadFactor):也被称为装载因子,LoadFactor 是用来判定 HashMap 是否扩容的依据,默认值为 0.75f,装载因子的计算公式 = HashMap 存放的 KV 总和(size)/ Capacity。
HashMap是怎么处理hash碰撞的?
Tips:当输入两个不同值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)
HashMap 使用链表和红黑树来解决哈希冲突

HashMap 和 Hashtable 有什么区别?
HashMap 和 Hashtable 区别如下:

Hashtable 使用了 synchronized 关键字来保障线程安全,而 HashMap 是非线程安全的; HashMap 允许 K/V 都为 null,而 Hashtable K/V 都不允许 null; 初始容量大小和每次扩充容量大小的不同。创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来 的2倍。 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
为什么重写 equals() 时一定要重写 hashCode()?
因为 Java 规定,如果两个对象 equals 比较相等(结果为 true),那么调用 hashCode 也必须相等。如果重写了 equals() 但没有重写 hashCode(),就会与规定相违背。

HashMap 和HashSet区别
ConcurrentHashMap线程安全的具体实现方式/底层具体实现
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数 据。

HashMap的底层数据结构? JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用。 HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的 长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值8时,将链表转化为红黑树,以减少搜索时间。 HashMap的存取原理? 1)存方法:put(Object key, Object value) 执行流程如下: 对 key 进行 hash 操作,计算存储 index; 判断是否有哈希碰撞,如果没碰撞直接放到哈希桶里,如果有碰撞则以链表的形式存储; 判断已有元素的类型,决定是追加树还是追加链表,当链表大于等于 8 时,把链表转换成红黑树; 如果节点已经存在就替换旧值; 判断是否超过阀值,如果超过就要扩容。 2)取方法:get(Object key) 执行流程如下: 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点; 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对。 Java7和Java8中HashMap的区别 HashMap 在 JDK 7 和 JDK 8 的主要区别如下。 存储结构:JDK 7 使用的是数组 + 链表;JDK 8 使用的是数组 + 链表 + 红黑树。 存放数据的规则:JDK 7 无冲突时,存放数组;冲突时,存放链表;JDK 8 在没有冲突的情况下直接存放数组,有冲突时,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。 插入数据方式:JDK 7 使用的是头插法(先将原位置的数据移到后 1 位,再插入数据到该位置);JDK 8 使用的是尾插法(直接插入到链表尾部/红黑树)。 为什么HashMap线程不安全 多线程put的时候导致数据不一致 可能会因为扩容引起死循环 HashMap 在并发场景中可能出现死循环的问题,这是因为 HashMap 在扩容的时候会对链表进行一次倒序处理,假设两个线程同时执行扩容操作,第一个线程正在执行 B→A 的时候,第二个线程又执行了 A→B ,这个时候就会出现 B→A→B 的问题,造成死循环。 有什么线程安全的类代替吗 Hashtable或ConcurrentHashMap 默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂? 默认初始大小是16 这样是为了位运算的方便,位与运算比算数计算的效率高了很多,之所以选择16,是为了服务将Key映射到index的算法。 为了存取高效,减少碰撞,把数据分配均匀 HashMap的扩容方式?负载因子是多少?为什么是这么多? 当hashmap中的元素个数超过负载因子×当前大小时,就会进行数组扩容,负载因子的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过12的时候,就把数组的大小扩展为32,即扩大一倍,然后重新计算每个元素在数组中的位置。 HashMap的主要参数都有哪些? HashMap 有两个重要的参数:容量(Capacity)和负载因子(LoadFactor)。 容量(Capacity):是指 HashMap 中桶的数量,默认的初始值为 16。 负载因子(LoadFactor):也被称为装载因子,LoadFactor 是用来判定 HashMap 是否扩容的依据,默认值为 0.75f,装载因子的计算公式 = HashMap 存放的 KV 总和(size)/ Capacity。 HashMap是怎么处理hash碰撞的? Tips:当输入两个不同值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞) HashMap 使用链表和红黑树来解决哈希冲突 HashMap 和 Hashtable 有什么区别? HashMap 和 Hashtable 区别如下: Hashtable 使用了 synchronized 关键字来保障线程安全,而 HashMap 是非线程安全的; HashMap 允许 K/V 都为 null,而 Hashtable K/V 都不允许 null; 初始容量大小和每次扩充容量大小的不同。创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来 的2倍。 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 为什么重写 equals() 时一定要重写 hashCode()? 因为 Java 规定,如果两个对象 equals 比较相等(结果为 true),那么调用 hashCode 也必须相等。如果重写了 equals() 但没有重写 hashCode(),就会与规定相违背。 HashMap 和HashSet区别 ConcurrentHashMap线程安全的具体实现方式/底层具体实现 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数 据。