Java hashCode 与 equals 方法详解
Java hashCode 与 equals 方法详解
1、简介
我们知道Object是所有类的父类,所有的对象在不重写的情况下使用的是Object的equals方法和hashcode方法,从Object类的源码我们知道,默认的equals 判断的是两个对象的引用指向的是不是同一个对象;而hashcode也是根据对象地址生成一个整数数值;
2、hashCode介绍
先用一张图看下什么是Hash?
Hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。关于散列值,有以下几个关键结论:
- 如果散列表中存在和散列原始输入K相等的记录,那么K必定在f(K)的存储位置上。
- 不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞。
- 如果两个Hash值不同(前提是同一Hash算法),那么这两个Hash值对应的原始输入必定不同。
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int 整数。这个散列码的作用是确定该对象在散列表中的索引位置。hashCode() 定义在 JDK 的 Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。另外我们可以看到 Object 的 hashcode() 方法的修饰符为native, 表明该方法是操作系统实现,java调用操作系统底层代码获取哈希值。
hashcode() 它是一个本地方法,它的实现与本地机器有关。当我们向一个集合中添加某个元素,集合会首先调用 hashCode 方法,这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存。若该处已经有元素存在,就调用 equals 方法来匹配这两个元素是否相同,相同则不存,不同则散列到其他位置。这样处理,当我们存入大量元素时就可以大大减少调用 equals() 方法的次数,极大地提高了效率。
数组是java中效率最高的数据结构,但是“最高”是有前提的。第一我们需要知道所查询数据的所在索引位置。 第二:如果我们进行迭代查找时,数据量一定要小,对于大数据量而言一般推荐集合。
在 Java 集合中有两类,一类是 List,一类是 Set, 他们之间的区别就在于 List 集合中的元素是有序的,且可以重复,而 Set 集合中元素是无序不可重复的。对于 List 好处理,但是对于 Set 而言我们要如何来保证元素不重复呢? 通过迭代来 equals() 是否相等。数据量小还可以接受,当我们的数据量大的时候效率可想而知(当然我们可以利用算法进行优化)。比如我们向 HashSet 插入 1000 数据,难道我们真的要迭代 1000 次,调用 1000 次 equals() 方法吗?hashCode 提供了解决方案。
所以 hashCode 在上面扮演的角色为寻域(寻找某个对象在集合中区域位置)。hashCode 可以将集合分成若干个区域,每个对象都可以计算出他们的 散列码,可以将 散列码分组,每个分组对应着某个存储区域(散列表),根据一个对象的 散列码就可以确定该对象所存储区域,这样就大大减少查询匹配元素的数量,提高了查询效率。
3、hashCode 对于一个对象的重要性
hashCode 重要么?对于 List 集合、数组而言不重要,但是对于 HashMap、HashSet、HashTable 而言,它变得异常重要。所以在使用 HashMap、HashSet、HashTable 时一定要注意 hashCode。对于一个对象而言,其 hashCode 过程就是一个简单的 Hash 算法的实现,其实现过程对你实现对象的存取过程起到非常重要的作用。
Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法
初始化一个整形变量,为此变量赋予一个非零的常数值,比如 int result = 17;
选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:
(1) 如果是 boolean值,则计算 f ? 1 : 0
(2) 如果是 byte\char\short\int,则计算(int)f
(3) 如果是 long值,则计算(int)(f ^ (f >>> 32))
(4) 如果是 float值,则计算Float.floatToIntBits(f)
(5) 如果是 double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
(6) 如果是对象引用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0
(7) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
java.util.Arrays.hashCode(long[])的具体实现:
public static int hashCode(long a[]) {
if (a == null)
return 0;
int result = 1;
for (long element : a) {
int elementHash = (int)(element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
对于涉及到的各个字段,采用第二步中的方式,将其依次应用于下式:
result = result * 31 + [hashCode];
5、hashCode() 重写固定模板
故总结出hashCode()重写的固定模板如下:
/**
* 重写hashCode方法
*/
@Override
public int hashCode() {
int result = 17;
// boolean 类型
result = 31 * result + (this.mBoolean == flase ? 0 : 1);
// int 类型
result = 31 * result + this.mInt;
// float 类型
result = 31 * result + Float.floatToIntBits(this.mFloat);
// long 类型
result = 31 * result + (int)(this.mLong ^ (this.mLong >>> 32));
// double 类型
result = 31 * result + Float.valueOf(Double.doubleToLongBits(this.mDouble)).hashCode();
// String 类型
result = 31 * result + (this.mString == null ? 0 : this.mString.hashCode());
// Object 类型
result = 31 * result + (this.mObj == null ? 0 : this.mObj.hashCode());
return result;
}
6、如何重写equals()方法
重写覆盖父类Object.equals()方法,五步走:
- 1.判断引用地址是否相同
- 2.判断引用地址是否为空
- 3.确认对象类型是否一致
- 4.转型 - 向下转型拆箱
- 5.比较对象中的实际内容
@Override
public boolean equals(Object obj) {
// 相同判断
if (this != obj) {
return false;
}
// null判断
if (obj == null) {
return false;
}
// 类型一致判断
if (this.getClass() != obj.getClass()) {
return false;
}
// 拆箱操作(类型一致)
MyUser w = (MyUser)obj;
// 比较内容(比较所有成员的值|或者比较你认为对象相等的条件,对象中的字段相比较)
if (this.toString().equals(w.toString())) {
return true;
}
return false;
}
7、hashCode 与 equals 总结
简而言之就是:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
8、为什么重写Object的equals(Object obj)方法尽量要重写Object的hashCode()方法
@Data
public class MyUser {
private String name;
private Integer sex;
private String date;
public MyUser() {}
public MyUser(String name) {
this.name = name;
}
public MyUser(String name, Integer sex) {
this.name = name;
this.sex = sex;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MyUser)) {
return false;
}
MyUser myUser = (MyUser) o;
return Objects.equals(getName(), myUser.getName()) && Objects.equals(getSex(), myUser.getSex());
}
}
只要,MyUser对象的 name、sex 的equals 方法相同, 我们就认为这个对象相同。我们只重写了 Object 的equals方法, 没有重写hashCode方法, 下面我们往Set集合添加数据测试:
public static void main(String[] args) {
MyUser myUser1 = new MyUser("1");
MyUser myUser2 = new MyUser("2");
MyUser myUser3 = new MyUser("1");
MyUser myUser4 = new MyUser("2");
MyUser myUser5 = new MyUser("3");
MyUser myUser6 = new MyUser("4");
HashSet<MyUser> myUsers = new HashSet<>();
myUsers.add(myUser1);
myUsers.add(myUser2);
myUsers.add(myUser3);
myUsers.add(myUser4);
myUsers.add(myUser5);
myUsers.add(myUser6);
System.out.println("myUsers.size()==" + myUsers.size());
}
打印结果:
myUsers.size()==6
下面我们重写hashcode方法:
// 省略以上代码......
@Override
public int hashCode() {
return Objects.hash(getName(), getSex());
}
再次测试如下: