1. 引言
在学习ADT的等价性这一章节中,我们学习到了不同的等价性的判定原理,包括不可变类型的等价性,可变类型的等价性,观察等价性,行为等价性等等。我们知道,对于可变类型的ADT,我们最好保留默认的equals与hashcode函数(继承自Object类),这是因为可变的equals与hashcode会导致包含此类元素集合的RI被破坏。而对于不可变类型的ADT,我们要求必须重写equals与hashcode,这是因为"=="仅能判断其引用等价性,而无法比较基于AF的对象等价性。
2. 原理 / 分析
那么,我们重写equals与hashcode,仅仅是为了我们编写程序的时候使用equals来判断两个对象是否“相等”吗?当然不是。在Java的集合框架(JCF)中,这两个函数被大量使用到。首先,我们可以看看JCF中的一些内容。JCF包括Collection体系集合与Map体系集合,前者又包含List,Set以及它们的具体实现,后者则是Map的一些具体实现。它们的共同点就在于“集合”这个概念,它们都包含了一些数据结构对象。那么,基本的增删改查是少不了的,而增删改查中又必然会使用到“Compare"(注意这里我说的compare并非指实现Java里的比较接口,而单指字面意义上的是否相等的比较),也就是说,这里的元素必须都是可比较的。比如下面一段代码:
package Test;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
public class Testscaaner {
public static void main(String[] args) {
Set<Person> aList = new HashSet<Person>();
Person aPerson = new Person("Tom");
Person bPerson = new Person("Tom");
aList.add(aPerson);
aList.add(bPerson);
System.out.println(aPerson.equals(bPerson));
System.out.println(aList.size());
}
}
class Person{
private String name;
public Person(String name) {
// TODO Auto-generated constructor stub
this.name = name;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(!(obj instanceof Person)) {
return false;
}
if(((Person)obj).name.equals(name)) {
return true;
}
return false;
}
}
这段代码表面上没有使用equals进行比较,但实际上仍然进行了比较操作。为什么呢?我们知道HashSet类会确保集合中没有一样的元素,再添加元素的时候,就会使用equals进行比较来决定是否添加。看到这里我们可能就会觉得,既然Person类重写了equals方法,改成了如果姓名相同即对象等价,那么aPerson就与bPerson等价,那这里的aList中就只应该有一个元素吧?
然而输出却是两个:
true
2
我们再来看下面的代码:
package Test;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
public class Testscaaner {
public static void main(String[] args) {
Set<Person> aList = new HashSet<Person>();
Person aPerson = new Person("Tom");
Person bPerson = new Person("Tom");
aList.add(aPerson);
aList.add(bPerson);
System.out.println(aPerson.equals(bPerson));
System.out.println(aList.size());
}
}
class Person{
private String name;
public Person(String name) {
// TODO Auto-generated constructor stub
this.name = name;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(!(obj instanceof Person)) {
return false;
}
if(((Person)obj).name.equals(name)) {
return true;
}
return false;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return name.hashCode();
}
}
现在输出确实是1了:
true
1
这就是我们要注意的点,也是为什么equals要与hashcode配套修改使用。当仅仅修改其中的一个函数时,是不会起作用的。事实上 ,由于HashSet是由哈希表实现的,我们可以理解为它会维护一个数组,记录每个储存在里面元素的hashcode,当试图向其中插入时,会先检查这个数组,如果hashcode存在,就不会插入。如果不存在,就会比较hashcode所指向的位置的元素与待插入元素,决定是否插入。在上述代码中由于hashcode不同,放置的位置不同,故不会产生比较,使得单个equals起不了作用。也就是存储过程(访问过程(包括remove)):先计算hashCode,再equals,想改变存储依据,既要改变hashCode也要重写equals(因为如果只改变equals,第一步计算hashCode就不一样,根本轮不到equals)。