概述
在我们使用类集框架(比如使用hashMap、hashSet)的时候,经常会涉及到重写equals()和hashCode()这两个方法。
这两个方法的联系是:
1. 如果两个对象不同,那么他们的hashCode肯定不相等;
2. 如果两个对象的hashCode相同,那么他们也未必相等。
所以说,如果想在hashMap里面让两个不相等的对象对应同一个值,首先需要让他们的hashCode相同,其次还要让他们的equals()方法返回true,因此为了达到这个目的,我们就只能重写hashCode()和equals()这两个方法了。
引用一篇文章的解说:The idea behind a Map is to be able to find an object faster than a linear search. Using hashed keys to locate objects is a two-step process. Internally the Map stores objects as an array of arrays. The index for the first array is the hashcode() value of the key. This locates the second array which is searched linearly by using equals() to determine if the object is found.
大致意思就是:使用Map比线性搜索要快。Map存储对象是使用数组的数组(可以理解为二维数组,这并不准确,不过可以按照这个理解。之前hashMap用的是数组,每个数组节点对应一个链表,现在Java 8已经把链表改成了treeMap,相当于一个二叉树,这样检索的时候比链表更快,尤其是在最坏情况下,由原来链表的O(n)变成了二叉树的O(logn),详见https://dzone.com/articles/hashmap-performance),所以搜索大致是分为两步的,第一步是根据hashCode寻找第一维数组的下标,然后根据equals的返回值判断对象是第二维数组中的哪一个。
例证
举个栗子——
import java.util.HashMap;
public class Apple {
private String color;
public Apple(String color) {
this.color = color;
}
public static void main(String[] args) {
Apple a1 = new Apple("green");
Apple a2 = new Apple("red");
//hashMap stores apple type and its quantity
HashMap<Apple, Integer> m = new HashMap<Apple, Integer>();
m.put(a1, 10);
m.put(a2, 20);
System.out.println(m.get(new Apple("green")));
}
}
程序的运行结果为:null
此时,我们已经向hashMap里面存储两个对象了,且a1就是green Apple,那么为什么我们通过”green”去查找却返回null呢?
显然,后来我们新new出来一个对象,这和之前加入的a1绿苹果那个对象绝对不是同一个对象,根据终极父类Object中的hashCode()的计算结果,其返回值绝对是不一样的。
所以——
第一步:重写hashCode()
我们需要先让“凡是color属性相同的对象,其hashCode都一样”,所以我们可以这样重写hashCode():
public int hashCode(){
return this.color.length();
}
这里我们使用color属性的内容的长度作为hashCode的大小,那么凡是green的苹果,hashCode肯定都是5(green字符串长度为5),这样一来,属性相同的对象的hashCode肯定都相同了。这只是保证了一维数组的下标找到了(姑且这样理解),还需要找第二维的下标呢,这个需要在第二步中解决。在解决第二步之前,你可能会有问题——这样一来的话,如果有black苹果(假设有black),那么它的hashCode也变成了5了啊,和green一样了。这个同样靠第二步解决。
第二步:重写equals()
我们已经知道,如果想让两个对象一样,除了让他们的hashCode值一样外,还要让他们的equals()函数返回true,两个都符合才算一样。所以第二步我们要重写equals()函数,使“只要color一样,两个苹果就是相同的”:
public boolean equals(Object obj) {
if (!(obj instanceof Apple)) //首先类型要相同才有可能返回true
return false;
if (obj == this) //如果是自己跟自己比,显然是同一个对象,返回true
return true;
return this.color.equals(((Apple) obj).color); //如果颜色相同,也返回true
}
这样一来,根据最后一句话,凡是颜色相同的苹果,第二维也映射到同一个位置了(姑且这么理解)。这样一来,就可以根据颜色在hashMap里寻找苹果了。
把我们重写过的hashCode()和equals()加入到之前的代码中,便会输出结果:10,即键a1所对应的值。
结语
感觉这篇文章涉及的内容还是相当基础和重要的。文章到此也差不多可以结束了,另附上以前学习时记的笔记,感觉还是挺有用的,我自己的笔记自己看起来自然是毫无障碍的,不过实在不想整理了,就直接贴上来吧,大家将就将就看看吧,可以作为上面内容的唠叨和补充。
附录1
HashSet在存储的时候(比如存的是字符串),则存进去之后按照哈希值排序(也就意味着遍历的时候得到的顺序不是我们添加的顺序,即乱序),如果第二个对象和第一个Hash值一样但是对象不一样,则第二个会链在第一个后面。在添加对象的时候,add()返回boolean型,如果添加的对象相同(比如两个相同的字符串),则返回false,添加失败。
HashSet如何保证元素的唯一性?
通过元素的方法——hashCode()和equals()来实现。
如果两个元素的hashCode不同,直接就存了;
如果两个元素的hashCode相同,则调用equals判断是否为true,true则证明是同一个对象,就不存了,false的话证明是不同的对象,存之。
一旦自定义了对象,想要存进HashSet,则一定要覆写hashCode()和equals()方法——
比如我们定义Person类,仅含有name,age两个参数,规定:只要姓名和年龄相同,就断定为“同一个人”,则不能存入HashSet,否则的话可以。
对于这种情况,如果我们new出来几个人,其中存在名字和年龄相同的,则均会存入HashSet,原因就是这些对象是不同的对象,所占内存不一样,则通过hashCode()返回的哈希值也都不一样,所以理所当然的存入了HashSet。为了避免把“同一个人”存进HashSet,我们首先需要让hashCode()针对“同一个人”返回相同的哈希值,即覆写hashCode()方法!
public int hashCode(){
return 110;
}
这样自然也可以,不过没有必要让所有的对象返回的哈希值都一样,只要“同一个人”的哈希值一样就行了,所以写成这样更好:
public int hashCode(){
return name.hashCode() + age;
}
这样的话“同一个人”返回的哈希值就是相同的。不过这样还是不够完美,因为覆写的这个hashCode()虽然会让“相同的人”返回相同的哈希值,但也可能会让“不同的人”返回相同的哈希值,比如两个人name不同,age不同,但name的哈希值加上age恰恰相同,这样的话就坑爹了。为了避免这种现象,让这种哈希值恰巧撞上的概率进一步减小,我们写成这样会更好:
public int hashCode(){
return name.hashCode() + age * 19;
}
最好是乘上一个素数之类的,可以大大降低“不同的人”的哈希值撞上的概率。
通过以上hashCode()函数的覆写,我们让“相同的人”的哈希值相同了,那么接下来就要覆写equals()函数!因为Java碰到哈希值相同的情况之后,接下来要根据equals()函数判断两个对象是否相同,相同则不再存入HashSet,不同的话就存进去,且是链在和它哈希值相同的对象上的(链成一串儿)。
附录2:以下是TreeSet内容,和上面关系不大了
TreeSet可以对里面的元素进行排序,比如如果对象是字符串,则按照字典序排序。
如果要存自定义对象,需要让自定义的对象具有比较性,这样的话TreeSet才能将其按照一定的顺序去排序。否则会报出异常。为了让自定义对象能够具有比较性,对象需要实现Comparable接口。
比如,有一个Person类,我们要new出来一些人放到TreeSet里面,为了使对象具有可比性从而能够存入TreeSet,我们规定按照对象的年龄排序。
首先,让Person类实现Comparable接口,覆写接口的compareTo()方法,其返回值和c语言的strcmp()相同:
这样的话,如果某两个对象的年龄相同,则后来者将不会被存入TreeSet,因为后来者被认为和之前的那个是同一个对象。
所以我们对主关键字排序之后一定要对次关键字进行排序,只有所有的关键字都比较完毕还是返回0,我们才能认为两个对象相同。
所以覆写的compareTo()应该是这样的:
public int compareTo(Object obj){ //传进来的需要是Object类型,这一点要注意
if(!(obj instanceof Person)){ //传进来的对象不对,直接抛出异常
throw new RuntimeException("Not The Same Kind Of Object!");
}
Person p = (Person)obj; //将对象向下转型
if(this.age < p.age)
return -1;
if(this.age > p.age)
return 1;
if(this.age == p.age)
{
return this.name.compareTo(p.name); //String类中覆写过Comparable接口的空方法compareTo(),按照字典序对字符串进行排序
}
}
因此,如果你想让TreeSet按照输入顺序存数据,而不是自动排序,可以这样覆写compareTo()方法:
public int compareTo(Object obj){
return 1;
}
很简单,不过缺陷就是“相同的人”也会被存进去。这个函数是完全按照输入内容的顺序不加以任何删改原模原样原顺序存进TreeSet的。
同理,如果return -1就是输入顺序的逆序;如果return 0则只能存入第一个输入的对象。
以上是TreeSet排序的第一种方法——让元素自身具有比较性(让类实现Comparable接口,覆写compareTo()方法)。
不过这种方法有缺陷,比如我突然不想按照年龄排,想按照姓名排序,这就需要重新修改代码了。
所以TreeSet排序的第二种方法——让集合自身具有比较性(将自定义的比较器传入Treeset的构造函数)。
比如我们现在要按照人名字典序排序:
首先建造一个比较器,实现Comparator接口,覆写compare()方法(返回值也和strcmp()一样):
public PersonCompareMethod implements Comparator{
public int compare(Object obj1, Object obj2){ //传入两个Object对象
Person p1 = (Person)obj1; //向下转型
Person p2 = (Person)obj2;
int num = p1.getName().compareTo(p2.getName()); //直接比较两个字符串的字典序,因为String类已经覆写过Comparable接口的空方法compareTo(),按照字典序对字符串进行排序
if(num == 0){
return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge()));
//这一句话比较巧妙!!!本来我们是可以直接按照数字大小比较年龄这个次要关键字的,不过在比较的时候,我们换了一种比较高端的方法:新建了两个Integer对象,因为Integer类里面也有compareTo()方法,将数字按照字典序进行比较。当然是实现了Comparable接口并覆写compareTo()方法才具有这样的功能
}
return num;
}
}
之后,将我们的构造器传入TreeSet新建对象时调用的构造函数即可:
TreeSet ts = new TreeSet(new MyCompareMethod());
使用第二种方式的情况:
对象不具有比较性,或者是比较性并不是我们所需要的。
版权声明:本文为博主原创文章,未经博主允许不得转载。