写点什么

为什么重写 equals 一定也要重写 hashCode 方法?

  • 2025-05-06
    福建
  • 本文字数:4766 字

    阅读完需:约 16 分钟

简要回答


这个是针对 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 方法,是无法满足需求的。尽管如此,也一般建议两者都要重写,几乎没有见过只重写一个的情况


详细介绍


==


“==" 是运算符


  1. 如果比较的对象是基本数据类型,则比较的是其存储的值是否相等;

  2. 如果比较的是引用数据类型,则比较的是所指向对象的地址值是否相等(是否是同一个对象)。


Person p1 = new Person("123");Person p2 = new Person("123");int a = 10;int b = 10;System.out.println(a == b);//trueSystem.out.println(p1 == p2); //显然不是同一个对象,false
复制代码


equals


作用是 用来判断两个对象是否相等。通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等。源码如下:


public boolean equals(Object obj) {    return (this == obj);}
复制代码


equals 方法不能用于比较基本数据类型,如果没有对 equals 方法进行重写,则相当于“==”,比较的是引用类型的变量所指向的对象的地址值。


一般情况下,类会重写 equals 方法用来比较两个对象的内容是否相等。比如 String 类中的 equals()是被重写了,比较的是对象的值。


hashcode


  1. hashcode 特性体现主要在它的查找效率上,O(1)的复杂度,在 Set 和 Map 这种使用哈希表结构存储数据的集合中。hashCode 方法的就大大体现了它的价值,主要用于在这些集合中确定对象在整个哈希表中存储的区域。

  2. 如果两个对象相同,则这两个对象的 equals 方法返回的值一定为 true,两个对象的 hashCode 方法返回的值也一定相同。(equals 相同,hashcode 一定相同,因为重写的 hashcode 就是计算属性的 hashcode 值)

  3. 如果两个对象返回的 HashCode 的值相同,但不能够说明这两个对象的 equals 方法返回的值就一定为 true,只能说明这两个对象在存储在哈希表中的同一个桶中。


只重写了 equals 方法,未重写 hashCode 方法


在 Java 中 equals 方法用于判断两个对象是否相等,而 HashCode 方法在 Java 中主要由于哈希算法中的寻域的功能(也就是寻找数据应该存储的区域的)。在类似于 set 和 map 集合的结构中,Java 为了提高在集合中查询匹配元素的效率问题,引入了哈希算法,通过 HashCode 方法得到对象的 hash 码,再通过 hash 码推算出数据应该存储的位置。然后再进行 equals 操作进行匹配,减少了比较次数,提高了效率。


public class Person {    String name;
public Person(String name) { this.name = name; }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return Objects.equals(name, person.name); }
public static void main(String[] args) { Person p1 = new Person("123"); Person p2 = new Person("123");
System.out.println(p1 == p2);//false System.out.println(p1.hashCode() == p2.hashCode());//false System.out.println(p1.equals(p2));//true
Set<Person> set = new HashSet<>(); set.add(p1); set.add(p2); System.out.println(set.size());//2 }}
复制代码


  • 当只重写了 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


@Overridepublic int hashCode() {    return Objects.hash(name);}
复制代码


重写了 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 方法)


private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not.
Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);
//线性探测的关键代码 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get();
if (k == key) { e.value = value; return; }
if (k == null) { replaceStaleEntry(key, value, i); return; } }
tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
复制代码


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




平方探测


在探测时不一个挨着一个地向后探测,可以跳跃着探测,这样就避免了一次聚集。


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




这种现象被称作“二次聚集(secondary clustering)”,其实这个在线性探测法里也有。


伪随机探测


di=伪随机数序列;具体实现时,应建立一个伪随机数发生器,(如 i=(i+p) % m),生成一个位随机序列,并给定一个随机数做起点,每次去加上这个伪随机数++就可以了。


再散列法


再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置


缺点:每次冲突都要重新散列,计算时间增加。一般不用这种方式


文章转载自: Seven

原文链接:https://www.cnblogs.com/seven97-top/p/18860274

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
为什么重写equals一定也要重写hashCode方法?_Java_电子尖叫食人鱼_InfoQ写作社区