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) 包装成同步集合
相关推荐
Algorithm157622 分钟前
谈谈接口和抽象类有什么区别?
java·开发语言
细心的莽夫1 小时前
SpringCloud 微服务复习笔记
java·spring boot·笔记·后端·spring·spring cloud·微服务
264玫瑰资源库2 小时前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏
pwzs2 小时前
Java 中 String 转 Integer 的方法与底层原理详解
java·后端·基础
东阳马生架构3 小时前
Nacos简介—2.Nacos的原理简介
java
普if加的帕3 小时前
java Springboot使用扣子Coze实现实时音频对话智能客服
java·开发语言·人工智能·spring boot·实时音视频·智能客服
爱喝一杯白开水3 小时前
SpringMVC从入门到上手-全面讲解SpringMVC的使用.
java·spring·springmvc
王景程3 小时前
如何测试短信接口
java·服务器·前端
zhang23839061544 小时前
IDEA add gitlab account 提示
java·gitlab·intellij-idea·idea
牛马baby4 小时前
Java高频面试之并发编程-07
java·开发语言·面试