Equals And HashCode 梳理
常言道:"基础不牢,地动山摇"。万丈高楼平地起,不管学习什么语言,在高谈阔论框架和语言应用的同时,也不能忘了语言基础。
本文是关于Equals和HashCode关系梳理的一篇文章,会主要从定义、联系、使用这三个方面围绕展开。
equals()是object里的方法,话不多说,先直接上源码:
正如注释第一句所谈到的, equals就是一个辨别两个对象之间的"相等"关系的函数 。
当然这种相等关系应由子类自行定义,但注释中指出需遵守以下性质:
性质1: reflexive,自反性 。对于任何非空引用x,x.equals(x)==true应恒成立。
性质2: symmetric,对称性 。对于两个非空引用x和y,x.euqals(y)==true当且仅当y.euqals(x)==true。
性质3: transitive,传递性。 简单地讲,x equals y , y equals z, 则x equals z成立。
性质4: consistent,一致性。 如果两个非空对象x,y没有发生变化,则反复调用x.euqals(y)所返回的结果应一致。
性质5:对于任何非空引用x,x.equals(null)==false成立。
了解完性质,关键的地方来了:
注释中指出,重写equals()则应重写hashcode(),使得equals相等的对象具有相等的hashCode。
hashcode是native方法,具体生成的细节和jdk版本有关,如何生成hashcode并不是今天的主题。接下来,我们看下hashcode的部分注释:
总的来讲:
hashcode的产生是为了hashmap等使用到hashcode的散列存储结构服务的。
hashcode有如下三个性质:
性质1 :一致性:对于同一对象,多次调用hashcode()应返回相同的Integer。
性质2 :equals相等,则hashCode必须相等。
性质3 :equals不等,hashCode不是必须不等。
了解了hashcode()以及equals()之后,肯定会发生这样一个疑问,为什么equals相等,hashcode必须相等?
实际上,这个问题只要搞懂hashcode()的用途,就可以很清楚的明白了。
hashcode()产生的目的就是为了hashMap等散列存储结构提供支持。
提供什么样的支持?
首先要明白,散列结构存储元素的重要过程之一是判别元素的“相同”/“不同”,这个相同与否是根据元素的hash值(为了降低hash冲突的概率,不同结构计算hash值的方式不同,但都是基于hashCode)决定的。每次put元素,则计算对应元素的hash值以及对应的坐标 ,有hash冲突则解决hash冲突,没有就直接存放。
这样做的好处是 ,当我要向一个散列结构存储一个元素时,我判别是否有重复元素,只需要计算hash值一次并比较,而不是对已经存储的每个元素依次equals(想象一个巨大的hashset,每次存放一个元素要和所有元素依次equals,这效率该多么低下)。
所以问题来了 ,如果散列结构存储的元素重写了equals方法却没有重写hashcode()方法,就会导致一个问题:逻辑上我们认为相同的对象(equals==true),在散列存储结构的存储中因为hashcode的不同,最终被判定为两个不同的对象,从而存放了两份。
显然这并不是我们想要的。
下面附上一个例子,来展示下
这里创建了一个Person对象,重写了equals方法,equals方法的逻辑是如果是同一个对象或者两个Person对象的名字和年龄相等,则认为是“相等”的。
这里我们创建了两个我们认为逻辑上相等的对象,使用hashset测试结果为:
显然hashset将其认为是两个对象,分别存储了。
所以我们现在要做的就是重写hashCode()方法,确保equals相等的实例hashcode()也相等。
不要小瞧equals()和hashcode()的重写,如果不恰当的重写,会导致一系列难以预料的结果产生。
在上面的person例子中,对equals的重写,我参照了String的equals重写。
大致逻辑是 判断是否为同一个对象,如果否,判断是否相同类型且逻辑相等。
实际上《Effective Java》这本书中提出了一个关于重写equals的规范,可以进行参考。
对于equals的重写, 最应该记住的是传入的应该是object! (最好使用@Override来帮助检查),否则就不是重写而是重载了!
当然,在重写完equals后最好对其进行单元测试,看其是否符合之前所讲的equals方法所强调的性质 。如果你是android开发人员,简单的junit单元测试可以参照我之前的一篇博客。 https://www.jianshu.com/p/5fea0dcc53b6
同样的《Effective Java》这本书中提出了一个关于重写HashCode方法的建议,可以进行参考。
我们参照上面的建议,重写一下Person类的hashcode():
在未测试的情况下这个hashcode()的冲突处理是否合适还需考量,但逻辑上它确实保证了与equals方法的同步。
现在再进行测试,发现已经搞定。
看到《Effective Java》建议的你,一定会有些疑问,why must 31?
事实上,如果你看过String(Android jdk)的hashCode源码,你会发现它是这样的
它竟然也使用了31这个数字!!
所以这背后的原理是什么呢?31又有怎样的好处呢?
《Effective Java》里也进行了解释:
这段话里已经讲的比较明确了:
1.选择奇数,防止溢出信息丢失。(实际这一点我还不太明白,有懂的老哥可以讲讲)
2.选择素数,because its traditional。(总的来讲就是和是否是素数关系不大。可以看下面的StackOverFlow链接,许多人认为和是否为素数没关系,毕竟所有的素数都是奇数(除了2))
3.乘以31,可以被优化为位运算,就会比传统的乘法快很多。
所以为什么必须是31呢?41,51不行吗?
https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier
stackOverFlow的这篇问答里有一个老哥做了测试:
也就是说,如果对超过50000个英语单词做hash运算,并采用31,33,37,39,41等做为常数乘,结果冲突次数都在7次以内。
那么31是最接近2幂次的数字,优化为位运算更简洁,自然采用31。
这篇博客在草稿箱里也待了好几天,一直耽搁着没有发出来。今天完成了发出来真是畅快啊。
2024-08-15 广告