hashcode与equals
Java 如何获取对象内存地址
maven引入依赖包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
示例代码
//获取两个对象的内存地址
System.out.println("user1的内存地址:"+ VM.current().addressOf(user1));
System.out.println("user2的内存地址:"+ VM.current().addressOf(user2));
Java 语言规范要求 equals 方法具有下面的特性:
- 自反性:对于任何非空引用 x,x.equals(x) 应该返回 true。
- 对称性:对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 true,x.equals(y) 也应该返回 true。
- 传递性:对于任何引用 x 、y 和 z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,x.equals(z) 也应该返回 true。
- 一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。
- 对于任意非空引用 x,x.equals(null) 应该返回 false
为什么重写equals方法后,要重写hashcode方法?(equals和hashCode间的通用约定)
如果覆写了equals方法,一般也要覆写hashCode方法,因为equals方法和hashCode方法间存在通用约定:
如果一个对象的 equals 方法做比较所用到的信息没有被修改的话,那么对该对象调用 hashCode 方法多次,它必须始终返回同一个整数,在同一个应用程序的多次执行过程中,这个整数可以不同
;如果两个对象根据 equals(Object) 方法是相等的,那么这两个对象的 hashCode 方法返回的整数值必然相等;
如果两个对象根据 equals(Object) 方法是不相等的,那么这两个对象的 hashCode 方法返回的整数值有可能相等(Hash冲突导致)
。
Java Object equals()方法
- equals方法的源代码
public boolean equals(Object obj) {
return (this == obj);
}
以上这个方法是Object类的默认实现
-
SUN公司设计
equals()方法的目的
?以后编程过程中,
都要通过equals方法来判断两个对象是否相等
。equals方法是判断两个对象是否相等的。 -
我们为什么要重写equals()方法?
- Object类这个默认的equals()方法不够用
- 在Object类中的equals方法中,默认采用的是
==
判断两个java对象是否相等。而==
判断的是两个java对象的内存地址
.我们应该判断两个Java对象的内容是否相等。所以Object的equals方法不够用,需要重写子类的equals()方法。
equals()方法比较两个对象,是判断两个对象的引用指向同一个对象,即比较两个对象的内存地址是否相等
代码示例:
-
创建一个User对象
public class User { private String name; private Integer sex; public User(String name,Integer sex){ this.name = name; this.sex = sex; } public User(){} }
-
未重写equals()方法
public class ObjectEqualsTest { public static void main(String[] args) { //判断两个基本数据类型的数据是否相等直接使用== int a = 100; int b = 100; //这个==是判断a中保存的100和b保存的100是否相等。 System.out.println(a==b); //判断两个Java对象是否相等,我们怎么办?能直接使用==吗? //创建一个用户对象是:姓名:张三;性别:1(男)2(女) User user1 = new User("张三",1); //创建一个新的用户对象是:姓名:张三;性别:1(男)2(女) User user2 = new User("张三", 1); //获取两个对象的内存地址 System.out.println("user1的内存地址:"+ VM.current().addressOf(user1)); System.out.println("user2的内存地址:"+ VM.current().addressOf(user2)); //测试如下,比较两个对象是否相等,能不能使用“==”? //这里的“==”判断的是:user1中保存的对象内存地址和user2保存的对象内存地址是否相等 System.out.println("user1==user2:"+(user1==user2)); //重写equals方法之前 System.out.println("重写equals方法之前:"+user1.equals(user2)); } }
运行结果:
true
user1的内存地址:30477892064
user2的内存地址:30477892136
user1==user2:false
重写equals方法之前:false
总结:判断对象是否相等,不能使用“==”,因为比较的是两个对象的内存地址。
覆写equals方法和hashCode方法要遵循的规则
如何覆写equals方法(步骤最好严格遵循)
- 入参为Object类型,变量名多为otherObject;
- 检查this和otherObject的引用指向的是否是同一个对象,同一个对象返回true,否则继续进行下面的步骤;
- 检查otherObject是否为null,是null则返回false,否则继续进行下面的步骤;
- 比较 this 与 otherObject 是否属于同一个类,用instanceOf或者getClass,不是同一个类则返回false,否则继续进行下面的步骤;
- 将otherObject强制转换为this的类型,然后一一进行各成员属性的比较
代码示例
重写equals方法
@Override
public boolean equals(Object obj) {
if(this==obj) return true;
if(obj==null || getClass()!=obj.getClass()) return false;
User user = (User) obj;
return Objects.equals(name,user.name) && Objects.equals(sex,user.sex);
}
执行代码:为重写hashcode
//重写equals方法之后
System.out.println("重写equals方法之后:"+user1.equals(user2));
//未重写hashcode方法之前
System.out.println("user1的hashcode:"+user1.hashCode());
System.out.println("user2的hashcode:"+user2.hashCode());
运行结果
重写equals方法之后:true
user1的hashcode:1177377518
user2的hashcode:1773206895
user1.equals(user2)方法已经返回true了,说明我们覆写的equals方法生效了,但hashCode方法返回的值还是不一样,由于存在equals和hashCode间的通用约定,因此我们覆写equals方法的同时,也要覆写hashCode方法
如何覆写hashCode方法
- 初始化一个int类型的result,这个result返回最终的hashCode值;
- 对类中的每个成员属性,进行下面的操作,并将结果累加起来一并组成result,操作如下:
- 将成员属性的值转化为int型,比如String类型的成员属性可以调用String里写好的hashCode方法;
- 将成员属性转化好的int类型的值乘以31,然后累加:result += intValue * 31;
-
如果该域是一个对象引用,并且该类的 equals 方法通过递归调用 equals 的方式来比较这个域,则同样对这个域递归调用 hashCode。如果要求一个更为复杂的比较,则为这个域计算一个 “规范表示”,然后针对这个范式表示调用 hashCode。如果这个域的值为 null,则返回 0(或者其他某个常数)。
-
如果该域是一个数组,则把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据下面的做法把这些散列值组合起来。
为什么 String hashCode 方法选择数字31作为乘子
代码示例
如果不重写hashcode,那么在Set或Map中会发生什么?
Set<User> userSet = new HashSet<>();
userSet.add(user1);
userSet.add(user2);
System.out.println("set的长度:"+userSet.size());
Map<User,String> userStringMap = new HashMap<>();
userStringMap.put(user1,"a");
userStringMap.put(user2,"a");
System.out.println("map的长度:"+userStringMap.size());
运行结果
set的长度:2
map的长度:2
两个User对象是相等的,所以说两个相等的对象放到Set里与Map里是不符合预期的,所以这就产生一个错误
Set区分两个对象是否相等:是看hashcode是否相等,然后再去判断两个对象的equals
Map根据key值的hashcode分配与获取对象数组保存的下标,然后再去根据equals方法去区分相等,也就是key是否唯一。
如果想要相同的User对象在Map或Set中只有一个,只有重写hashcode方法,为了解决这个问题,我们需要对User对象重写hashcode方法。
重写User对象hashcode
@Override
public int hashCode() {
int result = name!=null?name.hashCode():0;
result = 31*result+(sex!=null?sex.hashCode():0);
result = 31*result;
return result;
}
执行代码
//重写hashcode方法之后
System.out.println("user1的hashcode:"+user1.hashCode());
System.out.println("user2的hashcode:"+user2.hashCode());
运行结果
set的长度:1
map的长度:1
user1的hashcode:744668360
user2的hashcode:744668360
lombok实现equals方法和hashCode方法
可以看到上面介绍的覆写equals方法和hashCode方法很繁琐,如果我们每定义一个entity都要覆写这两个方法,一是麻烦,而是代码写的太冗余了,这时候插件lombok就发挥作用了,lombok支持我们在代码中通过注解来自动为我们的entity类创造很多基本方法,比如getter/setter,这些方法中包括了equals方法和hashCode方法,代码里具体使用如下
-
maven方式添加lombok项目依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency>
-
添加注解:@Data 和 @EqualsAndHashCode
import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(exclude = {"score","time"}) public class User { private String name; private Integer sex; public User(String name,Integer sex){ this.name = name; this.sex = sex; } public User(){} }
可以使用逗号分隔,排除不想比对的数据。
通过在实体类前加上@Data,@EqualsAndHashCode注解,即可为实体类自动生成equals方法和hashCode方法,执行结果如下:
重写equals方法之后:true
user1的hashcode:778429
user2的hashcode:778429equals方法返回结果为true,hashcode方法返回值相等,只是与我们自己写的方法返回的hash值不同,但是代码量大大减少了,有时间详细介绍一下lombok的使用。
比较两个对象调用顺序
- 先比较hashCode,如果不相等,则这两个对象不相等,相等则进入下一步;
- 调用equals方法,如果equal()也相同,则表示这两个对象是真的相同,否则不相等。
Java String equals()方法
-
equals()源代码
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String aString = (String)anObject; if (coder() == aString.coder()) { return isLatin1() ? StringLatin1.equals(value, aString.value) : StringUTF16.equals(value, aString.value); } } return false; }
总结:String类已经重写了equals方法,比较两个字符串不能使用“==”,必须使用equals。
-
代码示例
public class StringEqualsTest { public static void main(String[] args) { //大部分情况下,采用这样的方式创建字符串对象 String str1 = "hello"; String str2 = "world"; //实际上String也是一个类,不是基本类型,存在构造方法 String test1 = new String("test1"); String test2 = new String("test1"); //new两次,两个对象的内存地址不同 System.out.println("test1内存地址:"+ VM.current().addressOf(test1)); System.out.println("test2内存地址:"+ VM.current().addressOf(test2)); //“==”判断两个对象内存地址,不是内容 System.out.println("test1==test2: "+(test1==test2)); //比较两个字符串是否相等能不能使用==? //不能,必须使用equals() //String类已经重写equals() System.out.println("test1.equals(test2): "+test1.equals(test2)); } }
运行结果
test1内存地址:30476430520
test2内存地址:30476430592
test1==test2: false
test1.equals(test2):true
结论:
- java中什么类型的数据使用“==”判断
- Java中基本数据类型比较是否相等,使用==
- Java中什么类型的数据需要使用equals判断
- Java中所有的引用数据类型统一使用equals方法判断是否相等。