equals和hashCode方法详解

时间:2021-02-01 16:04:46

equals和hashCode方法详解

在Java.lang.Object类中有两个非常重要的方法:

public boolean equals(Object obj)
public int hashCode()

Object类是类继承结构的基础,所以是每一个类的父类,所有的对象,包括数组,都实现了Object类中定义的方法。


1.equals方法详解

equals方法是用来判断其他的对象是否和该对象相等。

equals方法在Object类中定义如下:

public boolean equals(Object obj){
return (this == obj);
}

很明显是对两个对象的地址值进行比较(即比较引用是否相同)。但是我们都知道,String、Math、Integer、Double等这些封装类在使用equals方式时已经覆盖了Object类的equals方法。在String类中的equals方法如下:

public boolean equals(Object anObject) {  
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n– != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}

很明显这是进行内容的比较,而已经不再是地址的比较,一次类推Math、Integer、Double等这些类都是重写equals方法的,从而进行的是内容的比较,当然基本类型是进行值的比较。它的性质有:

  • 自反性,对于任意不为null的引用值x, x.equels(x)一定是true。
  • 对称性,对于任意不为null的引用值x,y,当且仅当x.equels(y)是true时,y.equels(x)也是true
  • 传递性,对于任意不为null的引用值x,y,z,如果x.equels(y)是true同时y.equels(z)是true,那么x.equels(z)一定是true
  • 一致性,对于任意不为null的引用值x,y,如果用于equals比较的对象信息没有被修改的话,那么多次调用x.equels(y)要么一直的返回true,要么一致的返回false。
  • 对于任意不为null的引用x,x.equels(null)返回false。

对于Object类来说,equals方法在对象上实现的差别可能性最大的的等价关系,即,对于任意非引用值x,y当且仅当x和y引用的是同一个对象,该方法才会返回true。

需要注意的是当equals方法被override时,hashCode也要被override,按照一般hashCode方法的实现来说,相等的对象他们的hash code 一定相等。


2.hashCode方法详解

hashCode方法给对象返回一个hash code值。这个方法被用于hash tables,例如HashMap。

它的性质是:

  • 在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象多次调用hashCode方法,该方法必须始终如一的返回同一个Integer。
  • 如果两个对象根据equals方法是相等的,那么调用二者各自的hashCode方法必须产生同一个Integer结果
  • 并不要求根据equals方法不相等的两个对象,调用二者各自的hashCode方法必须产生不同的Integer结果,然而程序员应该意识到对于不同的对象产生不同的Integer结果,有可能会提高hashtable的性能。

大量实践表明,由Object类定义的hashCode方法对于不同的对象返回不同的Integer。在Object类中hashCode的定义如下:

public native int hashCode();

说明这是一个本地方法,它的实现是根据本地及其相关的,当然我们可以在自己写的类中覆盖hashCode方法,比如String、Integer、Double等这些类都是覆盖了hashCode方法,例如在String类中定义的hashCode方法如下:

public int hashCode() {  
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31 * h + val[off++];
}
hash = h;
}
return h;
}

想要明白hashCode的作用必须要先知道Java中的集合。

总的来说Java中的集合Collection有两类,一类是list,另一类是Set,前者集合内的元素是有序的,元素可以重复,后者元素是无序的,但是元素不可以重复。那么要想保证元素不重复,可两个元素是否重复应该依据什么来判断

这就是Object.equels方法了,但是如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了,也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法,这显然会大大的降低效率。

于是Java采用了哈希表的原理,hash实际是人名,由于他提出哈希算法的概念,所以就以它的名字命名了,哈希算法也称为散列算法,是将数据依特定的算法直接指定到一个地址上,初学者可以简单的理解,hashCode方法实际上返回的就是对象存储的物理地址(实际上可能并不是)。

这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上,如果这个位置上没有元素,它就可以直接存储在这个位置,不用再进行任何比较了,如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不同的话就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

简而言之,在集合查找时,hashCode能大大的降低对象比较次数,提高查找效率!

Java对象的equals方法和hashCode方法是这样规定的;

  • 1.相等的对象必须具有相等的哈希码
  • 2.如果两个对象的hashCode相同,他们并不一定相同

以下是Object对象API关于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.
  • It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.

关于第一点,相等的对象必须具有相等的hash码,为什么?

想象以下,假如两个Java对象A和B,A和B相等(A.equels(B)=true),但是A和B的哈希码不同,则A和B存入HashMap时的哈希码计算得到的HashMap内部数组位置索引可能不同,那么A和B很有可能同时存入HashMap,显然相等的元素是不允许同时存入HashMap,HashMap不允许存放重复的元素。

关于第二点,两个对象的hashCode相同,它们并不一定相同

也就是说,不同的对象的hashCode可能相同,假如两个Java对象A和B,A和B不等(A.equels(B)=flase)但是A和B的哈希码相等,将A和B都存入HashMap时会发生哈希冲突,也就是A和B存放的HashMap内部数组的位置索引相同,这是HashMap会在该位置建立一个链表,将A和B串起来放在该位置,显然,该情况不违反HashMap的使用原则,是允许的,当然,哈希冲突越少越好,尽量采用好的hash算法以避免哈希冲突。

所以Java对于equals方法和hashCode方法是这样规定的:

  • 1.如果两个对象相同,那么他们的hashCode值一定要相同
  • 2.如果两个对象的hashCode相同,他们并不一定相同(这里说的对象相同指的是equals方法比较),如果不按要求去做了,你会发现相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降
  • 3.equals相等的两个对象,hashCode一定相等,equals不等的两个对象并不能证明它们的hashCode不相等。

在Object类中,hashCode方法是本地方法,返回的是对象的地址值,而Object类中的equals方法比较的是两个对象的地址值,如果equals方法相等,说明两个对象的地址值也相等,当然hashCode方法返回的值也就相等了,在String类中,equals方法返回的是两个对象内容的比较,当两个对象内容相等时,hashCode方法根据String类的重写代码分析,也可知道hashCode返回结果也会相等,以次类推可以知道Integer、Double等封装类中经过重写的equals和hashCode方法也同样适合于这个原则,当然没有重写的类,在继承Object类的equals和hashCode方法后,也会遵守这个原则。


3.HashSer、HashMap、HashTable与hashCode和equals方法的密切关系

HashSet是继承Set接口,Set接口又实现Collection接口,这层次关系,那么HashSet、HashTable、HashMap中的存储操作是根据什么原理来存取对象的呢?

我们以HashSet为例,我们都知道HashSet中不允许出现重复对象,元素的位置也是不确定的,在HashSet中又是如何去判定元素重复的呢,在Java的集合中,判断两个对象是否相等的规则是:

1.判断两个对象的hashCode是否相等

如果不相等,认为两个对象也不相等,完毕,如果相等转入2进行判断(这一点只是为了提高存储效率而要求的,其实理论上也可以没有,但如果没有,实际使用效率会大大降低,所以我们这里将其作为必需的)

2.判断两个对象用equals运算是否相等

如果不相等,认为两个对象不相等,如果相等,认为两个对象相等(equals方法是判断两个对象是否相等的关键)。

例如1:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class HashSetDemo{

public static void main(String[] args){
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
Set hashSet = new HashSet<>();
hashSet.add(s1);
hashSet.add(s2);
Iterator it = hashSet.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
}
}

运行的结果是:

equals和hashCode方法详解

这是因为String类已经重写了equals和hashCode方法,所以HashSet认为他们是相等的,进行了重复添加。

例如 2:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class HashSetDemo{
public static void main(String[] args){
Set hashSet = new HashSet<>();
hashSet.add(new Ren(1, "lisi"));
hashSet.add(new Ren(2, "zhangsan"));
hashSet.add(new Ren(1, "lisi"));
hashSet.add(new Ren(1, "lisi"));
Iterator it = hashSet.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
}
}
class Ren{
String name;
int age;

Ren(int age, String name){
this.name = name;
this.age = age;
}

@Override
public String toString(){
return age + ":" + name;
}
}

运行结果:

equals和hashCode方法详解

可以看到HashSet添加了相等的元素,这是不是和HashSet的原则违背了吗,答案是否定的,因为在根据hashCode()对三次建立的new Ren(1, “lisi”)对象进行比较时生成了不同的哈希码值,所以HashSet把它当做不同的对象对待了,当然此时equals方法返回的值也不等。

为什么会产生不同的哈希码值,上面我们在比较s1和s2的时候不是产生了同意的哈希码吗,原因就是在我们自己重写的Ren类并没有重写自己的hashCode和equals方法,所以在进行比较时,是继承了Object类中的hashCode方法,而Object类中的hashCode方法是一个本地方法,比较的是对象的引用地址,使用new方法创建对象,两次生成的当然是不同的对象了,造成的结果就是两个对象的hashCode方法返回值不一样,所以HashSet会把他们当成不同的对象对待,如果要解决这个问题,那么需要在我们自己写的类中重写hashCode和equals方法。

class Ren{
String name;
int age;

Ren(int age, String name){
this.name = name;
this.age = age;
}

@Override
public int hashCode(){
return age * name.hashCode();
}

@Override
public boolean equals(Object obj){
Ren r = (Ren) obj;
return name == r.name && age == r.age;
}

@Override
public String toString(){
return age + ":" + name;
}
}

再次运行结果:

equals和hashCode方法详解

可以看到重复的元素已经消除了,根据重写的方法,即便是三次调用了new Ren(1, “lisi”),我们在获取对象的哈希码时,根据重写的方法hashCode方法,获取哈希码肯定是一样的,当然根据equals方法我们也可以判断是相同的,所以我们在向HashSet集合中添加他们时被当做重复元素看待了。


4.重写equals()和hashCode()小结

  • 1.重写是equals,重写hashCode只是技术要求(为了提高效率)
  • 2.为什么要重写equals呢,因为Java的集合框架中,是通过equals来判断两个对象是否相等的
  • 3.在hibernate中,经常使用Set集合来保存相关对象,而Set集合时不允许重复的,在向HashSet集合中添加元素时,其实只要重写equals方法一条就可以了,但是HashSet中元素比较多时,或者重写的equals方法比较复杂时,我妈们只用equals方法进行比较判断,效率也会非常低,所以引入了hashCode这个方法,只是为了提高效率,且这是非常有必要的。