【大厂面试08期】谈一谈你对HashMap的理解?
摘要
HashMap的原理也是大厂面试中经常会涉及的问题,同时也是工作中常用到的Java容器,本文主要通过对以下问题进行分析讲解,来帮助大家理解HashMap的原理。
1.HashMap添加一个键值对的过程是怎么样的?
2.为什么说HashMap不是线程安全的?
3.为什么要一起重写hashCode()和equal()方法?
HashMap添加一个键值对的过程是怎么样的?
这是网上找的一张流程图,可以结合着步骤来看这个流程图,了解添加键值对的过程。
1.初始化table
判断table是否为空或为null,否则执行resize()方法(resize方法一般是扩容时调用,也可以调用来初始化table)。
2.计算hash值
根据键值key计算hash值。(因为hashCode是一个int类型的变量,是4字节,32位,所以这里会将hashCode的低16位与高16位进行一个异或运算,来保留高位的特征,以便于得到的hash值更加均匀分布)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.插入或更新节点
根据(n - 1) & hash计算得到插入的数组下标i,然后进行判断
table[i]==null
那么说明当前数组下标下,没有hash冲突的元素,直接新建节点添加。
table[i].hash == hash &&(table[i]== key || (key != null && key.equals(table[i].key)))
判断table[i]的首个元素是否和key一样,如果相同直接更新value。
table[i] instanceof TreeNode
判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对。
其他情况
上面的判断条件都不满足,说明table[i]存储的是一个链表,那么遍历链表,判断是否存在已有元素的key与插入键值对的key相等,如果是,那么更新value,如果没有,那么在链表末尾插入一个新节点。插入之后判断链表长度是否大于8,大于8的话把链表转换为红黑树。
4.扩容
插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(一般是数组长度*负载因子0.75),如果超过,进行扩容。
源代码如下:
2.为什么说HashMap不是线程安全的?
其实通过学习HashMap添加键值对的方法,我们可以看到整个方法内都没有使用到锁,所以一旦多线并发访问,就有可能造成数据不一致的问题,
例如:
如果有两个添加键值对的线程都执行到if ((tab = table) == null || (n = tab.length) == 0)这行语句,都对table变量进行数组初始化,就会造成已经初始化好的数组table被覆盖,然后前面初始化的线程会将键值对添加到之前初始化的数组中去,造成键值对丢失。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
...后面的代码省略
}
3.为什么要一起重写hashCode()和equal()方法?
当我们的对象一旦作为HashMap中的key,或者是HashSet中的元素使用时,就必须同时重写hashCode()和equal()方法
首先看看hashCode()和equal()方法的默认实现
可以看到Obejct类中的源码如下,可以看到equals()方法的默认实现是判断两个对象的内存地址是否相同来决定返回结果。
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
网上很多博客说hashCode的默认实现是返回内存地址,其实不对,以OpenJDK为例,hashCode的默认计算方法有5种,有返回随机数的,有返回内存地址,具体采用哪一种计算方法取决于运行时库和JVM的具体实现。
感兴趣的朋友可以看看这篇博客
https://blog.csdn.net/xusiwei1236/article/details/45152201
然后看看hashCode()方法,equal()方法在HashMap中的应用
static final int hash(Object key) {
int h;
//因为hashCode是一个int类型的变量,是4字节,32位,所以这里会将hashCode的低16位与高16位进行一个异或运算,来保留高位的特征,以便于得到的hash值更加均匀分布
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
为了将一组键值对均匀得存储在一个数组中,HashMap对key的hashCode进行计算得到一个hash值,用hash对数组长度取模,得到数组下标,将键值对存储在数组下标对应的链表下(假设链表长度小于8,没有达到转换为红黑树的阀值)。
下面是添加键值对的putVal()方法,当数组下标对应的是一个链表时执行的代码
//遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//已经遍历到链表末尾,说明链表不存在这个key
p.next = newNode(hash, key, value, null);//在末尾添加这个键值对
if (binCount >= TREEIFY_THRESHOLD - 1) //超过链表转化为红黑树的阀值(也急速链表长度》=8)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
可以清楚地看到判断添加的key与链表中已存在的key是否相等的方法主要是e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))),
也就是:
1.先判断hash值是否相等,不相等直接结束判断,因为hash值不相等,key肯定不相等。
2.判断两个key对象的内存地址是否相等(相等指向内存中同一个对象)。
3.key不为null,调用key的equal()方法判断是否相等,因为有可能两个key在内存中存储的地址不一样,但是是相等的。
就像是
String a = new String("test");
String b = new String("test");
System.out.println("a==b is "+a==b);//打印为false
System.out.println("a.equals(b) is "+a.equals(b));//打印为true
背景
假设我们有一个KeyObject类,假设我们认为两个KeyObject的属性a相等,那么KeyObject就是相等的相等的,我们将KeyObject作为HashMap的key,以KeyObject是否相等作为去重标准,不能重复添加KeyObject相等,value不等的值到HashMap中去
public static class KeyObject {
Integer a;
public KeyObject(Integer a) {
this.a = a;
}
}
假设都hashCode()方法和equals()方法都不重写(结果:HashMap无法保证去重)
执行以下代码:
public static void main(String[] args) {
KeyObject key1 = new KeyObject(1);
KeyObject key2 = new KeyObject(1);
System.out.println("key1的hashCode为"+ key1.hashCode());
System.out.println("key2的hashCode为" + key2.hashCode());
System.out.println("key1.equals(key2)的结果为"+(key1.equals(key2)));
HashMap<KeyObject,String> hashMap = new HashMap<KeyObject,String>();
hashMap.put(key1,"value1");
hashMap.put(key2,"value2");
//打印hashMap
for(KeyObject key :hashMap.keySet()){
System.out.println("KeyObject.a="+key.a+" : "+hashMap.get(key));
}
}
如果KeyObject的hashCode()方法和equals()方法都不重写,那么即便KeyObject的属性a都是1,key1和key2的hashCode都是不相同的,key1和key2调用equals()方法也不相等,这样hashMap中就可以同时存在key1和key2了。
打印结果:
key1的hashCode为728890494
key2的hashCode为1558600329
key1.equals(key2)的结果为false
KeyObject.a=1 : value1
KeyObject.a=1 : value2
假如只重写hashCode()方法(结果:无法正确地与链表元素进行相等判断,从而无法保证去重)
执行以下代码:
public static class KeyObject {
Integer a;
public KeyObject(Integer a) {
this.a = a;
}
@Override
public int hashCode() {
return a;
}
public static void main(String[] args) {
KeyObject key1 = new KeyObject(1);
KeyObject key2 = new KeyObject(1);
System.out.println("key1的hashCode为"+ key1.hashCode());
System.out.println("key2的hashCode为" + key2.hashCode());
System.out.println("key1.equals(key2)的结果为"+(key1.equals(key2)));
HashMap<KeyObject,String> hashMap = new HashMap<KeyObject,String>();
hashMap.put(key1,"value1");
hashMap.put(key2,"value2");
for(KeyObject key :hashMap.keySet()){
System.out.println("TestObject.a="+key.a+" : "+hashMap.get(key));
}
}
}
此时equal()方法的实现是默认实现,也就是当两个对象的内存地址相等时,equal()方法才返回true,虽然key1和key2的a属性是相同的,但是他们在内存中是不同的对象,所以key1==key2结果会是false,KeyObject的equals()方法默认实现是判断两个对象的内存地址,所以 key1.equals(key2)也会是false,所以这两个键值对可以重复地添加到hashMap中去。
输出结果:
key1的hashCode为1
key2的hashCode为1
key1.equals(key2)的结果为false
TestObject.a=1 : value1
TestObject.a=1 : value2
假如只重写equals()方法(结果:映射到HashMap中不同数组下标,无法保证去重)
public static class KeyObject {
Integer a;
public KeyObject(Integer a) {
this.a = a;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KeyObject keyObject = (KeyObject) o;
return Objects.equals(a, keyObject.a);
}
public static void main(String[] args) {
KeyObject key1 = new KeyObject(1);
KeyObject key2 = new KeyObject(1);
System.out.println("key1的hashCode为"+ key1.hashCode());
System.out.println("key2的hashCode为" + key2.hashCode());
System.out.println("key1.equals(key2)的结果为"+(key1.equals(key2)));
HashMap<KeyObject,String> hashMap = new HashMap<KeyObject,String>();
hashMap.put(key1,"value1");
hashMap.put(key2,"value2");
for(KeyObject key :hashMap.keySet()){
System.out.println("TestObject.a="+key.a+" : "+hashMap.get(key));
}
}
}
假设只equals()方法,hashCode方法会是默认实现,具体的计算方法取决于JVM,(测试时发现是内存地址不同但是相等的对象,它们的hashCode不相同),所以计算得到的数组下标不相同,会存储到hashMap中不同数组下标下的链表中,也会导致HashMap中存在重复元素。
输出结果如下:
key1的hashCode为1289479439
key2的hashCode为6738746
key1.equals(key2)的结果为true
TestObject.a=1 : value1
TestObject.a=1 : value2
总结
所以当我们的对象一旦作为HashMap中的key,或者是HashSet中的元素使用时,就必须同时重写hashCode()和equal()方法,因为hashCode会影响key存储的数组下标及与链表元素的初步判断,equal()是作为判断key与链表中的key是否相等的最后标准。
- 所以只重写hashCode()方法,会导致无法正确地与链表元素进行相等判断,从而无法保证去重)
- 只重写equals()方法导致键值对映射到HashMap中不同数组下标,无法保证去重
【大厂面试08期】谈一谈你对HashMap的理解?的更多相关文章
- 【大厂面试02期】Redis过期key是怎么样清理的?
PS:本文已收录到1.1K Star数开源学习指南--<大厂面试指北>,如果想要了解更多大厂面试相关的内容,了解更多可以看 http://notfound9.github.io/inter ...
- 【大厂面试03期】MySQL是怎么解决幻读问题的?
问题分析 首先幻读是什么? 根据MySQL文档上面的定义 The so-called phantom problem occurs within a transaction when the same ...
- 【大厂面试07期】说一说你对synchronized锁的理解?
synchronized锁的原理也是大厂面试中经常会涉及的问题,本文主要通过对以下问题进行分析讲解,来帮助大家理解synchronized锁的原理. 1.synchronized锁是什么?锁的对象是什 ...
- 【大厂面试06期】谈一谈你对Redis持久化的理解?
Redis持久化是面试中经常会问到的问题,这里主要通过对以下几个问题进行分析,帮助大家了解Redis持久化的实现原理. 1.Redis持久化是什么? 2.Redis持久化有哪些策略?各自的实现原理是怎 ...
- 【大厂面试04期】讲讲一条MySQL更新语句是怎么执行的?
流程图 这是在网上找到的一张流程图,写的比较好,大家可以先看图,然后看详细阅读下面的各个步骤. 执行流程: 1.连接验证及解析 客户端与MySQL Server建立连接,发送语句给MySQL Serv ...
- 【大厂面试05期】说一说你对MySQL中锁的了解?
这是我总结的一个表格,是本文中涉及到的锁(因为篇幅有限就没有包括自增锁) 加锁范围 名称 用法 数据库级 全局读锁 执行Flush tables with read lock命令各整个库接加一个读锁, ...
- 4000字干货长文!从校招和社招的角度说说如何准备Java后端大厂面试?
插个题外话,为了写好这篇文章内容,我自己前前后后花了一周的时间来总结完善,文章内容应该适用于每一个学习 Java 的朋友!我觉得这篇文章的很多东西也是我自己写给自己的,比如从大厂招聘要求中我们能看到哪 ...
- 大厂面试:一个四年多经验程序员的BAT面经(字节、阿里、腾讯)
前言 上次写了篇欢聚时代的面经,公众号后台有些读者反馈说看的意犹未尽,希望我尽快更新其他大厂的面经,这里先说声抱歉,不是我太懒,而是项目组刚好有个活动要赶在春节前上线,所以这几天经常加班,只能工作之余 ...
- 从一张图开始,谈一谈.NET Core和前后端技术的演进之路
从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和 ...
随机推荐
- U-Learning 后端开发日志(建设中...)
目录 U-Learning--基于泛在学习的教学系统 项目背景 技术栈 框架 中间件 插件 里程碑 CentOS 7搭建JAVA开发环境 接口参数校验(不使用hibernate-validator,规 ...
- Oracle数字格式化
@ 目录 Oracle数字格式化 开发中的常见问题 数字格式模型元素 Oracle数字格式化 A format model is a character literal that describes ...
- 使用Docker发布blazor wasm
Blazor编译后的文件是静态文件,所以我们只需要一个支持静态页面的web server即可. 根据不同项目,会用不同的容器编排,本文已无网关的情况下为例,一步一步展示如何打包进docker 需求 H ...
- 【MobileNet-V1】-2017-CVPR-MobileNets Efficient Convolutional Neural Networks for Mobile Vision Applications-论文阅读
2017-CVPR-MobileNets Efficient Convolutional Neural Networks for Mobile Vision Applications Andrew H ...
- C语言/Linux命令行参数argc、argv[ ]详解
1.void main(int argc,char *argv[]) argv[]:表示的是一个指针数组,一共有argc个元素,其中存放的是指向每一个参数的指针. argc:参数个数 2.以Linux ...
- SpringCloud Netflix (六):Config 配置中心
------------恢复内容开始------------ SpringCloud Config 配置中心 Config 配置中心 Spring Cloud Config为分布式系统中的外部化配置提 ...
- Rocket - util - AsyncQueue
https://mp.weixin.qq.com/s/6McbqOKM4fu4J5vdpZvxKw 简单介绍异步队列(AsyncQueue)的实现. 0. 异步队列 异步队列的两端分 ...
- jchdl - RTL实例 - Adder
https://mp.weixin.qq.com/s/9S29BCTcJfbpR62ALjSidA 加法器. 参考链接 https://github.com/wjcdx/jchdl/blob/ ...
- 收藏!如何有效实施devops?
当今IT行业的竞争日益激烈,各家公司都在寻找优化软件研发过程的方法,因为交付比对手更具竞争力的产品已经越发成为一件成本高昂的事情.这也是DevOps发挥作用的地方,因为它可以在工程管理的各个方面提供帮 ...
- 数据库之 MySQL --- 下载、安装 及 概述(一)
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一 . MySql数据库的安装 1.图解MySQL程序结构 2.双击运行安装程序:以Win32位为例 ...