重写 equals 时为什么一定要重写 hashCode

news2024/9/20 18:53:26

equals 方法和 hashCode 方法是 Object 类中的两个基础方法,它们共同协作来判断两个对象是否相等。为什么要这样设计嘞?原因就出在“性能” 2 字上。

使用过 HashMap 我们就知道,通过 hash 计算之后,我们就可以直接定位出某个值存储的位置了,那么试想一下,如果你现在要查询某个值是否在集合中?如果不通过 hash 方式直接定位元素(的存储位置),那么就只能按照集合的前后顺序,一个一个的询问比对了,而这种依次比对的效率明显低于 hash 定位的方式。这就是 hash 以及 hashCode 存在的价值。
null在这里插入图片描述

当我们对比两个对象是否相等时,我们就可以先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals 再次确认两个对象是否相等,如果比较的结果是 true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。这样就大大的提升了对象比较的效率,这也是为什么 Java 设计使用 hashCode 和 equals 协同的方式,来确认两个对象是否相等的原因。

那为什么不直接使用 hashCode 就确定两个对象是否相等呢?

这是因为不同对象的 hashCode 可能相同但 hashCode 不同的对象一定不相等,所以使用 hashCode 可以起到快速初次判断对象是否相等的作用。

但即使知道了以上基础知识,依然解决不了本篇的问题,也就是:重写 equals 时为什么一定要重写 hashCode?要想了解这个问题的根本原因,我们还得先从这两个方法开始说起。

1.equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。

equals 方法的实现源码如下:

public boolean equals(Object obj) {
    return (this == obj);
}

通过上述源码和 equals 的定义我们可以看出,在大多数情况来说,equals 的判断是没有什么意义的!例如,使用 Object 中的 equals 比较两个自定义的对象是否相等,这就完全没有意义(因为无论对象是否相等,结果都是 false)。

通过以下示例,就可以说明这个问题:

public class EqualsMyClassExample {
    public static void main(String[] args) {
        Person u1 = new Person();
        u1.setName("Java");
        u1.setAge(18);
		
        Person u2 = new Person();
        u1.setName("Java");
        u1.setAge(18);
        
        // 打印 equals 结果
        System.out.println("equals 结果:" + u1.equals(u2));
    }
}
 
class Person {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

}

以上程序的执行结果,如下图所示:

在这里插入图片描述

因此通常情况下,我们要判断两个对象是否相等,一定要重写 equals 方法,这就是为什么要重写 equals 方法的原因。

2.hashCode 方法

hashCode 翻译为中文是散列码,它是由对象推导出的一个整型值,并且这个值为任意整数,包括正数或负数。

需要注意的是:散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode() 与 y.hashCode() 基本上不会相同;但如果 a 和 b 相等,则 a.hashCode() 一定等于 b.hashCode()。

hashCode 在 Object 中的源码如下:

public native int hashCode();

从上述源码可以看到,Object 中的 hashCode 调用了一个(native)本地方法,返回了一个 int 类型的整数,当然,这个整数可能是正数也可能是负数。

hashCode 使用

相等的值 hashCode 一定相同的示例:

public class HashCodeExample {public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = "Java";
        System.out.println("s1 hashCode:" + s1.hashCode());
        System.out.println("s2 hashCode:" + s2.hashCode());
        System.out.println("s3 hashCode:" + s3.hashCode());
    }
}

以上程序的执行结果,如下图所示:

在这里插入图片描述

不同的值 hashCode 也有可能相同的示例:

public class HashCodeExample {public static void main(String[] args) {
        String s1 = "Aa";
        String s2 = "BB";
        System.out.println("s1 hashCode:" + s1.hashCode());
        System.out.println("s2 hashCode:" + s2.hashCode());
    }
}

以上程序的执行结果,如下图所示:

在这里插入图片描述

3.为什么要一起重写?

接下来回到本文的主题,重写 equals 为什么一定要重写 hashCode?

为了解释这个问题,我们需要从下面的这个例子入手。

3.1 Set 正常使用

Set 集合是用来保存不同对象的,相同的对象就会被 Set 合并,最终留下一份独一无二的数据。

它的正常用法如下:

import java.util.HashSet;
import java.util.Set;
 
public class HashCodeExample {
    public static void main(String[] args) {
        Set<String> set = new HashSet();
        set.add("Java");
        set.add("Java");
        set.add("MySQL");
        set.add("MySQL");
        set.add("Redis");
        System.out.println("Set 集合长度:" + set.size());
        System.out.println();
        // 打印 Set 中的所有元素
        set.forEach(d -> System.out.println(d));
    }

}

以上程序的执行结果,如下图所示:

在这里插入图片描述

从上述结果可以看出,重复的数据已经被 Set 集合“合并”了,这也是 Set 集合最大的特点:去重。

3.2 Set 集合的“异常”

然而,如果我们在 Set 集合中存储的是,只重写了 equals 方法的自定义对象时,有趣的事情就发生了,如下代码所示:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
 
public class EqualsExample {
    public static void main(String[] args) {
        // 对象 1
        Persion p1 = new Persion();
        p1.setName("Java");
        p1.setAge(18);
		// 对象 2
        Persion p2 = new Persion();
        p2.setName("Java");
        p2.setAge(18);
		// 创建 Set 集合Set<Persion> set = new HashSet<Persion>();
        set.add(p1);
        set.add(p2);
		// 打印 Set 中的所有数据
        set.forEach(p -> {
            System.out.println(p);
        });
    }
}
 
 
class Persion {
    private String name;
    private int age;
 
    // 只重写了 equals 方法
    @Override
    public boolean equals(Object o) {if (this == o) return true; // 引用相等返回 true// 如果等于 null,或者对象类型不同返回 falseif (o == null || getClass() != o.getClass()) return false;// 强转为自定义 Persion 类型
        Persion persion = (Persion) o;
        // 如果 age 和 name 都相等,就返回 truereturn age == persion.age &&
                Objects.equals(name, persion.name);
    }
	
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
     @Overridepublic String toString() {
        return "Persion{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

以上程序的执行结果,如下图所示:

在这里插入图片描述

从上述代码和上述图片可以看出,即使两个对象是相等的,Set 集合竟然没有将二者进行去重与合并。这就是重写了 equals 方法,但没有重写 hashCode 方法的问题所在。

3.3 解决“异常”

为了解决上面的问题,我们尝试在重写 equals 方法时,把 hashCode 方法也一起重写了,实现代码如下:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
 
public class EqualsToListExample {
    public static void main(String[] args) {
        // 对象 1
        Persion p1 = new Persion();
        p1.setName("Java");
        p1.setAge(18);
		// 对象 2
        Persion p2 = new Persion();
        p2.setName("Java");
        p2.setAge(18);
		// 创建 Set 对象Set<Persion> set = new HashSet<Persion>();
        set.add(p1);
        set.add(p2);
		// 打印 Set 中的所有数据
        set.forEach(p -> {
            System.out.println(p);
        });
    }
}
 
 
class Persion {
    private String name;
    private int age;
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 引用相等返回 true// 如果等于 null,或者对象类型不同返回 falseif (o == null || getClass() != o.getClass()) return false;// 强转为自定义 Persion 类型
        Persion persion = (Persion) o;
        // 如果 age 和 name 都相等,就返回 truereturn age == persion.age &&
                Objects.equals(name, persion.name);
    }
 
    @Overridepublic int hashCode() {
        // 对比 name 和 age 是否相等return Objects.hash(name, age);
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
    @Overridepublic String toString() {
        return "Persion{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

}

以上程序的执行结果,如下图所示:
在这里插入图片描述

通过上述结果可以看出,当我们一起重写了两个方法之后,奇迹的事情又发生了,Set 集合又恢复正常了,这是为什么呢?

3.4 原因分析

出现以上问题的原因是,如果只重写了 equals 方法,那么默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false:两个对象不是相等的,于是就在 Set 集合中插入了两个相同的对象。

但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。

总结

hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,如果在重写 equals 时,不重写 hashCode,就会导致在某些场景下,例如将两个相等的自定义对象存储在 Set 集合时,就会出现程序执行的异常,为了保证程序的正常执行,所以我们就需要在重写 equals 时,也一并重写 hashCode 方法才行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/177811.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

移动web 空间转换 3D

移动web 空间转换 3D空间转换 3D3D位移透视3D旋rotateXrotateY左手法则立体呈现空间转换 3D 3D坐标系 3D 坐标系比2D 多了一个Z轴。 一定要记住3个坐标轴取值的正反&#xff1a; X 轴 往右越大&#xff0c;是正值&#xff0c; 否则反之Y 轴 往下越大&#xff0c;是正值&…

React错误边界

首先 我们先构建出问题的场景 我们创建一个react项目 然后在src下创建 components 文件夹目录 在下面创建一个 error.jsx 组件 参开代码如下 import React from "react";export default class App extends React.Component{constructor(props){super(props);this.…

CUDA编程笔记(5)

文章目录前言CUDA的内存组织全局内存常量内存纹理内存和表面内存寄存器局部内存共享内存L1和L2缓存SM的构成API函数查询设备总结前言 cuda的内存组织&#xff0c;在使用GPU时尽可能提高性能&#xff0c;合理的使用设备的内存也是十分重要的。 CUDA的内存组织 如表所示&#…

Docker基本操作

Docker基本操作一、镜像操作1.镜像名称2.镜像命令&#xff08;1&#xff09;拉取、查看镜像&#xff08;2&#xff09;保存、导入镜像二、容器操作1.容器相关命令2.创建并运行一个容器3.进入容器&#xff0c;修改文件4.小结三、数据卷&#xff08;容器数据管理&#xff09;1.什…

Java:枚举类型

Java&#xff1a;枚举类型 每博一文案 师父说&#xff1a;人活一世&#xff0c;每个人都有他的特别&#xff0c;每个人都值得被温柔相待。红尘一遭&#xff0c;每段经历都有它的必然&#xff0c; 每段经历都造就了现在的你&#xff0c;最快乐的事情&#xff0c;就是做自己&…

Java多线程案例之定时器

一. 定时器概述 1. 什么是定时器 定时器是一种实际开发中非常常用的组件, 类似于一个 “闹钟”, 达到一个设定的时间之后, 就执行某个指定好的代码. 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自…

排序算法: 数据的离散化(排序+去重 C++例题实现)

文章目录数据的离散化例题&#xff1a;电影完整代码数据的离散化 离散化是指将一个无穷大的集合中的若干个元素映射到一个有限的集合中&#xff0c;以便于对那个无穷大的集合进行操作。 在很多情况下&#xff1a;对于一个规定在Z范围内的整数范围&#xff0c;他有可能包含非常…

maven创建自定义web工程模板

一&#xff0c;先搭建好一个项目模板。 注意每个文件夹下都放一个文件占位&#xff0c;否则创建模板时会认为是空目录不进行创建。 注意项目文件夹名字 和 pom.xml 中<artifactId 和 <name 的名字都使用相同的名字&#xff0c;写一个好记的名字&#xff0c;因为后面生…

QT UI布局设置整理-边框设置

一、设置边距的方法 1、设置容器内部的内容控件的边距 //设置容器leftBar&#xff08;QWidget&#xff09;内部marginui->leftBar->setContentsMargins(10,10,0,0); 2、 设置内部控件之间的间距 //editWidget是一个QWidget ui->editWidget->layout()->setSpac…

【云原生kubernetes】k8s中控制器使用详解

一、什么是控制器 控制器是管理pod的中间层&#xff0c;只需要告诉Pod控制器&#xff0c;想要创建多少个什么样的Pod&#xff0c;它会创建出满足条件的Pod &#xff1b;控制器相当于一个状态机&#xff0c;用来控制Pod的具体状态和行为 &#xff1b;controller会自动创建相应的…

【数据库概论】3.2 SQL的查询、更新和删除语句

一、 数据查询 SQL提供SELECT语句用于查询&#xff0c;一般格式为&#xff1a; 根据WHERE子句条件表达式从FROM子句指定的基本表、视图中找出满足条件的元组 GROUP BY语句则用作将结果按照<列名1>的值进行分组&#xff0c;该属性列值相等的元组为一个组&#xff1b;ORD…

Essential C++第五章习题

目录 5.1 5.2 5.3 5.4 5.1 C代码&#xff1a; //Stack.h#include<vector> #include<string> #include<iostream> using namespace std;#pragma once#ifndef _STACK_H_ #define _STACK_H_typedef string elemType;class Stack { public://基类的析构函数…

【JavaSE专栏5】Java 基本数据类型和取值范围

作者主页&#xff1a;Designer 小郑 作者简介&#xff1a;Java全栈软件工程师一枚&#xff0c;来自浙江宁波&#xff0c;负责开发管理公司OA项目&#xff0c;专注软件前后端开发&#xff08;Vue、SpringBoot和微信小程序&#xff09;、系统定制、远程技术指导。CSDN学院、蓝桥云…

Mine Goose Duck 0.2版本发布

本次我增加了模组的1.16.5和1.18.2的适用版本&#xff0c;新增了一些职业和装扮 1.新职业 1.冒险家 你不会死于摔伤、溺水、火烧、冰冻。 2.工程师 你可以修改888范围内红石设备的状态。 3.模仿者 怪物认为你是他们的一员。 4.加拿大鹅 你会自动报警并召唤警车。 5.…

深度卷积对抗神经网络 基础 第三部分 (WGAN-GP)

深度卷积对抗神经网络 基础 第三部分 (WGAN-GP&#xff09; Wasserstein GAN with Gradient Penalty (WGAN-GP) 我们在训练对抗神经网络的时候总是出现各种各样的问题。比如说模式奔溃 (mode collapse)和 梯度消失&#xff08;vanishing gradient&#xff09;的问题。 比如说…

在linux下安装docker

文章目录 目录 文章目录 前言 一、docker 二、使用步骤 1.环境准备 2.安装 三、配置阿里云镜像加速 四、卸载 总结 前言 一、docker 镜像&#xff08;image&#xff09;&#xff1a; docker镜像就好比是一个模板&#xff0c;可以通过这个模板来创建容器服务&#xff0c;tomc…

【攻坚克难】详解k8s持久化存储数据pv、pvc存储问题

问题 如图:pod中的容器,创建一个包含文件的目录,重启pod或系统重启后,此目录及其文件都会丢失,如何保证其不会丢失? 图 1 创建包含文件的目录 方法 分析:用pv、pvc为k8s持久化存储数据是最好的选择,可解决上述问题。流程:pv → pvc → pod把创建的目录挂载到pvc上步…

路由 OSPF 优化(FA地址、路由汇总、路由过滤、区域认证、接口认证)

1.2.0 路由 OSPF 优化&#xff08;FA地址、路由汇总、路由过滤、区域认证、接口认证&#xff09; 一、FA地址 该文章介绍的FA地址说辞简单易懂&#xff1a;路由协议系列之六&#xff1a;OSPF FA地址 产生条件 ASBR在其连接外部网络的接口&#xff08;外部路由的出接口&#xf…

CS61A 2022 fall HW 01: Functions, Control

CS61A 2022 fall HW 01: Functions, Control 文章目录CS61A 2022 fall HW 01: Functions, ControlQ1: A Plus Abs BQ2: Two of ThreeQ3: Largest FactorQ4: HailstoneHW01对应的是Textbook的1.1和1.2 Q1: A Plus Abs B 题目&#xff1a; Fill in the blanks in the following f…

Java | 解决并发修改异常问题【CurrentModificationException】

今日碰到Java中的一个异常&#xff0c;名为CurrentModificationException&#xff0c;从属于RunTimeException运行时异常&#xff0c;故作此记录 异常解析 首先来说明一下什么是【并发修改异常】❓ 因为迭代器依赖集合而存在&#xff0c;因为当你在操作集合中元素的时候&#…