文章目录

  • 1.HashMap的底层数据结构?
  • 2.为啥需要链表,链表又是怎么样子的呢?
  • 3.新的Entry节点在插入链表的时候,是怎么插入的么?
  • 4.Java7中的HashMap和Java8中的HashMap的区别?
  • 5.java8之后为啥改为尾部插入呢?
  • 6.为啥会线程不安全?
  • 7.有什么线程安全的类代替么?
  • 8.那你知道ConcurrentHashMap的分段锁的实现原理吗?
  • 9.默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?
  • 10.HashMap的扩容方式?负载因子是多少?为什是这么多?
  • 11.HashMap是怎么处理hash碰撞的?
  • 12.提到HashMap的初始化,那HashMap怎么设定初始容量大小的吗?
  • 13.你了解HashMap在JDK8中的数据插入原理吗?
  • 14.HashMap的哈希函数怎么设计的?
  • 15.为什么这么设计Hash函数?
  • 16.为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?
  • 17.JDK8对hash函数做了优化,JDK8还有别的优化吗?
  • 18.你分别跟我讲讲为什么要做这几点优化?
  • 19.HashMap内部节点是有序的吗?
  • 20.那有没有有序的Map?
  • 21.跟我讲讲LinkedHashMap怎么实现有序的?
1.HashMap的底层数据结构?

HashMap是我们非常常用的数据结构,由数组和链表组合构成的数据结构。数组里面每个地方都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node。因为他本身所有的位置都为null,在put插入的时候会根据key的hash去计算一个index值。

Java 7 中的HashMap的底层是一个数组+链表的设计,每个hash值的第一个值放在数组里,之后经过hash运算得到相同的hash值锁定数组下标,数组中的每一个元素都是一个单向链表(碰撞,列表)

2.为啥需要链表,链表又是怎么样子的呢?

我们都知道数组长度是有限的,在有限的长度里面我们使用哈希,哈希本身是通过Hash函数计算出来的,结果就存在概率性,两个不同的key,hash有一定的概率会一样,那就形成了链表。每一个节点都会保存自身的hash、key、value、以及下个节点。

3.新的Entry节点在插入链表的时候,是怎么插入的么?

java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率。但是,在java8之后,都用尾部插入了

4.Java7中的HashMap和Java8中的HashMap的区别?

Java 7 中的HashMap的底层是一个数组+链表的设计,每个hash值的第一个值放在数组里,之后经过hash运算得到相同的hash值锁定数组下标,数组中的每一个元素都是一个单向链表(碰撞,列表)

5.java8之后为啥改为尾部插入呢?

在插入时采用尾插法(1.7是头插法),在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

什么是头插法和尾插法

头插法带来的环状

6.为啥会线程不安全?

通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

7.有什么线程安全的类代替么?

Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。

HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大。

Collections.synchronizedMap是使用 Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现。

ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。

我们一般都会使用HashTable或者ConcurrentHashMap,但是因为前者的并发度的原因基本上没啥使用场景了,所以存在线程不安全的场景我们都使用的是ConcurrentHashMap。

HashTable我看过他的源码,很简单粗暴,直接在方法上锁,并发度很低,最多同时允许一个线程访问,ConcurrentHashMap就好很多了,1.7和1.8有较大的不同,不过并发度都比前者好太多了。

8.那你知道ConcurrentHashMap的分段锁的实现原理吗?

ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现 赋值操作,多线程操作只会锁住当前操作索引的节点。 如下图,线程A锁住A节点所在链表,线程B锁住B节点所在链表,操作互不干涉。

9.默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

是16,因为在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。 只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。 这是为了实现均匀分布。

10.HashMap的扩容方式?负载因子是多少?为什是这么多?

capacity:当前数组容量,始终保持 $2^n$,可以扩容,扩容后数组大小为当前的 2 倍。

loadFactor:负载因子,默认为 0.75。

threshold:扩容的阈值,等于 capacity * loadFactor。

Java 8 中的HashMap的底层是数组+链表+红黑树的方式实现。

改进的是在数据量较大的情况下(Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树)单向链表的时间复杂是O(N),采用红黑树将时间复杂度降到O(logN)。

那么, 如果我们在删除容器中的元素的时候,删到多少才使得红黑树的存储结构转为链表呢?答案是6

也就是说,在JDK8之后,创建HashMap对象=>添加数组中同一个位置元素超过8个=>该位置链表转为红黑树=>删除数组中同一个位置元素少于6个=>该位置红黑树转为列表。

最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树), 否则,若桶内元素太多时,则直接扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD。

也就是说,当数组中某个桶中的元素大于8,小于64时,使用容量进行两倍扩容(其实就是用扩容代替链表转红黑树操作)。当数组中某个桶中的元素大于8且大于64时,链表转红黑树操作。

hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换

hashMap的红黑树不一定小于等于6的时候就会转换为链表,而是只有在resize的时候才会根据 UNTREEIFY_THRESHOLD(6) 进行转换

11.HashMap是怎么处理hash碰撞的?

Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。

在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,我们通过put和get方法存取对象。

当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。

HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。

键对象的equals()来找到键值对。

12.提到HashMap的初始化,那HashMap怎么设定初始容量大小的吗?

一般如果new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为大于k的2的整数次方,例如如果传10,大小为16

13.你了解HashMap在JDK8中的数据插入原理吗?

14.HashMap的哈希函数怎么设计的?

hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。

15.为什么这么设计Hash函数?

这个也叫扰动函数,这么设计有二点原因:

一定要尽可能降低hash碰撞,越分散越好;

算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

16.为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?

因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。

int值范围为-2147483648~2147483647, 前后加起来大概40亿的映射空间。

只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。

但问题是一个40亿长度的数组,内存是放不下的。

因为如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。

bucketIndex = indexFor(hash, table.length);static int indexFor(int h, int length) {return h & (length-1);
}

便说一下,这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

HashMap与运算

HashMap碰撞

17.JDK8对hash函数做了优化,JDK8还有别的优化吗?

1. 数组+链表改成了数组+链表或红黑树;
2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的 后继节点,1.8遍历链表,将元素放置到链表的最后;
3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

18.你分别跟我讲讲为什么要做这几点优化?

防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);

因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如上图所示

扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?

这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,怎么理解呢?

扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。

因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)
HashMap中的与运算

19.HashMap内部节点是有序的吗?

是无序的,根据hash值随机插入

20.那有没有有序的Map?

LinkedHashMap 和 TreeMap

21.跟我讲讲LinkedHashMap怎么实现有序的?

LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。

【Java自顶向下】HashMap面试题(2021最新版)相关推荐

  1. 非常详细的Redis面试题(2021最新版)

    文章目录 概述 什么是Redis Redis有哪些优缺点 为什么要用 Redis /为什么要用缓存 为什么要用 Redis 而不用 map/guava 做缓存? Redis为什么这么快 数据类型 Re ...

  2. java技术栈xmind,2021最新版!

    前言 互联网时代,瞬息万变.一个小小的走错,就有可能落后于别人.我们没办法去预测任何行业.任何职业未来十年会怎么样,因为未来谁都不能确定.只能说只要有互联网存在,程序员依然是个高薪热门行业.只要跟随着 ...

  3. Java多线程常见面试题及答案汇总1000道(春招+秋招+社招)

    Java多线程面试题以及答案整理[最新版]Java多线程高级面试题大全(2021版),发现网上很多Java多线程面试题都没有答案,所以花了很长时间搜集,本套Java多线程面试题大全,汇总了大量经典的J ...

  4. 【金三银四】Java集合面试题(2021最新版)

    目录 前言 一.集合容器概述 1. 什么是集合 2. 集合的特点 3. 集合和数组的区别 4. 使用集合框架的好处 5. 常用的集合类有哪些? 6. List,Set,Map三者的区别? 7. 集合框 ...

  5. Java基础知识面试题(2021最新版)

    转载自:https://thinkwon.blog.csdn.net/article/details/104390612 文章目录 Java概述 何为编程 什么是Java jdk1.5之后的三大版本 ...

  6. 【金三银四】Java中间件面试题(2021最新版)

    目录 前言 Zookeeper 1. ZooKeeper 是什么? 2. ZooKeeper 提供了什么? 3.Zookeeper 文件系统 4. ZAB 协议? 5. 四种类型的数据节点 Znode ...

  7. Java 虚拟机(JVM)面试题(2021最新版)

    点击下方公众号「关注」和「星标」 回复"1024"获取独家整理的学习资料! Java内存区域 说一下 JVM 的主要组成部分及其作用? JVM包含两个子系统和两个组件,两个子系统为 ...

  8. 【2021最新版】Java多线程并发面试题总结(108道题含答案解析)

    文章目录 JAVA并发知识库 1.Java中实现多线程有几种方法? 2.继承Thread类 3.实现Runnable接口. 4.ExecutorService.Callable.Future有返回值线 ...

  9. Java集合详解9:2021年的hashmap面试题怎么考?涵盖性能优化、扩容、并发问题等核心考点

    摘要 HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例 ...

最新文章

  1. module ‘imgaug.augmenters‘ has no attribute ‘Resize‘
  2. django 快速实现session的操作
  3. C#中类的继承问题04
  4. 【数字逻辑设计】卡诺图
  5. 我如何开始使用Linux
  6. php 递归遍历文件夹,php递归遍历目录 | 学步园
  7. python线程安全_线程,线程安全与python的GIL锁
  8. 一个资源管理系统的设计--基于cgroup机制
  9. c语言栈的实现以及操作_C++语言实现顺序栈
  10. WMI 错误 10的解决
  11. python做可视化_python可视化界面怎么做?
  12. 职称计算机 河南,2017年河南职称计算机报名入口
  13. django 1.11 文档
  14. 概率统计Python计算:双正态总体均值差的单侧区间估计
  15. fastadmin 邮件配置
  16. Roy Li:币市里,我相信社区的力量和草根的崛起
  17. 容联与腾讯云牵手!这一合作背后有何深意?
  18. 安装smartPPT,电脑显示系统桌面就卡住……
  19. wifidog认证接口
  20. 谓词推进 PUSHED PREDICATE

热门文章

  1. 我的第一份vPlan衍变路线
  2. (三) 类图和对象图
  3. STM32PB2(BOOT1)使用注意
  4. 跨链(8)Cosmos之“跨链交互协议IBC”前言
  5. 深入了解Kubernetes CRD开发工具kubebuilder
  6. 椭圆曲线加密算法ECC
  7. [reference]-armv8汇编学习-书籍推荐
  8. optee的RPC设计(模型)详解
  9. 增量式爬虫与分布式爬虫
  10. arm 跳转指令跳转范围