【大厂面试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和 ...
随机推荐
- 二,文件上传控件el-upload
需求: 上传文件,保存到服务器,并保存历史记录 上效果图 <el-form-item label="文件"> <el-upload ref="uploa ...
- C#常见异常
SystemException 其他用户可处理的异常的基本类 ArgumentException 方法的参数是非法的 ArgumentNullException 一个空参数传递给方法,该方法不能接受该 ...
- Kivy主窗体大小的控制
1. 引入依赖模块 主窗体大小的控制,需要使用到kivy.core.window中的Window模块 from kivy.app import App from kivy.core.window im ...
- NO.6 ADS1115与MSP432进行I2C通信_运行实例
B站第一次传视频,手机拍摄大家见谅!
- 二、Spring装配Bean
内容 声明bean 构造器注入和Setter方法注入 装配Bean 控制bean的创建和销毁 关键词 装配(wiring) 组件扫描(component scanning) 自动装配(AutoWiri ...
- 那些面试官必问的JAVA多线程和并发面试题及回答
Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程是在进程中执行的一个任务.Java运行环 ...
- 一文读懂JAVA多线程
背景渊源 摩尔定律 提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一Gordon Moore提出来的.其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍 ...
- 你确信 X-Forwarded-For 拿到的就是用户真实 IP 吗?
X-Forwarded-For 拿到的就是真实 IP 吗? 1.故事 在这个小节开始前,我先讲一个开发中的小故事,可以加深一下大家对这个字段的理解. 前段时间要做一个和风控相关的需求,需要拿到用户的 ...
- iOS开发添加新手引导
往往项目中经常出现此类需求 用户通过点击引导按钮可响应页面附带按钮的点击事件. // // gzhGuideView.h // GuideView // // Created by 郭志贺 on 20 ...
- 【Java8新特性】关于并行流与串行流,你必须掌握这些!!
写在前面 提到Java8,我们不得不说的就是Lambda表达式和Stream API.而在Java8中,对于并行流和串行流同样做了大量的优化.对于并行流和串行流的知识,也是在面试过程中,经常被问到的知 ...