概述
学过java的同学应该都有了解,在重写equals方法的时候我们都要重写hashCode方法,那么我们为什么要这么做呢,这么做的意义又是什么呢,笔者将会在这边篇文章中基于《Effective Java》与大家深入讨论一下。
hashCode的诞生
在hash集合中,最大的特点是每个元素只能出现一次,如果元素重复,将不会添加入集合,那么集合类是如何判断元素是否重复的呢,答案就是hash值。
我们定义equals方法时,相当于是将equals方法的入参根据给定的逻辑规则进行判断,以判定这两个元素是否相等,如果hash集合也调用equals方法去判断存在性,那性能就太低了。
于是前人们就想到了使用hash码,hash码是根据equals中所有的值,根据某个函数规则而计算出的一个值,每个对象都能计算出一个hash值,而计算hash值的性能开销明显要远低于equals逻辑比较。
hashCode需要重写的原因
那为什么要在重写equals方法的时候同时重写hashCode呢?
其实上文已经讲到了,因为hashCode的值也是代表两个对象是否相等的依据之一,所以equals所依赖的值,hashCode方法也要完全相同的依赖它们中的每一个做计算。
hashCode的不足以及解决方式
前文已经提到,hashCode是根据一个给定的函数,例如f(x) =x^2 + 2x + 3,这个函数并不是真实存在的,但很有可能x的值不同却计算出了相同的hashCode码,那这不就导致比较错误了吗?
所以工程师们选择了一个折中的思路,当hashCode值不一样的时候就认定两个对象绝对不同,如果hashCode值一样,那就再去根据对象的equals方法去检验两个对象是否真的相同。
这样既在一定程度上降低了全equals比较的性能开销,又确保了两个对象的相同性比较一定正确。
hashSet等集合的处理方式
hashSet利用了数组加链表的方式储存对象,如果hashCode值不同,就将它单独放在一个桶里,如果发现有一个对象跟另一个对象hash值相同,但实际上却是两个不同的对象,那么hashSet就会将hashCode相同的元素以链表的形式连接在其后面。
那么将来又有新的对象hash值相同时,就会用equals比较这个链表中每个元素了,如果确实都不一样,就会将它继续链在链表之后。
hash集合类引发的思考
根据以上内容我们可以想到:
1.如果两个对象的hashCode不同,那么它们一定不是equals的。
2.而如果两个对象hashCode相同,它们也不一定是equals的。
你可能会说,那既然这个要求不死,那我们给一个对象制定一个hashCode方法,直接return一个值比如说42,那不就可以满足上述要求了吗,反正相同也不一定equals。
那就要回想到hashSet等集合类存在的意义了,它们的存在就是为了可以通过计算一个对象的hash值来快速判断一个对象在集合中的存在性,那么如果所有对象的hash值都相同,那么散列表就会退化为链表,查询每个元素存在的时间复杂度就从O(1)退化成O(n)了。
那每次查询一个元素的存在性都要遍历一次整个集合,那用hash集合的意义就不存在了。
那么,如何编写一个好的hashCode方法呢?
常规方法
我们可以按照下面的公式,递归的计算每一个对象加入后的值,再合并入result返回:
result = 31 * result + c;
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
}
对此我们需要做出一点儿解释:
1.选择31作为基数去做乘法的原因是它是一个奇素数,并且它的值正好是2的5次方-1,对于这个计算,现代虚拟机是可以自动做出这样的优化的(位运算效率更高)。
2.此处我们对基础类型选择了使用Type.hashCode(parameter),这样做比直接使用值进行计算要更好,使用这种系统提供的方法的好处是它可以良好的应对值为null时的情况,使得即便传入的参数是null也不会引发空指针异常,并且能够正常计算hashCode值。
3.如果有一个完整的数组也是hashCode考虑的域,那么可以使用Arrays.hashCode方法。
Objects类提供的静态方法
Objects类中有一个静态方法,它可以接受任意数量的对象,并为它们返回一个散列码:
Objects.hash(areaCode, prefix, lineNum);
这个方式生成的hash码与我们刚才那样手动制作的hashCode函数所生成的散列码,从质量上说是想当的,但遗憾的是,它相比我们刚才的操作要慢一些,因为对于基本类型的参数而言,会触发额外的自动装箱和自动拆箱动作。
当然,如果在不太注重性能的情况下,图方便还是可以使用这种便捷的形式的。
hash码获取形式的优化
如果一个类是不可变的,并且每次计算散列码的开销很大,哪门我们就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都依赖hashCode方法重新计算散列码。
如果你觉得这种类型的大多数对象会被作为散列key,或者就是说你觉得它的散列码会被经常使用,那么你就应该在创建实例之初就生成这个hash码;否则的话你可以选择延迟初始化,也就是Lazy初始化这个散列值。
private int hashCode; // Automatically initialized to 0
@Override public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
不要对hash码有其他的想法
作为类的制定者,我们不应该对hashCode方法的返回值做出具体的规定,也就是不要对hash码的计算策略在文档中做详细说明,因为其他程序员不必关心也不应该利用这一点。
作为类的使用者,不要依赖hashCode方法计算hash值的具体逻辑来做其他操作,因为可能有一天我们对性能有更高要求时,可能会改变hashCode的计算逻辑,如果你强依赖它,那么你的程序可能在那时就要大改,甚至可能完全无法工作。
总结
本文从hashCode的诞生原因和意义讲起,并讲解了多种创建hashCode()的方法,以及在什么情况下应该使用什么方式,希望能对你的学习产生帮助。
同时你也可以使用谷歌开源的AutoValue框架自动生成对应的equals和hashCode方法,现在很多的IDE编译器也有自带这种类似的功能,在你了解原理之后,同样也是推荐使用这两种方式的。