第9条 覆盖equals总要覆盖hashCode

1. equals与hashCode的关系

通过《覆盖equals时请遵守通用约定》(https://blog.csdn.net/m0_38095922/article/details/104587380)的讲述,我们知道,每个覆盖了equals方法的类中,也必须覆盖hashCode方法

2. hashCode的通用约定

摘自Object规范(JavaSE6)
1)在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。

2)相等的对象必须具有相等的散列码(hash code)。如果两个对象根据equals(Object)方法比较是相等的,那么调用两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。

3)如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是,给不同的对象产生截然不同的整数结果,有可能减少hash冲突、提高散列表的性能。

3. 如何判断两个对象相等?

1)当类没有覆盖equals和hashCode方法时,使用==即可判断是否相等,若是对象引用就比较内存地址是否相等,若是基本数据类型就比较具体的值。

2)当类覆盖equals和hashCode方法时,通过equals方法判断对象是否相等,这个时候对象相等概念是程序自己定义的,实际中大部分是比较对象关键成员变量是否相等。

4. hashCode方法应该是什么样

hashCode约定中第三条告知程序员尽量“为不相等的对象产生不相等的散列码”,那么怎么做到呢?

1)把某个非零的常数值,比如17,保存子啊一个名为result的int类型变量中。

2)对于每个对象每个关键成员f(指equals方法涉及的每个成员),完成以下步骤:
	a. 为改成员计算int类型的散列码c
		i. 如果该成员是boolean类型,则计算(f ?1:0)
		ii. 如果该成员是byte、char、short或者int类型,则计算(int)f
		iii. 如果该成员是long类型,则计算(int)(f^(f>>>32))
		iv. 如果该成员是float类型,则计算Float.floatToIntBits(f)
		v. 如果该成员是double类型,则计算Double.doubleToLongBits(f),		       	     然后按照步骤2.a.iii,为得到的long类型值计算散列值
		vi. 如果该成员是一个对象引用,并且该类的equals方法通过递归调用equals的方式来比较这个成员,则同样为这个成员递归地调用hashCode。如果徐亚哟更复杂的比较,则为该成员计算一个“范式”,然后针对这个范式调用hashCode。如果这个成员的值为null,则返回0(或者其他某个常数,但通常是0)
		vii. 如果该成员是一个数组,则要把每一个元素当做单独的成员来处理。也即,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组成员中的每个元素都很重要,可以利用发行版本1.5中新增的一个Arrays.hashCode方法
    b. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
    	result = 31 * result + c;

3) 返回result

4)写完了hashCode方法之后,写单元测试来验证“相等的实例是否具有相等的散列码”
public class CounterPoint2{
    private Point point;
    private int z;
    private String desc;
    public CounterPoint2(int x, int y, int z, String desc) {
        point = new Point(x, y);
        z = z;
        desc = desc;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CounterPoint2 that = (CounterPoint2) o;

        if (z != that.z) return false;
        if (point != null ? !point.equals(that.point) : that.point != null) return false;
        return desc != null ? desc.equals(that.desc) : that.desc == null;
    }

    @Override
    public int hashCode() {
    	int result = 17;
        int result = 31 * result + (point != null ? point.hashCode() : 0);
        result = 31 * result + z;
        result = 31 * result + (desc != null ? desc.hashCode() : 0);
        return result;
    }
}

结合实例,上述步骤1中用到了一个非零的初始值,当步骤2.a中计算的成员的散列值为0时、初始的非零值就会影响当前成员的散列值、使其最终能影响到对象的散列值。如果步骤1中的初始值为0,那么散列值为0的成员变量对于对象的散列值将没有任何影响,这可能会增加冲突的可能性。

步骤2.b中的乘法部分使得散列值依赖于成员的顺序,如果一个类包含多个相似的变量,这样的乘法运算就会产生一个更好的散列函数。例如,如果String的散列函数省略了这个乘法运算,那么字母顺序不同的所有字符串的散列码是相同的。之所以选择31,是因为它是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i = (i<<5) - i。

如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部。如果这个对象不被常用的话,可以选择“延迟初始化”散列码,一直到hashCode被第一次调用的时候才初始化。

@Override
    public int hashCode() {
    	if(result == 0{
    		int result = 17;
        	int result = 31 * result + (point != null ? point.hashCode() : 0);
        	result = 31 * result + z;
        	result = 31 * result + (desc != null ? desc.hashCode() : 0);
    	}
        return result;
    }

5. 总结

所以,覆盖equals方法的时候一定要覆盖hashCode方法,并且遵从“不相等的对象散列码不相等”的原则去生成散列码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值