equals()和hashcode()是java.lang.Object中提供的用以对象比较的两个重要方法,下面是其定义及默认实现:
- public boolean equals(Object obj) { return (this == obj); }:用以判断变量参数与当前实例是否相等,JDK默认实现是基于对象内存地址是否相同,如果两个对象内存地址相同,则表示两个对象相同。
- public native int hashCode();: 默认情况下,该方法返回一个随机整数,对于每个对象来说该数字唯一,但该数字并非恒定,可能随着程序的执行发生变化。
equals() 与 hashCode() 使用契约
If two objects are equal according to the equals(Object) method, then calling the hashcode() method on each of the two objects must produce the same integer result
上面的话翻译成程序语言即是:
if obj1.equal(obj2) then:
obj1.hashCode() == obj2.hashCode()
在实际应用当中,JDK提供的默认实现可能无法满足实际业务场景,这时,我们就需要根据业务场景来重载hashCode和equals方法,但需谨记:当我们重载一个对象的equals方法,就必须重载他的hashCode方法,如果我们仅仅重载equals但没有重载hashcode,实际应用可能会带来潜在问题,接下我们示例说明:
示例说明
定义Student类
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.name = name;
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试主类HashcodeEquals,测试两个Student类实例是否相同
public class HashcodeEquals {
public static void main(String[] args) {
Student alex1 = new Student(1, "Alex");
Student alex2 = new Student(1, "Alex");
System.out.println("alex1 hashcode = " + alex1.hashCode());
System.out.println("alex2 hashcode = " + alex2.hashCode());
System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
}
}
输出如下:
alex1 hashcode = 1852704110
alex2 hashcode = 2032578917
Checking equality between alex1 and alex2 = false
上面的输出也很好理解:尽管两个实例属性完全相同,但根据JDK中equals的默认实现规则,他们的内存地址不同,因此,在equals的默认实现中,它们被认为是不相等的,这同样适用于hashCode —— JDK为每个实例生成一个随机的惟一哈希值
重载equals()方法
实际业务场景中,如果两个student实例ID相同,那么我们即可认为实例相同,因此,我们重载equals方法,重载规则如下:内存地址一致或者实例ID相同,代码如下:
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (!(obj instanceof Student))
return false;
if (obj == this)
return true;
return this.getId() == ((Student) obj).getId();
}
重新执行判断语句,结果如下
alex1 hashcode = 2032578917
alex2 hashcode = 1531485190
Checking equality between alex1 and alex2 = true
ArrayList中使用equals()方法
equals方法另一个常用业务是从一组实例中查找指定实例是否存在:
public class HashcodeEquals {
public static void main(String[] args) {
Student alex = new Student(1, "Alex");
List < Student > studentsLst = new ArrayList < Student > ();
studentsLst.add(alex);
System.out.println("Arraylist size = " + studentsLst.size());
System.out.println("Arraylist contains Alex = " + studentsLst.contains(new Student(1, "Alex")));
}
}
执行结果如下:
Arraylist size = 1
Arraylist contains Alex = true
重载hashCode()方法
通过上面的实例我们可以看到,通过重载equals方法,我们得到了我们期待的结果,即使是两个实例对象的哈希值并不相同,那么问题来了?重载hashCode方法的目的何在?
HashSet中使用equals方法
我们用一个例子来说明这个问题:
public class HashcodeEquals {
public static void main(String[] args) {
Student alex1 = new Student(1, "Alex");
Student alex2 = new Student(1, "Alex");
HashSet < Student > students = new HashSet < Student > ();
students.add(alex1);
students.add(alex2);
System.out.println("HashSet size = " + students.size());
System.out.println("HashSet contains Alex = " + students.contains(new Student(1, "Alex")));
}
}
运行上面的代码,我们得到如下输出:
HashSet size = 2
HashSet contains Alex = false
上述例子中,我们应该已经看出了问题,尽管我们已经重载了equals方法,理论上 alex1与 alex1应该是同一个对象,而HashSet是无法存储重复对象,但为什么JVM会认为他们是不同的对象。
这涉及到了HashSet的内部存储结构,在JDK中,HashSet将其内部元素存储在内存桶中。每个内存桶都关联一个特定的哈希值。在调用student.add(alex1)时,Java将alex1存储在一个内存桶中,并将其与alex1.hashCode()的值关联。后续,一旦插入相同哈希值的对象,那么插入对象将替换原有对象alex1。但是,由于alex2与alex1哈希值不同,因此被JVM视为完全不同的对象,将其存储在另外一个单独的内存桶中。
这就是重载hashCode的重要性,因此,我们按照业务场景重载hashCode方法,确保拥有相同ID的Student实例存储在同一个内存桶中,代码如下:
@Override
public int hashCode() {
return id;
}
之后,我们重写运行测试代码,执行结果如下:
HashSet size = 1
HashSet contains Alex = true
这就是hashCode的魅力所在,这两个实例被认为是一个对象并存储在同一个内存桶中,后续通过contains方法查找对象时,只要哈希值相同,对象即可被查到,这样适用于使用哈希机制存储对象的数据结构,如:HashMap, HashTable
结论
如果两个对象相同,则他们的哈希值(hashcode)一定相同
如果两个对象的哈希值相同(hashcode)相同,并不意味着他们是相同的。
对于使用Hash散列方式存储对象的数据结构:HashSet、HashMap、HashTable等,仅仅重载equals方法可能会导致实际业务逻辑失败
在比较两个对象时,仅重载hashCode方法并不能强制Java忽略内存地址。
https://dzone.com/articles/working-with-hashcode-and-equals-in-java