qidao123.com技术社区-IT企服评测·应用市场

标题: 为什么重写equals一定也要重写hashCode方法? [打印本页]

作者: 莱莱    时间: 2025-5-6 08:11
标题: 为什么重写equals一定也要重写hashCode方法?
简要答复

这个是针对set和map这类使用hash值的对象来说的
总结:对于普通判断对象是否相等来说,只equals是可以完成需求的,但是假如使用set,map这种需要用到hash值的集适时,不重写hashCode方法,是无法满足需求的。尽管如此,也一般建议两者都要重写,几乎没有见过只重写一个的情况
详细先容

==

"==" 是运算符
  1. Person p1 = new Person("123");
  2. Person p2 = new Person("123");
  3. int a = 10;
  4. int b = 10;
  5. System.out.println(a == b);//true
  6. System.out.println(p1 == p2); //显然不是同一个对象,false
复制代码
equals

作用是 用来判断两个对象是否相等。通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等。源码如下:
  1. public boolean equals(Object obj) {
  2.     return (this == obj);
  3. }
复制代码
equals 方法不能用于比较基本数据范例,假如没有对 equals 方法举行重写,则相称于“==”,比较的是引用范例的变量所指向的对象的地址值。
一般情况下,类会重写equals方法用来比较两个对象的内容是否相等。比如String类中的equals()是被重写了,比较的是对象的值。
hashcode

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

在Java中equals方法用于判断两个对象是否相等,而HashCode方法在Java中主要由于哈希算法中的寻域的功能(也就是寻找数据应该存储的地域的)。在类似于set和map集合的结构中,Java为了提高在集合中查询匹配元素的服从问题,引入了哈希算法,通过HashCode方法得到对象的hash码,再通过hash码推算出数据应该存储的位置。然后再举行equals操作举行匹配,减少了比较次数,提高了服从。
  1. public class Person {
  2.     String name;
  3.     public Person(String name) {
  4.         this.name = name;
  5.     }
  6.     @Override
  7.     public boolean equals(Object o) {
  8.         if (this == o) return true;
  9.         if (o == null || getClass() != o.getClass()) return false;
  10.         Person person = (Person) o;
  11.         return Objects.equals(name, person.name);
  12.     }
  13.     public static void main(String[] args) {
  14.         Person p1 = new Person("123");
  15.         Person p2 = new Person("123");
  16.         System.out.println(p1 == p2);//false
  17.         System.out.println(p1.hashCode() == p2.hashCode());//false
  18.         System.out.println(p1.equals(p2));//true
  19.         Set<Person> set = new HashSet<>();
  20.         set.add(p1);
  21.         set.add(p2);
  22.         System.out.println(set.size());//2
  23.     }
  24. }
复制代码

重写hashCode方法:重写hashcode方法时,一般也是对属性值举行hash
  1. @Override
  2. public int hashCode() {
  3.     return Objects.hash(name);
  4. }
复制代码
重写了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值的对象来说的
总结:对于普通判断对象是否相等来说,只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方法)
  1. private void set(ThreadLocal<?> key, Object value) {
  2.     // We don't use a fast path as with get() because it is at
  3.     // least as common to use set() to create new entries as
  4.     // it is to replace existing ones, in which case, a fast
  5.     // path would fail more often than not.
  6.     Entry[] tab = table;
  7.     int len = tab.length;
  8.     int i = key.threadLocalHashCode & (len-1);
  9.     //线性探测的关键代码
  10.     for (Entry e = tab[i];
  11.          e != null;
  12.          e = tab[i = nextIndex(i, len)]) {
  13.         ThreadLocal<?> k = e.get();
  14.         if (k == key) {
  15.             e.value = value;
  16.             return;
  17.         }
  18.         if (k == null) {
  19.             replaceStaleEntry(key, value, i);
  20.             return;
  21.         }
  22.     }
  23.     tab[i] = new Entry(key, value);
  24.     int sz = ++size;
  25.     if (!cleanSomeSlots(i, sz) && sz >= threshold)
  26.         rehash();
  27. }
复制代码
但是如许会有一个问题,就是随着键值对的增多,会在哈希表里形成连续的键值对。当插入元素时,恣意一个落入这个区间的元素都要一直探测到区间末尾,并且最终将本身加入到这个区间内。如许就会导致落在区间内的关键字Key要举行多次探测才气找到符合的位置,并且还会继承增大这个连续区间,使探测时间变得更长,如许的现象被称为“一次聚集(primary clustering)”。


平方探测

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


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

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

再散列法其实很简单,就是再使用哈希函数去散列一个输入的时间,输出是同一个位置就再次散列,直至不发生冲突位置
缺点:每次冲突都要重新散列,计算时间增加。一般不消这种方式

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/) Powered by Discuz! X3.4