1. HashMap概述:

HashMap是基于哈希表的Map接口的非同步实现(他与Hashtable类似,但Hashtable是线程安全的,所以是同步的实现),此实现提供可选的映射操作,允许使用null值和null键,但他并非有序。

2. HashMap数据结构与实现原理:

在jdk1.7和jdk1.8中,HashMap的数据结构是有所差别的,进行一些优化来解决冲突问题,下面我们就分别从两个版本的角度来分析一下他的改动与区别

(一)jdk1.7版本

在jdk1.7版本的时候采用的是数组+链表的形式,也就是采用了拉链法。

 

将哈希冲突值加入到每个数组的链表中,他的插入采用的是头插法的形式(这种方法最大的弊端就是会使插入值产生环,从而无限循环,后面我们将详细讲解这种方法的弊端操作),在进行hash值计算的时候,jdk1.7则采用的是9次扰动(4次位运算+5次异或运算)的方式进行处理(因为本人目前暂时用的jdk1.8所以无法利用源码进行讲解,望见谅),除此之外在扩容上也有所不同,在jdk1.7中采用的全部按照原来的方式进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)),而在jdk1.8中则采用按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量),下面让我们来详细讲解一下jdk1.8。

(二)jdk1.8版本

而jdk1.8的版本则采用数组+链表+红黑树的方式,如下图:

 

这种方法可以大大优化了哈希冲突的问题,减少了搜索时间,当添加的数目达到阈值的时候可以将链表转换为红黑树的形式,而当红黑树的节点小于6的时候就会从红黑树转化为链表的形式(如果不理解红黑树的话,可以参考我的上一篇文章),而在进行插入值的时候则采用的是尾插法的形式,这种方法解决了成环的情况发生

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

  

上面代码便是对数组进行put操作时,节点的判断以及添加,里面有几个核心的方法让我们一个一个来分析:

resize():

这个方法顾名思义------扩容,他在jdk1.8的时候进行两次调用,第一次是在数组进行初始化的时候对其进行扩容,而第二次则是在数组满的时间进行的扩容,一次是开始,一次则是快结束的时候,让我们点进去来详细查看一下他在内部所作的操作及含义:

 final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //定义老数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; //判断老数组是否为空,来实现初始化扩容操作
int oldThr = threshold; //使初始容量暂时为创建数组时产生的容量,默认为16
int newCap, newThr = 0; //进行初始化赋值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 双重容量,也可以理解为进行扩容
}
else if (oldThr > 0) // 初始容量处于阈值
newCap = oldThr;
else { //零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor; //进行与加载因子的乘积,此时表示在自动扩容之前,数组达到多满的一种度量,当超过这个乘积值的时候才进行扩容,加载因子的默认值为0.75
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

  

从中我们可以看出,扩容方法对数组初始化以及为空都进行了详细的判断,许多人看到这里都会问为什么加载因子的默认值为0.75呢?那么这个问题我们会在接下来的面试典型题中进行简单的解释。让我们回到上面的put方法中继续分析。

hash(key)

这个方法也是我们的重点,他是为了保证充分利用数组的每个位置(下标)并大大解决哈希冲突问题而诞生的,在jdk1.7中我们提到了计算hash的时候进行了9次扰动,而在jdk1.8中我们仅仅使用了2次扰动就进行了hash值的计算,

  static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

  

从上面代码可以看出他是通过key.hashCode()进行高16位和低16位进行了一次异或运算得到的,减少了hash的碰撞问题。
两个主要的方法分析完了,让我们来整体描述一下put的方法整体流程,在这个执行流程中,可以清晰的看出值的插入以及两个核心方法的使用(这里我引用了一张我认为画的比较全的流程图,进行讲解)

 

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
至此我们的讲解完毕,下面来讲述一下面试过程中所遇到的一些面试题。

面试过程中的典型问题

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

因为key.hashCode调用的是key键值类型自带的哈希函数,他返回的是一个int类型的散列值,我们都知道int类型的-2147483648~2147483647**大约有40亿的空间,而在我们的数组中容量仅仅为16个,所以就会造成数组无法承载值的情况,因为必须是均匀分布才能有效地避免哈希碰撞的问题,所以就要对其进行取模运算。

2.HashMap默认加载因子为什么选择0.75?

因为提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,因为在hashmap中影响性能的因为有俩个,一个是初始容量,一个便是加载因子,如果加载因子小的话就会造成空间利用率低,并提高了rehash的次数,为了保证最大程度上减少rehash的次数,0.75是最适宜的标准数也是折中的一种考虑。

3.jdk1.7插入数据方式有什么弊端?

上面我提到了形成环,那么到底是为什么呢,因为在使用的过程中,如果一个线程在插入一个节点的时候,另一个线程也在插入节点,而且两个线程插入的都是对方线程上的几点,这样当进行头插的时候会使链表发生反转,便形成了环状,造成了死循环,如下图:

 

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

面试官:小伙子,你能给我说一下HashMap的实现原理吗?的更多相关文章

  1. [每日一题]面试官问:for in和for of 的区别和原理?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  2. 阿里P7面试官:请你简单说一下类加载机制的实现原理?

    面试题:类加载机制的原理 面试官考察点 考察目标: 了解面试者对JVM的理解,属于面试八股文系列. 考察范围: 工作3年以上. 技术背景知识 在回答这个问题之前,我们需要先了解一下什么是类加载机制? ...

  3. 面试官:请讲一下Redis主从复制的功能及实现原理

    摘要:Redis在主从模式下会有许多问题需要考虑,这里写了一些关于redis在多服务器下的一些问题分析和总结. Redis单节点存在单点故障问题,为了解决单点问题,一般都需要对redis配置从节点,然 ...

  4. 【面试题2020-03-30】面试官:对并发熟悉吗?说说Synchronized及实现原理

    一.Synchronized的基本使用 Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法. Synchronized的作用主要有三个: 1.确保线程互斥的访问同 ...

  5. 一个HashMap能跟面试官扯上半个小时

    一个HashMap能跟面试官扯上半个小时 <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 一个volatile ...

  6. 口述完SpringMVC执行流程,面试官就让同事回家等消息了

    Srping MVC 执行流程真的是老生常谈的话题了,最近同事小刚出去面试,前面面试官相继问了几个 Spring 相关的问题,但当面试官问他,你知道 Srping MVC 的执行流程吗?小刚娴熟的巴拉 ...

  7. [每日一题]面试官问:谈谈你对ES6的proxy的理解?

    [每日一题]面试官问:谈谈你对ES6的proxy的理解? 关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  8. 面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

    前言 Ym8V9H.png (高清无损原图.pdf关注公众号后回复 ThreadLocal 获取,文末有公众号链接) 前几天写了一篇AQS相关的文章:我画了35张图就是为了让你深入 AQS,反响不错, ...

  9. 面试官:小伙子,你给我说一下Java Exception 和 Error 的区别吧?

    前言 昨天在整理粉丝给我私信的时候,发现了一个挺有意思的事情.是这样的,有一个粉丝朋友私信问我Java 的 Exception 和 Error 有什么区别呢?说他在面试的时候被问到这个问题卡壳了,最后 ...

  10. 面试官:"谈谈分库分表吧?"

    原文链接:面试官:"谈谈分库分表吧?" 面试官:“有并发的经验没?”  应聘者:“有一点.”   面试官:“那你们为了处理并发,做了哪些优化?”   应聘者:“前后端分离啊,限流啊 ...

随机推荐

  1. Linux用户和组管理命令-用户创建useradd

    用户管理命令 useradd usermod userdel 组帐号维护命令 groupadd groupmod groupdel 用户创建 useradd 命令可以创建新的Linux用户 格式: u ...

  2. H5移动端实现图片上传

    转至 :https://blog.csdn.net/qq_37610423/article/details/84319410 效果图: 我在用这个的时候发现博主少写了一些东西,导致功能无法实现,所以我 ...

  3. final修饰注意事项

    StringBuilder , StringBuffer ,String 都是 final 的,但是为什么StringBuilder , StringBuffer可以进行修改呢,因为不可变包括的是,引 ...

  4. 【Flutter 混合开发】与原生通信-EventChannel

    Flutter 混合开发系列 包含如下: 嵌入原生View-Android 嵌入原生View-iOS 与原生通信-MethodChannel 与原生通信-BasicMessageChannel 与原生 ...

  5. nginx负载均衡常见问题配置信息

    nginx为后端web服务器(apache,nginx,tomcat,weblogic)等做反向代理 几台后端web服务器需要考虑文件共享,数据库共享,session共享问题.文件共享可以使用nfs, ...

  6. Kubernetes 搭建 ES 集群(存储使用 cephfs)

    一.集群规划 使用 cephfs 实现分布式存储和数据持久化 ES 集群的 master 节点至少需要三个,防止脑裂. 由于 master 在配置过程中需要保证主机名固定和唯一,所以搭建 master ...

  7. JWT实现过程及应用

    jwt实现过程 # 用户登录,返回给客户端token(服务端不保存),用户带着token,服务端拿到token再校验; 1,提交用户名和密码给服务端,如果登陆成功,jwt会创建一个token,并返回; ...

  8. WC2019 填坑记

    2019年1月8日 1.Luogu P2147 [SDOI2008]洞穴勘测 (LCT模板题&LCT学习) 2019年1月9日 2.LuoguP3203 [HNOI2010]弹飞绵羊  (LC ...

  9. POJ2432 Around the world

    题意描述 Around the world 在一个圆上有 \(n\) 点,其中有 \(m\) 条双向边连接它们,每条双向边连接两点总是沿着圆的最小弧连接. 求从 \(1\) 号点出发并回到 \(1\) ...

  10. Positions of Large Groups

    Positions of Large Groups In a string S of lowercase letters, these letters form consecutive groups ...