java中hashCode方法的深入讨论

概述

学过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编译器也有自带这种类似的功能,在你了解原理之后,同样也是推荐使用这两种方式的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值