概述
在我们使用类集框架(比方使用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;
}
非常easy,只是缺陷就是“同样的人”也会被存进去。这个函数是全然依照输入内容的顺序不加以不论什么删改原模原样原顺序存进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());
使用另外一种方式的情况:
对象不具有比較性。或者是比較性并非我们所须要的。