众所周知HashMap是工作和面试中最常遇到的数据类型,但很多人对HashMap的知识止步于会用的程度,对它的底层实现原理一知半解,了解过很多HashMap的知识点,却都是散乱不成体系,今天一灯带你一块深入浅出的剖析HashMap底层实现原理。

看下面这些面试题,你能完整的答对几道?

1. HashMap底层数据结构?

JDK1.7采用的是数组+链表,数组可以通过下标访问,实现快速查询,链表用来解决哈希冲突。

链表的查询时间复杂度是O(n),性能较差,所以JDK1.8做了优化,引入了红黑树,查询时间复杂度是O(logn)。

JDK1.8采用的是数组+链表+红黑树的结构,当链表长度大于等于8,并且数组长度大于等于64时,链表才需要转换成成红黑树。

2. HashMap的初始容量是多少?

如果面试的时候,你回答是16,面试官肯定让你回去等通知。

JDK1.7的时候初始容量确实是16,但是JDK1.8的时候初始化HashMap的时候并没有指定容量大小,而是在第一次执行put数据,才初始化容量。

// 负载因子大小
final float loadFactor; // 默认负载因子大小
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 初始化方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

执行new HashMap()方法初始化的时候,只指定了负载因子的大小。

3. HashMap的put方法流程?

  1. 计算key的哈希值
  2. 判断数组是否为空,如果为空,就执行扩容,初始化数据大小。
  3. 如果数组不为空,根据哈希值找到数组所在下标
  4. 判断下标元素是否为null,如果为null就创建新元素
  5. 如果下标元素不为null,就判断是否是红黑树类型,如果是,则执行红黑树的新增逻辑
  6. 如果不是红黑树,说明是链表,就追加到链表末尾
  7. 如果判断链表长度是否大于等于8,数组长度是否大于等于64,如果不是就执行扩容逻辑
  8. 如果是,则需要把链表转换成红黑树
  9. 最后判断新增元素后,判断元素个数是否大于阈值(16*0.75=12),如果是则执行扩容逻辑,结束。

源码如下:

// put方法入口
public V put(K key, V value) {
// 计算哈希值
return putVal(hash(key), key, value, false, true);
} 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;
// 计算数组下标位置,判断下标位置元素是否为null
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);
// 链表结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在数组中找到key值、哈希值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 记录修改次数
++modCount;
// 元素个数大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}

4. HashMap容量大小为什么要设置成2的倍数?

int index = hash(key) & (n-1);

为了更快的计算key所在的数组下标位置。

当数组长度(n)是2的倍数的时候,就可以直接通过逻辑与运算(&)计算下标位置,比取模速度更快。

5. HashMap为什么是线程不安全?

原因就是HashMap的所有修改方法都没有加锁,导致在多线程情况下,无法保证数据一致性和安全性。

比如:一个线程删除了一个key,由于没有加锁,其他线程无法及时感知到,还继续能查到这个key,无法保证数据的一致性。

比如:一个线程添加完一个元素,由于没有加锁,其他线程无法及时感知到,另一个线程正在扩容,扩容后就把上一个线程添加的元素弄丢了,无法保证数据的安全性。

6. 解决哈希冲突方法有哪些?

常见有链地址法、线性探测法、再哈希法等。

  • 链地址法

    就是把发生哈希冲突的值组成一个链表,HashMap就是采用的这种。

  • 线性探测法

    发生哈希冲突后,就继续向下遍历,直到找到空闲的位置,ThreadLocal就是采用的这种,以后再详细讲。

  • 再哈希法

    使用一种哈希算法发生了冲突,就换一种哈希算法,直到不冲突为止(就是这么聪明)。

7. JDK1.8扩容流程有什么优化?

在JDK1.7扩容的时候,会遍历原数组,重新哈希,对新数组长度逻辑与,计算出数据下标,然后放到新数组中,比较麻烦耗时。

在JDK1.8扩容的时候,会遍历原数组,然后统计出两组数据,一组是新数组的下标位置不变,另一组是新数组的下标位置等于原数组的下标位置加上原数组的长度。

比如:数组长度由16扩容到32,哈希值是0和32的元素,在新旧数组中下标位置不变,都是下标为0的位置。而哈希值是16和48的元素,在新数组的位置=原数组的下标+原数组的长度,也就是下标为16的位置。

我说HashMap初始容量是16,面试官让我回去等通知的更多相关文章

  1. 因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!

    因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知! volatile 是并发编程的重要组成部分,也是面试常被问到的问题之一.不要向小强那样,因为一句:volati ...

  2. 因为不知道Java的CopyOnWriteArrayList,面试官让我回去等通知

    先看再点赞,给自己一点思考的时间,微信搜索[沉默王二]关注这个靠才华苟且的程序员.本文 GitHub github.com/itwanger 已收录,里面还有一线大厂整理的面试题,以及我的系列文章. ...

  3. 我说MySQL联合索引遵循最左前缀匹配原则,面试官让我回去等通知

    面试官: 我看你的简历上写着精通MySQL,问你个简单的问题,MySQL联合索引有什么特性? 心想,这还不简单,这不是问到我手心里了吗? 听我给你背一遍八股文! 我: MySQL联合索引遵循最左前缀匹 ...

  4. Java中boolean类型占用多少个字节?我说一个,面试官让我回家等通知

    摘自:https://www.cnblogs.com/qiaogeli/p/12004962.html 程序员乔戈里 腾讯面试官问我Java中boolean类型占用多少个字节?我说一个,面试官让我回家 ...

  5. 美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

    本文首发于微信公众号:程序员乔戈里 public class testT { public static void main(String [] args){ String A = "hi你 ...

  6. 为什么HashMap初始大小为16,为什么加载因子大小为0.75,这两个值的选取有什么特点?

    先看HashMap的定义: public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V> ...

  7. 腾讯面试官问我Java中boolean类型占用多少个字节?我说一个,面试官让我回家等通知

    本文首发于微信公众号:程序员乔戈里 什么是boolean类型,根据官方文档的描述: boolean: The boolean data type has only two possible value ...

  8. ArrayList、Vector、HashMap、HashSet的默认初始容量、加载因子、扩容增量

    当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,这无疑使效率大大降低. 加载因 ...

  9. ArrayList、Vector、HashMap、HashTable、HashSet的默认初始容量、加载因子、扩容增量

    这里要讨论这些常用的默认初始容量和扩容的原因是: 当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全 ...

随机推荐

  1. Canal实时解析mysql binlog数据实战

    一.说明 通过canal实时监听mysql binlog日志文件的变化,并将数据解析出来 二.环境准备 1.创建maven项目并修改pom.xml配置文件 <dependencies> & ...

  2. React报错之Object is possibly null

    正文从这开始~ 类型守卫 使用类型守卫来解决React中useRef钩子"Object is possibly null"的错误.比如说,if (inputRef.current) ...

  3. PHP小知识收集

    PEAR 是"PHP Extension and Application Repository"的缩写,即PHP扩展和应用仓库. PECL 是"PHP Extension ...

  4. python面向对象的特征及反射

    目录 派生类实操 面向对象特征之封装 property伪装属性(python内置装饰器) 面向对象特征之多态 面向对象之反射 派生类实操 1.将时间字典序列化成json格式,由于序列化数据类型的要求, ...

  5. 使用Python3.7+Tornado5.1集成新浪微博三方登录(无需企业资质)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_137 新浪微博:山寨版的twitter,各种粉丝的集散地,天朝人民的最爱,基本上网民都人手一个微博账号,所以使用新浪微博账号进行三 ...

  6. MySQL主从复制之半同步(semi-sync replication)

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 半同步简介 MASTER节点在执行完客户端提交的事务后不是立刻返回结果给客户端,而是等待至少一个SLAVE节点接收并写到r ...

  7. 金秋十月 - Apache DolphinScheduler 收获 2 位新 Committer

    点击上方蓝字关注 Apache DolphinScheduler Apache DolphinScheduler(incubating),简称"DS", 中文名 "海豚调 ...

  8. WebGPU的计算着色器实现冒泡排序

    大家好~本文使用WebGPU的计算着色器,实现了奇偶排序. 奇偶排序是冒泡排序的并行版本,在1996年由J Kornerup提出.它解除了每轮冒泡间的串行依赖以及每轮冒泡内部的串行依赖,使得冒泡操作可 ...

  9. 介绍下Java内存区域(运行时数据区)

    介绍下Java内存区域(运行时数据区) Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域.JDK 1.8 和之前的版本略有不同. 下图是 JDK 1.8 对JV ...

  10. Java-往数据库插入日期

    Java-往数据库中插入日期 将字符串类型的时间转换成mysql的日期格式 String str = "2022-6-11"; SimpleDateFormat sdf = new ...