我说对象相等,HashCode一定相等,面试官笑了...
本文已收录于:github.com/danmuking/a…
哈喽,大家哈,我是 DanMu。今天在刷博客的时候刷到一个挺有意思的面试题:“ 两个对象相等,Hashcode
一定相等,那如果两个对象不相等,**HashCode**
有没有可能相等?”
在Java
中想要比较两个对象是否相同,主要有两种方式:
- 重写
equals
方法,通过调用equals
方法进行比较 - 直接使用
==
进行比较
这也是老八股了,直接开始吟唱
equals()
方法
equals()
的作用是用来判断两个对象是否相等。equals()
定义在JDK
的Object
类中。默认的equals()
通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等。源码如下:
publicbooleanequals(Objectobj){
return(this==obj);
}
既然Object
中定义了equals()
方法,这就意味着所有的Java
类都实现了equals()
方法,所有的类都可以通过**equals()**
去比较两个对象是否相等。并且默认的equals
方法等价于==
,因此,我们通常需要重写比较两个对象的逻辑:若两个对象的内容相等,则返回true
;否则,返回fasle
。都是基础的知识了这里简单提一下,知道的朋友可以跳过:
不重写equals()
方法
如果不重写equals()
,那实际上,调用的Object
的equals()
方法,即调用的(p1==p2)
。它是比较“p1
和p2
是否是同一个对象”。由于p1
和p2
都是new
出来的对象,由独立的空间地址。因此,返回结果是false
。
public class EqualsTest1{
public static void main(String[] args) {
// 新建2个相同内容的Person对象,
// 再用equals比较它们是否相等
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
// false
System.out.printf("%s\n", p1.equals(p2));
}
/**
* @desc Person类。
*/
private static class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " - " +age;
}
}
}
重写equals()
方法
在实际代码中,我们通常都会采用这种方法,比如下面这段代码,我们在中重写了Person
的equals()
函数:当两个Person
对象的name
和age
都相等,则返回true
。
public class EqualsTest2{
public static void main(String[] args) {
// 新建2个相同内容的Person对象,
// 再用equals比较它们是否相等
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
// true
System.out.printf("%s\n", p1.equals(p2));
}
/**
* @desc Person类。
*/
private static class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " - " +age;
}
/**
* @desc 覆盖equals方法
*/
@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
//如果是同一个对象返回true,反之返回false
if(this == obj){
return true;
}
//判断是否类型相同
if(this.getClass() != obj.getClass()){
return false;
}
Person person = (Person)obj;
return name.equals(person.name) && age==person.age;
}
}
}
equals()
与 ==
的区别是什么?
简单小结一下:
==
: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不试同一个对象。
equals()
: 它的作用也是判断两个对象是否相等。但它有两种使用情况:
- 类没有重写equals()方法。则通过
equals()
比较该类的两个对象时,等价于通过==
比较这两个对象。 - 类重写了equals()方法。一般,我们都重写
equals()
方法来判断两个对象的内容是否相等;若它们的内容相等,则返回true
(即,认为这两个对象相等)。
hashCode()
方法
上面分析了equals()
与==
,似乎通过这两个方法已经可以实现判断对象是否相等的任务了,那么,为什么还需要**hashCode()**
方法呢?它的作用是什么呢?
hashCode()
的作用是获取哈希码,也称为散列码;它实际上是返回一个int
整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()
同样定义在JDK
的Objest
中,这就意味着Java
中的任何类都包含有hashCode()
函数。
但是!虽然每个Java
类都包含hashCode()
函数。但是!仅仅当创建并某个“散列表”时,该类的**hashCode()**
才会被使用,其它情况下,hashCode()
将不会被使用。Java
中常见的散列表主要是Map
接口的实现类,比较常见的如HashMap
,Hashtable
,HashSet
等等。
我们都知道,散列表存储的是键值对(key-value
),它的特点是:能根据“key
”快速的检索出对应的“value
”。这其中就用到了**hashCode()**
!
散列表的数据结构一般是通过数组+链表
实现的。当我们要获取散列表中的某个“value
”时,实际上是要获取数组中的某个位置的元素。而数组的位置,就是通过“key
”来获取的;更进一步说,数组的位置,是通过“key
”对应的**hashCode()**
计算得到的。
我们想要从散列表中检索某个元素时,会经过两个步骤
- 根据
hashCode()
计算出元素在数组中的哪个下标位置 - 遍历对应位置的链表,调用
equals()
方法判断两个对象是否相等
因此,hashCode()
的主要作用,就是用于计算元素在散列表中的位置。
hashCode()
和 equals()
的关系
那么接下来我们来总结一下hashCode()
和 equals()
的关系
- 使用散列表时
- 如果两个对象相等,那么它们的
hashCode()
值一定相同。 - 如果两个对象
hashCode()
相等,它们并不一定相等。 因为在散列表中,hashCode()
相等,只能说明两个对象被存放在数组的同一个下标的链表中,但是并不能说明这两个对象的内容相等。
- 如果两个对象相等,那么它们的
- 不使用散列表时,
hashCode()
根本不会被调用,所以两者之间实际上毫无关系。
在阿里巴巴开发手册-华山版中有一处强制规定: **【强制】**关于
hashCode
和equals的处理,遵循如下规则: 1)只要覆写equals
,就必须覆写hashCode
。 2)因为Set
存储的是不重复的对象,依据hashCode
和equals
进行判断,所以Set
存储的对象必须覆写这两个方法。 3)如果自定义对象作为Map
的键,那么必须覆写hashCode
和equals
。 说明:String
已覆写hashCode
和equals
方法,所以我们可以愉快地使用String
对象作为key
来使用。
两个对象不相等,HashCode
有没有可能相等?
在深入了解了**equals()**
和**hashCode()**
的基本原理后,我们可以来探讨一个有趣的问题:两个对象不相等,**HashCode**
有没有可能相等?
前面已经提到了,hashcodes()
定义在Object
中,并且允许子类重载,最终的返回值是一个int
类型,那么我们当然可以通过重写**hashCode()**
来返回相同的**HashCode**
,比如这个例子
// 鹿
class Deer{
double age;
Deer(double age){
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
Deer itemOne = (Deer) o;
return Objects.equals(age, itemOne.age);
}
@Override
public int hashCode() {
return 1;
}
}
// 马
class Horse {
double age;
Horse(double age){
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
Deer itemOne = (Deer) o;
return Objects.equals(age, itemOne.age);
}
@Override
public int hashCode() {
return 1;
}
}
上面的例子分别定义了一个Deer
类和一个Horse
类,但是由于重写了他们的hashCode()
方法,因此它们的hashCode
都是1
,在这种情况下,如果使用散列表将会发生一些有趣的事情:
public static void main(String[] args) {
Deer deer = new Deer(2.5);
Horse horse = new Horse(2.5);
Set<Object> set = new HashSet<>();
set.add(deer);
// true
System.out.println(set.contains(horse));
}
在这种情况下,明明我们只向set
中添加了deer
,但是在判断set.contains(horse)
的时候,set
出现了误判。原因是,在set
进行contains
判断的时候,首先会根据hashCode()
找到对应的数组下标,由于两个类的hashCode()
相同,所以将会索引到同一个位置,然后遍历链表元素,由于当前只存在一个元素deer
,因此就会调用equals()
方法比较两个元素是否相同,正好,两个对象有具有相同的属性,因此set
就认为这是两个相同的对象。
想要避免这个错误其实也非常简单,虽然这两个对象具有相同的属性,但是这两个对象属于不同的类,因此,只需要在**equals()**
中额外判断是否是同一个类即可:
class Deer{
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Deer)) return false;
Deer itemOne = (Deer) o;
return Objects.equals(age, itemOne.age);
}
...
}
class Horse {
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Horse)) return false;
Deer itemOne = (Deer) o;
return Objects.equals(age, itemOne.age);
}
...
}
但是如果正巧,两个对象有正好存在继承关系,那上面这种防御措施仍然会失效,因此,最好最好的方法,就是别搞事情,乖乖的使用自动生成hashcode
,这样可以极大程度的避免出现这种意外情况。
点关注,不迷路
好了,以上就是这篇文章的全部内容了,如果你能看到这里,非常感谢你的支持! 如果你觉得这篇文章写的还不错, 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!! 白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! 如果本篇博客有任何错误,请批评指教,不胜感激 !
最后推荐我的IM项目DiTing(github.com/danmuking/D…),致力于成为一个初学者友好、易于上手的 IM 解决方案,希望能给你的学习、面试带来一点帮助,如果人才你喜欢,给个Star⭐叭!
转载自:https://juejin.cn/post/7372456890343325711