为什么重写 equals 一定也要重写 hashCode 方法?
简要回答
这个是针对 set 和 map 这类使用 hash 值的对象来说的
1、只重写 equals 方法,不重写 hashCode 方法:
有这样一个场景有两个 Person 对象,可是如果没有重写 hashCode 方法只重写了 equals 方法,equals 方法认为如果两个对象的 name 相同则认为这两个对象相同。这对于 equals 判断对象相等是没问题的。
对于 set 和 map 这类使用 hash 值的对象来说,由于没有重写 hashCode 方法,此时返回的 hash 值是不同的,因此不会去判断重写的 equals 方法,此时也就不会认为是相同的对象。
2、重写 hashCode 方法不重写 equals 方法
不重写 equals 方法实际是调用 Object 方法中的 equals 方法,判断的是两个对象的堆内地址。而 hashCode 方法认为相等的两个对象在 equals 方法处并不相等。因此也不会认为是用一个对象
因此重写 equals 方法时一定也要重写 hashCode 方法,重写 hashCode 方法时也应该重写 equals 方法。
总结:对于普通判断对象是否相等来说,只 equals 是可以完成需求的,但是如果使用 set,map 这种需要用到 hash 值的集合时,不重写 hashCode 方法,是无法满足需求的。尽管如此,也一般建议两者都要重写,几乎没有见过只重写一个的情况
详细介绍
==
“==" 是运算符
如果比较的对象是基本数据类型,则比较的是其存储的值是否相等;
如果比较的是引用数据类型,则比较的是所指向对象的地址值是否相等(是否是同一个对象)。
equals
作用是 用来判断两个对象是否相等。通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等。源码如下:
equals 方法不能用于比较基本数据类型,如果没有对 equals 方法进行重写,则相当于“==”,比较的是引用类型的变量所指向的对象的地址值。
一般情况下,类会重写 equals 方法用来比较两个对象的内容是否相等。比如 String 类中的 equals()是被重写了,比较的是对象的值。
hashcode
hashcode 特性体现主要在它的查找效率上,O(1)的复杂度,在 Set 和 Map 这种使用哈希表结构存储数据的集合中。hashCode 方法的就大大体现了它的价值,主要用于在这些集合中确定对象在整个哈希表中存储的区域。
如果两个对象相同,则这两个对象的 equals 方法返回的值一定为 true,两个对象的 hashCode 方法返回的值也一定相同。(equals 相同,hashcode 一定相同,因为重写的 hashcode 就是计算属性的 hashcode 值)
如果两个对象返回的 HashCode 的值相同,但不能够说明这两个对象的 equals 方法返回的值就一定为 true,只能说明这两个对象在存储在哈希表中的同一个桶中。
只重写了 equals 方法,未重写 hashCode 方法
在 Java 中 equals 方法用于判断两个对象是否相等,而 HashCode 方法在 Java 中主要由于哈希算法中的寻域的功能(也就是寻找数据应该存储的区域的)。在类似于 set 和 map 集合的结构中,Java 为了提高在集合中查询匹配元素的效率问题,引入了哈希算法,通过 HashCode 方法得到对象的 hash 码,再通过 hash 码推算出数据应该存储的位置。然后再进行 equals 操作进行匹配,减少了比较次数,提高了效率。
当只重写了 equals 方法,未重写 hashCode 方法时,equals 方法判断两个对象是否相等时,返回的是 true(第三个输出),这是因为我们重写 equals 方法时,是对属性的比较;但判断两个对象的 hashCode 值是否相等时,返回的是 false(第二个输出),在没有重写 hashCode 方法的情况下,调用的是 Object 的 hashCode 方法,返回的是本对象的 hashCode 值,两个对象不一样,因此 hashCode 值不一样。
在 set 和 map 中,首先判断两个对象的 hashCode 方法返回的值是否相等,如果相等然后再判断两个对象的 equals 方法,如果 hashCode 方法返回的值不相等,则直接会认为两个对象不相等,不进行 equals 方法的判断。因此在 set 添加对象时,因为 hashCode 值已经不一致,判断出 p1 和 p2 是两个对象,都会添加进 set 集合中,因此返回集合中数据个数为 2 (第四个输出)

重写 hashCode 方法:重写 hashcode 方法时,一般也是对属性值进行 hash
重写了 hashCode 后,其是对属性值的 hash,p1 和 p2 的属性值一致,因此 p1.hashCode() == p2.hashCode()为 true,再进行 equals 方法的判断也为 true,认为是一个对象,因此 set 集合中只有一个对象数据。
为什么重写 hashCode 一定也要重写 equals 方法?
如果两个对象的 hashCode 相同,它们是并不一定相同的,因为 equals 方法不相等而 hashCode 方法返回的值却有可能相同的,比如两个不同的对象 hash 到同一个桶中
hashCode 方法实际上是通过一种算法得到一个对象的 hash 码,这个 hash 码是用来确定该对象在哈希表中具体的存储区域的。返回的 hash 码是 int 类型的所以它的数值范围为 [-2147483648 - +2147483647] 之间的,而超过这个范围,实际会产生溢出,溢出之后的值实际在计算机中存的也是这个范围的。比如最大值 2147483647 + 1 之后并不是在计算机中不存储了,它实际在计算机中存储的是-2147483648。在 java 中 hash 码也是通过特定算法得到的,所以很难说在这个范围内情况下不会不产生相同的 hash 码的。也就是说常说的哈希碰撞,因此不同对象可能有相同的 hashCode 的返回值。
因此 equals 方法返回结果不相等,而 hashCode 方法返回的值却有可能相同!
为什么重写 equals 一定也要重写 hashCode 方法?
这个是针对 set 和 map 这类使用 hash 值的对象来说的
1、只重写 equals 方法,不重写 hashCode 方法:
有这样一个场景有两个 Person 对象,可是如果没有重写 hashCode 方法只重写了 equals 方法,equals 方法认为如果两个对象的 name 相同则认为这两个对象相同。这对于 equals 判断对象相等是没问题的。
对于 set 和 map 这类使用 hash 值的对象来说,由于没有重写 hashCode 方法,此时返回的 hash 值是不同的,因此不会去判断重写的 equals 方法,此时也就不会认为是相同的对象。
2、重写 hashCode 方法不重写 equals 方法
不重写 equals 方法实际是调用 Object 方法中的 equals 方法,判断的是两个对象的堆内地址。而 hashCode 方法认为相等的两个对象在 equals 方法处并不相等。因此也不会认为是用一个对象
因此重写 equals 方法时一定也要重写 hashCode 方法,重写 hashCode 方法时也应该重写 equals 方法。
总结:对于普通判断对象是否相等来说,只 equals 是可以完成需求的,但是如果使用 set,map 这种需要用到 hash 值的集合时,不重写 hashCode 方法,是无法满足需求的。尽管如此,也一般建议两者都要重写,几乎没有见过只重写一个的情况
解决哈希冲突的三种方法
拉链法
HashMap,HashSet 其实都是采用的拉链法来解决哈希冲突的,就是在每个位桶实现的时候,采用链表的数据结构来去存取发生哈希冲突的输入域的关键字(也就是被哈希函数映射到同一个位桶上的关键字)

但是如果 hash 冲突⽐较严重,链表会⽐较⻓,查询的时候,需要遍历后⾯的链表,因此 JDK 优化了⼀版,链表的⻓度超过阈值的时候,会变成红⿊树,红⿊树有⼀定的规则去平衡⼦树,避免退化成为链表,影响查询效率。

但是你肯定会想到,如果数组太⼩了,放了⽐较多数据了,怎么办?再放冲突的概率会越来越⾼,其实这个时候会触发⼀个扩容机制,将数组扩容成为 2 倍⼤⼩,重新 hash 以前的数据,哈希到不同的数组中。
hash 表的优点是查找速度快,但是如果不断触发重新 hash , 响应速度也会变慢。同时,如果希望范围查询, hash 表不是好的选择。
拉链法的装载因子为 n/m(n 为输入域的关键字个数,m 为位桶的数目)
开放地址法
所谓开放地址法就是发生冲突时在散列表(也就是数组里)里去寻找合适的位置存取对应的元素,就是所有输入的元素全部存放在哈希表里。也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过 1。
它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。
探查序列的方法:
线性探查
平方探测
伪随机探测
线性探查
di =1,2,3,…,m-1;这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

(使用例子:ThreadLocal 里面的 ThreadLocalMap 中的 set 方法)
但是这样会有一个问题,就是随着键值对的增多,会在哈希表里形成连续的键值对。当插入元素时,任意一个落入这个区间的元素都要一直探测到区间末尾,并且最终将自己加入到这个区间内。这样就会导致落在区间内的关键字 Key 要进行多次探测才能找到合适的位置,并且还会继续增大这个连续区间,使探测时间变得更长,这样的现象被称为“一次聚集(primary clustering)”。


平方探测
在探测时不一个挨着一个地向后探测,可以跳跃着探测,这样就避免了一次聚集。
di=12,-12,22,-22,…,k2,-k2;这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。虽然平方探测法解决了线性探测法的一次聚集,但是它也有一个小问题,就是关键字 key 散列到同一位置后探测时的路径是一样的。这样对于许多落在同一位置的关键字而言,越是后面插入的元素,探测的时间就越长。


这种现象被称作“二次聚集(secondary clustering)”,其实这个在线性探测法里也有。
伪随机探测
di=伪随机数序列;具体实现时,应建立一个伪随机数发生器,(如 i=(i+p) % m),生成一个位随机序列,并给定一个随机数做起点,每次去加上这个伪随机数++就可以了。
再散列法
再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置
缺点:每次冲突都要重新散列,计算时间增加。一般不用这种方式
文章转载自: Seven
评论