JAVA基础-HashMap怎么实现的?

HashMap可谓是面试中的重中之重,基本上每家面试公司都会问一下。

常见面试题总结在文章后部分~!

介绍

HashMap是怎么实现的?

  1. JDK1.7的HashMap是采用数组+链表实现的
  2. JDK1.8之后HashMap是采用的数组+链表+红黑树实现的

HashMap的主干就是一个数组,假设我们有三个键值对 dnf:1,cf:2,lol:3。每次放入HashMap中时候都会根据key.hash % table.length(对象的hashcode进行操作后对数组的长度取余)确定这个键值应该放到数组的那个位置

1 = indexFor(dnf),我们将此键值对放到数组下标为1的位置

3 = indexFor(cf)

1 = indexFor(lol) 这时候我们发现下标为1的位置已经存在数据,我们把lol:3放到数组下标为1的位置,将原来dnf:1采用链表形式放到lol键值对下面

jdk1.7是头插法(从链表头部插入相同hascode的数据) jdk1.8是尾插法(从链表尾部插入相同hascode的数据)

在获取key为dnf的键值对时候,1=hash(dnf),得到这个键值对在数组下标为1的位置,dnf和lol不相等,和下一个元素进行对比,相等返回。set和get的过程大致就如此。先定位到槽的位置(数组中的位置),在遍历链表找到相同元素。

由上面图可以看出,HashMap在发生hash冲突时候采用的链地址法,解决hash冲突并不只有这种方法常见的如下

  1. 开放定址法
  2. 链地址法
  3. 再哈希法
  4. 公共溢出区域法

jdk1.7源码

几个重要属性

  1. DEFAULT_INITIAL_CAPACITY (map初始大小 默认 16)
  2. MAXIMUM_CAPACITY (最大容量,如果指定最大容量使用)
  3. DEFAULT_LOAD_FACTOR (负载因子 默认0.75)
  4. threshold (map是否扩容的决定性因素)

threshold = caopacity * loadFactor,即扩容的阈值=数组长度 * 负载因子,如果HashMap中放入16个元素,负载因子为0.75,则扩容阈值为16*0.75=12

负载因子越小,容易扩容,浪费空间,但是查询效率高
负载因子越大,不容易扩容,堆空间利用更加充分,查找效率低(链表拉长)

存储数据的静态内部类,数组+链表,这里数组指的就是Entry数组

构造函数

其他都是在此基础上的扩展,主要就是设置初始容量和负载因子

put方法的执行过程

scss 复制代码
key为null直接放到table[0]处,对key的hashCode()做hash运算,计算index;
如果节点已存在数据,就替换old value(保证key的唯一性),并返回old value;
如果达到扩容的阈值(超过threshold),并且发生碰撞们就要resize扩容;
将元素放到bucket的首位,即头插法;
  1. 为null时,HashMap还没有创建这个数组,有可能用的是默认16的初始值,也可能自定义长度,这时候需要把数组长度变为2的最小倍数,并且这个2的倍数大于等于初始容量。如下图:

  2. 如key为null时候,则将至放到table[0]这个链上

  3. 找到应该放在数组的位置,h&(length-1)这个方式可以理解为hash值对数组长度取余

  4. 添加元素

  5. 将新的元素放到table的第一位,并且将其他元素跟在第一个元素后

  6. 容量超过阈值并且发生碰撞,开始扩容

  7. transfer 重新计算元素在新的数组中的位置,并进行复制处理

transfer有两个需要注意的地方

css 复制代码
原来在oldTable[i]位置的元素,会被放到newTable[i]或者newTable[i+oldTable.length]的位置
链表在复制的时候会反转

get方法的执行过程

  1. key为null直接从table[0]处取值,对key的hashCode()做hash运算,计算index
  2. 通过key.equals(k)去查找对应的Entry,返回value
  1. 从table[0]取值,key为null

  2. key不为null时候

jdk1.8源码

jdk1.8存取key对null的数据没有进行特殊判断,而是通过将hash值返回为0的将其放到table[0]处

put执行过程

sql 复制代码
对key的hashcode()高16位和低16位进行异或运算求出具体的hash值
如果table数组没有初始化,则初始化table数组长度为16
根据hash值计算index,如果没有碰撞则直接放到bucket里面(bucket可为链表或者红黑树)
如果碰撞导致链表过长,则把链表转为红黑树
如果key一簇按在,用new value替换old value
如果超过扩容阈值则进行扩容,threshold = caopacity * loadFactor

移动的过程和jdk1.7相比变化较大

jdk1.8和jdk1.7重新获取元素值在新的数组中所处位置的算法发生变化(实际效果还一样)

scss 复制代码
jdk1.7, hash&(length-1)
jdk1.8, 判断原来的hash值要新增bit位是0还是1.假如是0,放到newTable[i],否则放到newTbale[i=oldTable.length]

get的执行过程

scss 复制代码
对key的hashcode()高16位和低16位进行异或运算求出具体的hash值
如在bucket里的第一个节点直接命中则直接返回
如果有冲突,通过key.equals(k)去找相对应的Node,并返回value。在树中查找的效率为O(logn),在链表中查找的效率为O(N)

常见面试题

1.HashMap,HashTable,ConcurrentHashMap之间的区别

主要问题点为 是否key允许为null,线程是否安全

2.HashMap在什么条件下扩容

jdk1.7

超过扩容阈值
发生碰撞

jdk1.8

超过扩容阈值

3.HashMap的大小为何是2的N次方

简单说为了降低碰撞的概率,增加查询检索效率

  1. 为了通过hash值确定元素在数组中位置,我们需要进行hash%length操作,当%操作时比较消耗时间,所以优化为hash &(length - 1)

  2. 当length为2的n次方时候, hash&(length-1) = hash%length

  3. 我们假设数组长度为15和16,hash码为8和9

    如图可以看出数组长度为15时候,hash码为8和9的元素被放到数组中统一位置形成链表,降低了查询效率,当hash码和15-1(1110)进行&时候,最后一位永远是0,这样 0001,0011,0101,1001,1011,0111,1101这些位置永远不会被放入元素,导致的结果就是

    空间浪费大
    增加了碰撞几率,减慢查询效率
    

    当数组长度为2的n次方时候,2的n次方-1的所有位都是1,如8-1=7 即111,那么进行低位&运算时候,值总与原来的hash值相同,降低的碰撞概率

4.jdk1.8发生了那些变化?

  1. 由数组+链表改为了数组+链表+红黑树,当链表长度超过8时候,链表变为红黑树。

    scss 复制代码
     为什么要这样改?我们知道链表的查询效率为O(n),而红黑树的查询效率为O(length),查询效率变高了。
     为什么不直接用红黑树?因为红黑树查找效率高,但是插入效率低,如果从一开始直接使用红黑树并不合适。从概率学角度官方选择了一个临界点8
  2. 优化了hash算法

  3. 计算元素在数组中的位置算法发生了变化,新算法通过新增位判断oldTable[i]应该放到newTable[i]还是放到newTable[i+oldTable.length]

  4. 头插法改为尾插法,扩容时候链表不发生倒置(避免形成死循环)

5.Hash在高并发下会发生什么问题

  1. 多线程下扩容,会让链表形成环,从而导致死循环
  2. 多线程put可能会导致元素丢失

jdk1.8中死循环问题已经解决,但是元素丢失问题还存在

6.怎样避免HashMap在高并发下的问题?

  1. 使用ConcurrentHashMap
  2. 使用Collection.synchronizedMap(hashMap) 包装成同步集合
相关推荐
飞升不如收破烂~4 分钟前
Redis的String类型和Java中的String类在底层数据结构上有一些异同点
java·数据结构·redis
苹果酱05678 分钟前
windows安装redis, 修改自启动的redis服务的密码
java·开发语言·spring boot·mysql·中间件
feilieren12 分钟前
信创改造 - TongRDS 替换 Redis
java·spring boot·后端
Allen Bright14 分钟前
Jedis连接池的操作
java·redis
庞传奇1 小时前
【LC】560. 和为 K 的子数组
java·算法·leetcode
@糊糊涂涂1 小时前
MAC借助终端上传jar包到云服务器
java·服务器·macos·jar
东方巴黎~Sunsiny1 小时前
给定数字 [3, 30, 34, 5, 9] 拼接成的最大数字,使用java实现
java·开发语言
daiyang123...2 小时前
Java 复习 【知识改变命运】第九章
java·开发语言·算法
Erosion20202 小时前
RMI原理及常见反序列化攻击手法
java·反序列化·java sec
AskHarries2 小时前
Spring Cloud Consul实现选举机制
java·后端·spring cloud·consul