【Effective Java笔记】第9条:覆盖equals时总要覆盖hashCode

该篇博客阐述覆盖equals时总要覆盖hashcode,之前隐隐约约写过几次hashcode,但也没搞清楚为什么要去覆盖hashcode,而且也听别人说覆盖hashcode很简单,只需要随意返回一个整型数就好了,反反复复看了这个条目,终于弄明白为什么要hashcode了,而应当遵循一些约定

##该篇博客主要阐述
#####1、覆盖equals必须覆盖hashCode,why?

#####2、覆盖equals时应当同时覆盖hashCode的一些约定

#####3、没有覆盖hashCode违反了(1)中的约定第二条解析

#####4、场景:当没有覆盖hashCode时出现的问题

#####5、如何重写hashCode方法的步骤

#####6、result = 31 * result + c中(31的来源?,为什么是31*result?)


###一、覆盖equals必须覆盖hashCode,why?
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable


###二、覆盖equals时应当同时覆盖hashCode的一些约定

#####1、只要对象的equals方法的比较操作所用到的信息未被修改,那么对同一个对象调用多次其hashCode返回值不变


#####2、若两个对象通过equals得到是相等的,那么调用这两个对象任意一个对象的hashCode方法产生整数结果一样


#####3、若两个对象通过equals得到是不相等的,那么调用这两个对象任意一个对象的hashCode方法产生的结果也可能相等,但是从提高散列表(hash table)的性能分析,给不相等的对象产生不同的结果会更好


###三、没有覆盖hashCode违反了(1)中的约定第二条解析
因为Object类的hashCode方法,它们仅仅是两个没有任何共同之处对象。因此任意对象的hashCode方法返回两个看似是随机的整数,这与约定第二条相矛盾(返回两个相同整数)

以下DEMO验证了没有覆盖hashCode违反约定二

这里写图片描述

结果

这里写图片描述


###四、场景:当没有覆盖hashCode时出现的问题
####1、场景
key和value存入HashMap集合中,没有覆盖hashCode时,若想通过两个实例(new User(21,330))获取value是不行的!

####2、模拟场景

这里写图片描述

结果:可以看到value实际返回的是null

这里写图片描述

这里涉及两个User实例,第一个被用于插入到HashMap中,第二个实例与第一个相等,被用于(试图用于)获取。但是由于User类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。因此put方法把User对象存放到一个散列桶(hash bucket)中,get方法却在另一个散列桶中找User。

即使这两个实例正好被放到同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性


###五、如何重写hashCode方法的步骤
#####首先要声明的一点就是重写hashCode方法并不是如下这么简单,虽然这样的写法是合法的,因为它确保了相等的对象总是具有同样的散列码,但也可以说它是不好的,因为它使得每个对象都具有同样的散列码。因此每个对象都被映射到同一个散列桶中,使散列表退化为链表(linked list)。它使得本该线性时间运行的程序变成了以平方级时间在运行。对于规模很大的散列表而言,这会关系到散列表能否正常工作
@override public int hashCode(){ return 42; }//这样的写法使得本该线性时间运行的程序变成了以平方级时间在运行

上面这点很重要,也是很多人会这样写的(不是优方法),接下来就进入正题,到底应该怎么重写hashCode方法

#####1、设一个int类型变量result初始为任意数值,如int result = 17;

#####2、处理equals方法中的每个域f(指的就是上面代码中的age和areaCode):求得散列码c

a、该域是boolean类型					计算(f?1:0)
b、该域是byte、char、short、int类型	  计算(int)f
c、该域是long类型 					计算(int)(f^(f>>>32))
d、该域是float类型					计算Float.floatToIntBits(f)
e、该域是double类型					计算Double.doubleToLongBits(f),然后按步骤c计算
f、如果该域是一个对象的引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。
g、如果该域是一个数组,则要把每个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤3中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法 

#####3、按照下面的公式,将步骤2中得到的散列码合并到result中
result = 31*result+c

#####4、返回result

#####5、写完hashCode方法之后,问问自己“相等的实例是否具有相等的散列码”,编写单元测试来验证推断

所以上面返回null的DEMO应当加上覆盖hashCode方法

这里写图片描述

结果:可以看到成功返回linjie

这里写图片描述

同时根据步骤5,检测是否散列码相同,

	User user1 = new User(21, 111);
	User user2 = new User(21, 111);
	System.out.println("user1 hashcode : "+user1.hashCode());
	System.out.println("user2 hashcode : "+user2.hashCode());

结果显示散列码相等
这里写图片描述

####注意:如果这个类是一个不可变的类
public final class User{}

并且计算的散列码开销也比较大,你可以选择"延迟初始化(lazily initialize)"散列码,一直到hashCode被第一次调用的时候才初始化。如下实现,虽然该类并不确定是否值得这么做,但是这里目的是为了提供一种思路方法

这里写图片描述


###六、result = 31 * result + c中(31的来源?,为什么是31*result?)

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

#####2、为什么是31*result?
使用乘法使得散列值依赖于域的顺序,如果一个类包含多个相似的域,这样的乘法运算就会产生一个更好的散列函数。例如,如果String散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码。

发布了254 篇原创文章 · 获赞 695 · 访问量 117万+

猜你喜欢

转载自blog.csdn.net/w_linux/article/details/80440112