惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
博客园 - Franky
MyScale Blog
MyScale Blog
Jina AI
Jina AI
B
Blog
Microsoft Security Blog
Microsoft Security Blog
T
Troy Hunt's Blog
博客园_首页
T
Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
Lohrmann on Cybersecurity
GbyAI
GbyAI
T
Tenable Blog
B
Blog RSS Feed
S
Securelist
T
Threat Research - Cisco Blogs
P
Privacy International News Feed
P
Proofpoint News Feed
T
The Exploit Database - CXSecurity.com
H
Hackread – Cybersecurity News, Data Breaches, AI and More
量子位
博客园 - 三生石上(FineUI控件)
大猫的无限游戏
大猫的无限游戏
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
罗磊的独立博客
AWS News Blog
AWS News Blog
V
V2EX
宝玉的分享
宝玉的分享
J
Java Code Geeks
小众软件
小众软件
Spread Privacy
Spread Privacy
腾讯CDC
Google Online Security Blog
Google Online Security Blog
月光博客
月光博客
V
Visual Studio Blog
The Hacker News
The Hacker News
C
CERT Recently Published Vulnerability Notes
Project Zero
Project Zero
Know Your Adversary
Know Your Adversary
T
The Blog of Author Tim Ferriss
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
NISL@THU
NISL@THU
C
Check Point Blog
Webroot Blog
Webroot Blog
D
DataBreaches.Net
Cloudbric
Cloudbric
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家

Java技术经验分享

生产故障处理SOP分享 | Java技术经验分享 系统稳定性建设实践总结【转载】 | Java技术经验分享 valine访问leancloud国际版异常,评论失效修复 | Java技术经验分享 匆匆忙忙的2021 | Java技术经验分享 聊一下换工作 | Java技术经验分享 IoT系列(2):WIFI设备常见配网方案介绍 | Java技术经验分享 IoT系列(1):什么是物联网 | Java技术经验分享 Java8特性2 - StreamApi | Java技术经验分享 Java8特性1 - lambda表达式&函数式接口 | Java技术经验分享 设计模式(5)-适配器模式 | Java技术经验分享 设计模式(4)-建造者模式 | Java技术经验分享 设计模式(3)-原型模式与浅拷贝和深拷贝 | Java技术经验分享 设计模式(2)-工厂模式图文介绍 | Java技术经验分享 设计模式(1)-带你了解3类8种单例模式 | Java技术经验分享 Java时间处理5---Java8中时区相关类库介绍 | Java技术经验分享 Java时间处理4---Java8中LocalDate、LocalTime、LocalDateTime介绍 | Java技术经验分享 Java时间处理3---Java8中Instant、Duration、Period、Clock介绍 | Java技术经验分享 一些有意思的问答 | Java技术经验分享 Nacos系列博客说明 | Java技术经验分享 菜鸡程序员的2019年度总结 | Java技术经验分享 Java中“附近的人”实现方案讨论及代码实现 | Java技术经验分享 Java时间处理2----时区TimeZone类方法探究(Java8以前) | Java技术经验分享 Java时间处理1----Date和Calendar方法探究(Java8以前) | Java技术经验分享 FastJson中JSONString、JavaBean、JSONObject、JSONArray的转换关系及API示例 | Java技术经验分享 2019.11软考软件设计师归来心得体会及复习备考指南 | Java技术经验分享 你还没用过“约定式提交”吗?那你赶紧来补补知识吧 | Java技术经验分享 教你如何看懂UML中的类图及类图中的关系 | Java技术经验分享 设计模式总览 | Java技术经验分享 萌新入门Github请看这里,学不会远程教 | Java技术经验分享 Hexo的工作原理探究 | Java技术经验分享 Hexo-theme-butterfly修改调整记录教程 | Java技术经验分享 排序8:基数排序 | Java技术经验分享 排序7:归并排序 | Java技术经验分享 排序6:快速排序 | Java技术经验分享 排序5:冒泡排序 | Java技术经验分享 排序4:堆排序 | Java技术经验分享 排序3:选择排序 | Java技术经验分享 排序2:希尔排序 | Java技术经验分享 排序1:直接插入排序 | Java技术经验分享 推荐一款博客一文多发的良心工具OpenWrite | Java技术经验分享 近期学习计划 | Java技术经验分享 Nacos(九):Nacos集群部署和遇到的问题 | Java技术经验分享 Nacos(八):Nacos持久化 | Java技术经验分享 Nacos(七):Nacos共享配置 | Java技术经验分享 Nacos(六):多环境下如何“管理”及“隔离”配置和服务 | Java技术经验分享 Nacos(五):多环境下如何“读取”Nacos中相应的配置 | Java技术经验分享 Nacos(四):SpringCloud项目中接入Nacos作为配置中心 | Java技术经验分享 Nacos(三):Nacos与OpenFeign的对接使用 | Java技术经验分享 Nacos(二):SpringCloud项目中接入Nacos作为注册中心 | Java技术经验分享 Nacos(一):Nacos介绍 | Java技术经验分享 20190719小组分享 | Java技术经验分享 Java中==和equals方法的分析 | Java技术经验分享 Java中的自动拆装箱、装箱缓存 | Java技术经验分享 About-blog | Java技术经验分享 Java中的编译、反编译和反编译工具全家桶分享 | Java技术经验分享 finalize()的生命周期(执行过程) | Java技术经验分享 Java关键字之final、finally与finalize方法 | Java技术经验分享 Java中重写、重载 | Java技术经验分享 Java中面向对象的三大特征:继承、封装、多态 | Java技术经验分享 DockerFile介绍 | Java技术经验分享 Docker环境下安装Gitlab | Java技术经验分享 Docker中私有仓库的搭建流程 | Java技术经验分享 Centos7下两种方式安装Docker-CE | Java技术经验分享 Vert.x创建一个Http服务 | Java技术经验分享 Vert.x创建TCP服务端及客户端 | Java技术经验分享 Vert.x Core(二)- Event Bus(事件总线) | Java技术经验分享 Vert.x-Core(一)- 基础篇 | Java技术经验分享 SpringBoot项目中实现国际化 | Java技术经验分享 Vert.x介绍 | Java技术经验分享 毕设选题项目本地运行环境搭建教程 | Java技术经验分享 Jupyter Notebooks的安装和使用介绍 | Java技术经验分享 算法笔试题:1元,5元,10元,20元,50元、100元面值人民币组合给定x元的问题 | Java技术经验分享 Quartz学习总结 | Java技术经验分享 SpringBoot2.x集成Redis | Java技术经验分享 SpringBoot2.x集成MongoDB | Java技术经验分享 [SpringCloud学习] - 浅谈微服务架构 | Java技术经验分享 基于hexo和coding免费搭建个人博客网站 | Java技术经验分享 Hello World | Java技术经验分享
Java中equals和HashCode方法的分析 | Java技术经验分享
文章作者: LarsCheng · 2019-06-17 · via Java技术经验分享

前言

上一篇文章简单分析了equals()与==的关系,本文我们再来看看equals()与hashcode()的关系。hashcode的使用还是有很多坑的,一起看看吧~

本文主要有以下几点来分析:

  1. hashCode使用中产生的问题
  2. equals/hashcode的渊源
  3. 产生问题的原因
  4. 正确的使用姿势

hashCode使用中产生的问题

注:HashSet是一个无序、不可重复的集合,我们做一个小测试运行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

public class HashEqualsDemo {

static class Person {
private String age;

Person(String age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" + "age='" + age + '\'' + '}';
}
}
}

public static void main(String[] args) {
HashSet set1 = new HashSet();
set1.add("1");
set1.add("1");

for (Object a : set1) {
System.out.println(a);
}

HashSet set2 = new HashSet();
Person p1 = new Person("1");
Person p2 = new Person("1");
set2.add(p1);
set2.add(p2);

for (Object a : set2) {
System.out.println(a);
}
}
}

由于HashSet是不可重复的集合,所以输出的结果中set1和set2中都应该只有一个元素,那么执行结果是什么呢?如下

1
Person{age='1'}
Person{age='1'}

好吧,又双叒叕和我想象的不一样,set1不重复,set2明显发生了重复现象,这是为什么呢?

这是因为equals、hashCode使用不规范导致的,问题且放在这,我们先看看equals和hashCode的关系

equals/hashcode的渊源

同为Object类中的方法

public boolean equals(Object obj)
public int hashCode()

  • equals(): 用来判断两个对象是否相同,再Object类中是通过判断对象间的内存地址来决定是否相同
  • hashCode(): 获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

由于同为Object类中的方法,所以基本上所有Java类都会继承这两个方法,所以通过阅读hashCode方法的注释发现了:

概括为以下几点:

  1. 该方法返回对象的哈希码,支持该方法是为哈希表提供一些优点,例如,HashMap 提供的哈希表。
  2. 同一个对象未发生改变时多次调用hashCode()返回值必须相同,
  3. 两个对象equals不相等,那么两对象的hashCode()返回必定不同(此处可用来提高哈希表性能)
  4. 两个对象的hashCode()返回值相同,两对象不一定相同,还需要通过equals()再次判断
  5. 当equals方法被重写时,通常有必要重写 hashCode 方法

通过第1点其实可以看出,hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置,当对象不会用来创建像hashMap、hashSet等散列表时,hashCode()实际上用不上。

产生问题的原因

了解了两者的关系,我们在回过头来看看产生问题的原因

分析原因前需要了解哈希表的底层实现,hashCode在哈希表中充当的作用:

举一个栗子说明下:

  • 假设内存中有0 1 2 3 4 5 6 7 8这8个位置,如果我有个字段叫做ID,那么我要把这个字段存放在以上8个位置之一,如果不用HashCode而任意存放,那么当查找时就需要到8个位置中去挨个查找

  • 使用HashCode则效率会快很多,把ID的HashCode%8,然后把ID存放在取得余数的那个位置,然后每次查找该类的时候都可以通过ID的HashCode%8求余数直接找到存放的位置了

  • 如果ID的HashCode%8算出来的位置上本身已经有数据了怎么办?这就取决于算法的实现了,比如ThreadLocal中的做法就是从算出来的位置向后查找第一个为空的位置,放置数据;HashMap的做法就是通过链式结构连起来。反正,只要保证放的时候和取的时候的算法一致就行了。

  • 如果ID的HashCode%8相等怎么办(这种对应的是第三点说的链式结构的场景)?这时候就需要定义equals了。先通过HashCode%8来判断类在哪一个位置,再通过equals来在这个位置上寻找需要的类。对比两个类的时候也差不多,先通过HashCode比较,假如HashCode相等再判断equals。如果两个类的HashCode都不相同,那么这两个类必定是不同的。

其实在HashSet就是采用的这种存储和获取方式,通过HashCode和equals组合的方式来保证集合无重复。也说明了HashCode()在散列表中是发挥作用的

ok,我们分析下最开始的代码,找一下输出结果重复的原因(代码片段):

1
2
3
4
5
6
7
8
9
10
HashSet set1 = new HashSet();
set1.add("1");
set1.add("1");


HashSet set2 = new HashSet();
Person p1 = new Person("1");
Person p2 = new Person("1");
set2.add(p1);
set2.add(p2);

set1.add(“1”);:set1集合为空,找到hashCode对应在哈希表中的存储区,直接存入字符串1

set1.add(“1”);:首先判断该字符串1的hashCode值对应哈希表中所在的存储区域是否有相同的hashCode,此处调用String类中的hashCode(),显然两次返回了相同的hashCode,接着进行equals()方法的比较,此处调用String类中的equals(),由于两个字符串指向的常量池中的同一个字符串1,所以两个String对象相同,字符串1重复,不进行存储。

set2.add(p1);:set2集合为空,找到对象p1的hashCode对应在哈希表中的存储区,直接存入对象p1

set2.add(p2);:首先判断该对象p2的hashCode值对应哈希表中所在的存储区域是否有相同的hashCode,Person中未重写hashCode()此处调用Object类中的hashCode(),所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为p1、p2为不同对象,地址值不同,所以这里不存在与p2相同hashCode值的对象,直接存入对象p2

看到这里已经知道Set集合中出现重复的原因了。都是因为hashCode、equals的不规范使用。

正确的使用姿势

从Jdk源码的注释中可以看出,hashCode() 在散列表中才会发挥作用,当对象无需创建像HashMap、HashSet等集合时,可以不用重写hashCode方法,但是如果有使用到对象的哈希集合等操作时,必须重写hashCode()和equals()。

修改最初的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

public class HashEqualsDemo {

static class Person {
private String age;

Person(String age) {
this.age = age;
}


@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Person)) {
return false;
}

if (obj == this) {
return true;
}
Person person = (Person) obj;

return person.age.equals(this.age);
}


@Override
public int hashCode() {
return Objects.hash(age);
}

@Override
public String toString() {
return "Person{" + "age='" + age + '\'' + '}';
}
}
}

public static void main(String[] args) {
HashSet set1 = new HashSet();
set1.add("1");
set1.add("1");

for (Object a : set1) {
System.out.println(a);
}

HashSet set2 = new HashSet();
Person p1 = new Person("1");
Person p2 = new Person("1");
set2.add(p1);
set2.add(p2);

for (Object a : set2) {
System.out.println(a);
}
}
}

重写了equals和hashCode方法之后,执行结果就恢复正常了:

1
Person{age='1'}

总结

  • hashCode主要用于提升查询效率提高哈希表性能,来确定在散列结构中对象的存储地址
  • 重写equals()必须重写hashCode()
  • 哈希存储结构中,添加元素重复性校验的标准就是先检查hashCode值,后判断equals()
  • 两个对象equals()相等,hashcode()必定相等
  • 两个对象hashcode()不等,equals()必定也不等
  • 两个对象hashcode()相等,对象不一定相等,需要通过equals()进一步判断。

参考和感谢

哈希存储结构中添加元素的逻辑:https://blog.csdn.net/lijiecao0226/article/details/24609559
hashcode详解:https://www.cnblogs.com/whgk/p/6071617.html