在学习散列集集或者图(Map)时,我们也许对这两个方法并不陌生:equals()和hashCode();添加到散列集中的对象必须以正确地方式实现hashCode()方法才能确保集合中不出现重复的值。最近在阅读《Thinking in Java》这本书时,里面有对其系统地介绍,现将其总结一下:
在开发中,我们对于HashMap的使用并不陌生,但有时却也会犯错。我们先看一个例子:
//定义人这个类,设定一个编号
class Person {
private int cardId;
public Person(int cardId) {this.cardId= cardId;}
@Override
public String toString() {
return "Person#" + cardId; //返回编号
}
}
//人的性别
class Sex {
private int i = (int) (Math.random() * 2);
@Override
public String toString() {
return i == 0? "male" : "female"; //随机返回一个性别
}
}
public class TestMap {
public static void main(String[] args) throws Exception{
detectSex(Person.class);
}
public static <T extends Person> void detectSex(Class<T> type) throws Exception {
Constructor<T> person = type.getConstructor(int.class);
Map<Person, Sex> map = new HashMap<>(); //创建一个HashMap用于存放人和性别的键值对
for (int i = 0; i < 10; i++) {
map.put(person.newInstance(i), new Sex());
}
System.out.println("map:" + map);
Person p = person.newInstance(3); //创建一个编号为3的人,并在map中查找此人性别
System.out.println("查找某人性别" + p);
if (map.containsKey(p))
System.out.println(map.get(p));
else
System.out.println("无此人");
}
}
输出结果:
map:{Person#7=female, Person#0=female, Person#9=male, Person#3=female, Person#5=male, Person#8=female, Person#2=male, Person#4=male, Person#1=male, Person#6=male}
查找Person#3性别
无此人
这个程序看起来简单,但却在结果上出现了错误。原因在于Person默认继承于Object类,所以使用了Object类的hashCode()方法来生成散列码(根据地址并通过一定的算法产生)。明显,第一次创建的和第二次创建的Person#3地址不同,散列码是不同的,因此无法找到此人。所以,我们要以正确地形式实现hashCode()方法。当然,在覆盖hashCode()方法时,也要覆盖equals()方法,因为这个方法也来自Object类,也需要以正确地方式实现。
这里提到equals()方法时,我们必须要注意以下几点:
1)自反性 对于任意x, x.equals(x)必定返回true。
2)对称性 对于任意x,y,x.equals(y)与y.equals(x)的返回值相同。
3)传递性 对于任意x,y,z,如果x.equals(y)与y.equals(z)返回值相同,那么x.equals(z)返回值也相同。
4)一致性 对于任意的x,y,无论x.equals(y)执行多少次,返回值要么是true,要么为false。
5)对于任意x != null, x.equals(null)返回false。
(作为学生党第一次感觉自己学的离散结构知识用上了T-T)
那么有了这些理念后再修改一下Person类:
class Person {
private int cardId;
public Person(int cardId) {this.cardId= cardId;}
@Override
public String toString() {
return "Person#" + cardId;
}
@Override
public boolean equals(Object obj) {
return (obj instanceof Person && (((Person) obj).cardId == cardId));
}
@Override
public int hashCode() {
return cardId; //把唯一的编号作为散列码
}
}
再运行看结果:
map:{Person#0=male, Person#1=male, Person#2=female, Person#3=female, Person#4=male, Person#5=female, Person#6=male, Person#7=male, Person#8=female, Person#9=female}
查找Person#3性别
female
如果我们比较时至比较编号,并且把编号作为散列码,这样子覆盖hashCode()和equals()方法程序就正常输出结果了。
在我们了解散列的实现方式之前,首先要知道散列的意义是什么:散列不仅能够方便在某些情况下,如(Map),通过一个对象来查找另一个对象,它的价值更在于速度,即散列能使查询得以快速进行。
那么散列是如何实现的呢?我们知道存储一组元素最快的数据结构是数组,因此使用了数组来存储键的信息,该数组形成的表又称散列表,哈希表。数组并不保存键本身,而是保存通过算法将键对象转换过来的一个数字,而这个数字作为数组的下标,这个数字称为散列码。就像上面Person类中,覆盖了hashCode()方法,这个方法称为散列函数,用于产生散列码。
那么我们可能会想,如果说当前有很多个对象,即有很多键,数组又是固定大小的,会不会出现存储不下的现象?答案是这样的:不同的键值可以产生相同的下标,可能会有冲突(如果没有冲突,那么这个散列函数称为完美散列函数)。那么怎么样解决冲突呢?
解决方案是这样子的:冲突通常由外部链接处理,数组并不保存值,而是保存一个list,然后对list中的值使用equals()方法来查询(显然这里的查询会慢一点)。如果我们存储的数值比较少,不产生冲突的话,那么就可以直接通过数组下标确定位置,索引的速度就会相当快(这也是HashSet,HashMap索引快的原因)。我们可以看一个图理解一下:
我们知道,当我们执行下面的语句时:
String s = new String("scau");
String s1 = new String("scau");
这里会产生三个对象,如图所示:
显然,当我们用”==”比较两个String对象是返回false,即两个对象是不相等的。但我们用equals()比较时,却能返回true(可以得知String覆盖了equals()方法)。但是它们却共同映射到同一块内存区域,它们的hashCode()产生相同的值。我们可以测试一下:
public static void main(String[] args) {
String s = new String("scau");
String s1 = new String("scau");
System.out.println(s.hashCode() == s1.hashCode());
}
结果:true
Bingo!