一文讲透hashCode和equals方法

本文主要回答一下几个问题,意在消除所有关于HashCode和equals方法的模糊地带,彻底掌握这个知识点,虽然HashCode和equals是Java中的基础概念但是包含的内容却一点也不少,所谓基础不牢,地动山摇,所以大家还是重视。

0.到底什么是hashCode,什么又是equals,跟==有什么区别?

1.Java为什么要设计hashCode和equals?

2.为什么比较对象的时候一定要重写hashCode和equals?

3.hashCode是怎么算出来的?

4.String的equals源码剖析。

我们先讲一下为什么要设计hashCode,其实hashCode的诞生就是为Java中的集合服务的,例如Map和Set。

我们都知道HashMap是一种基于键值对形式的高效存储方式(底层采用数组加链表),那么HashMap是如何保证键的唯一性的,其实就用到了hashCode,而另一个我们熟知的HashSet,它其中的元素是不重复的,那如何判断放入的元素是不重复的呢?其实也是用了hashCode。(下面会解释)。

先来看看hashCode到底是什么?简单来说,hashCode就是根据某种hash算法得到的int类型的值,哈希即散列,是一种高效的数据结构(不熟悉这种数据结构的建议先百度补课),而hash算法的目的就是尽量减少哈希冲突,尽量使每个对象都能有自己的hash值,而这个值就是hashCode,就好像一个人的身份证一样,唯一标识着一个人的身份,因此同一个对象调用多次hashCode一定是一样的。

那么这种hash算法究竟是怎么算出来的,为什么能够尽可能的避免了哈希冲突呢?

我们来看源码:

HashCode

首先可以看到Object类中的hashCode是native方法,jdk底层有些是用C/C++写的,因此我们追溯到C++源码上(如何查看native方法的源码自行百度,这里不是重点)。

C++源码

再追踪核心代码ObjectSynchronizer的源码

ObjectSynchronizer

可以看到代码多次出现了指针,可以判断hashCode其实就是根据对象的地址进行相关的计算得到的,而这种计算必须具体到某一个对象上,重写了hashCode方法之后才能知道到底进行了怎样的运算,下面我们来举个简单的例子:

360截图17290508589593

在eclipse中我们定义一个Peson类,有一个age属性,然后利用IDE工具直接自动生成hashCode方法,重写Object类中的hashCode方法。这里我们可以看到首先生成一个prime=31,定义一个result,然后下面利用prime乘result加上age计算完成后进行返回。

这里就必须要面对一个问题,为什么要定义一个final的prime值,而且是31,这就涉及到计算机组成原理方面的知识。

在《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:

之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。

这里大家不必深究下去,具体为何选择31其实是数学家和统计学家要考虑的问题,我们只需要知道这个31主要是为了计算机底层的效率就行。

那为何比较对象的时候要重写hashCode呢?

我们先来回顾一下比较两个对象和基本数据类型的知识。

360截图17290508417876

如图,==是用来判断地址的,如果是基本数据类型a==b返回的是true,这是因为基本数据类型是在栈中存储的,因此值相等,则必定相等。而p1==p2返回的则是false,是因为new出来的对象地址是在堆内存中,每一次new出来的对象都占特定的堆内存,因此地址必定不相同。

那么如果我这里没有重写equals方法,来用equals比较

System.out.println(p1.equals(p2));

会发现依然会返回false,这是因为没有重写equals,对象会默认调用Object类的equals方法,源码如下:

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

可以发现Object默认还是用==判断,所以如果要比较对象的内容是否相等,切记一定要重写equals方法,只有这样才能返回true。

那hashCode和equals方法有什么联系?

简单来说如果两个对象equals相等,则它们的hashcode必须相等,而hashcode相等,equals则不一定相等。就好比,4个数,前两个数和后两个数相等,则前两数的和与后两数的和一定相等,但前两数的和与后两数的和相等,则前两数和后两数不一定相等,2+3=5,1+4=5。原因在于,前面我们说过hashCode是尽量保证唯一,尽量平均分布,但由于不可避免地会存在哈希值冲突的情况,此时两个对象即便hashCode相等,equals也不一定相等,而equals相等hashCode必定相等是跟String的源码息息相关的,我们最后解释。

而对象重写equals方法,它的内部是会将对象里的所有属性逐一进行地址和内容上的判断,如果全部都相等,则返回true,否则就会返回false,例如:360截图17860601474246

这里我们还要注意一点,任何时候重写equals,都必须要同时重写hashCode,这两者是相伴而生的,不要去单一的重写其中某一个。这个在阿里巴巴的《开发手册》中名明确规定,原因是因为:

在Map 和Set 类集合中, 用到这两个方法时, 首先会判断hashCode 的值, 如果
hash 相等, 则再判断equals 的结果。 HashMap 的get(Object key) 判断代码如下:360截图17571121434953

可以看到if条件表达式中的e.hash==hash 是先决条件, 只有相等才会执行阴影部分。如果不相等, 则阴影部分后边的equals 根本不会被执行。因此当两个对象hashCode相同时,还需要再调用equals 进行一次值的比较,但是, 若hashCode 都不同, 根据短路原则将直接判定对象不同, 跳过equals , 这大大加快了冲突处理效率。

如果只重写equals 而不重写hashCode的话:

Set hashSet = new HashSet ();
Person a = new Person( l , "one") ;
Person b = new Person( l , ”one” ) ;
Person c = new Person( l , ”one” );
hashSet.add(a);
hashSet.add(b) ;
hashSet.add(c) ;
System.out.println(hashSet.size()) ;

显然a,b,c三个对象的内容完全相同的,根据hashSet的去重特性,按理来说这里应该打印1,但是这里打印出来的结果却是3,原因是因为没有重写hashCode的话,每个new出来的对象的即使内容相等,hashCode却不相等(因为hashCode是根据对象的地址进行运算的,而堆内存的地址必定是不同的),此时hashSet的去重特性无法发挥(即无法进一步判断他们的值是否相同,因为hashCode不同,根据短路原则一票否决了)。所以如果不重写hashCode(),即使equals()相等也毫无意义,而只要像这样重写hashCode:

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

此时hashCode方法已经与对象的地址无关了,而只跟对象属性的内容有关。也就是说a,b,c此时的hashCode是相同的,age + name.hashCode() ;因为age是基本数据类型,而name是String类型(String类内部已经重写了hashCode(),所以此时直接调用即可),只要我们查看String源码后,如果String的hashCode()方法是字符串相同返回hashCode也相同,那么此时hashCode就肯定相同了,这样一来也就可以发挥hashSet的去重功能了。

所以最后我们来看一下String 的hashCode源码,分析为何equals相等则hashCode肯定相等。360截图19000807407664

可以看到String的hashcode方法的实现原理是将字符串变成char[]数组,从而对字符串中的字符进行逐一的比较,所以字符串内容相同,它的hashCode肯定是相同的。而一个对象的属性要么就是基本数据类型,要么就是引用类型,而给引用类型用构造方法或者set方法赋值时本质上就是赋字符串!


 上一篇
一文讲透抽象类和接口 一文讲透抽象类和接口
什么是抽象类和接口抽象类:简而言之,用abstract关键词修饰的类就是抽象类,抽象类不能创建实例对象,即不能new对象。 接口:接口就是一种特殊的抽象类,接口中的所有方法都必须是抽象的且默认是public abstaract类型的。 接口
2019-03-14 统行
下一篇 
判断奇偶 判断奇偶
最近在知乎上看到一位大佬分享了一道面试题,题目很简单,但对基础考察的淋漓尽致,很有启发意义,分享给大家。 题目:判断一个数的是奇数。 相当一部分人会这样写: public boolean Odd(int i){ if(i%2 =
2019-03-12 统行
  目录