为什么重写equals方法时必须重写hashcode方法

2023-10-27


相关文章:
为什么重写equals方法时必须重写hashcode方法
Java字符串不相同但HashCode相同的例子(算法)
深入分析Java中打印对象内存地址 System.identityHashCode()方法

1. == 与 equals的区别

  • 如果两个引用类型变量使用==运算符,那么比较的是地址,它们分别指向的是否是同一地址的对象,结果一定是false,因为两个对象地址必然不同。

  • ==不能实现比较对象的值是否相同。

  • 所有对象都有equals方法,默认是Object类的equals,其结果与==一样。

  • 如果希望比较对象的值相同,必须重写equals方法。

Object 类源码:

public class Object {
     public native int hashCode();
     
     public boolean equals(Object obj) {
        return (this == obj);
    }

从源码上可以看到,hashCode()和equals()都和地址相关,因此 new 出来的2个实例,a.equals(b)一定返回false。

2. 重写equals()

重写equals()的作用是什么?

如果希望比较对象的值相同,必须重写equals方法。这样可以在equals内自定义比较条件:
在这里插入图片描述
如上图所示,自定义的equals()比较条件是Student类的属性,只要姓名、年龄、QQ号都相同,则认为是同一个人。

这样是有很大作用的,例如一个Set<Student> 可以避免把同一个人加入2次。

但是这样是存在问题,涉及到hashCode

3. 为什么重写equals方法时必须重写hashcode方法?

当确定一个对象不会用在集合中时,可以仅重写equals方法不必重写hashcode方法;如果会用在集合中,那么二者必须同时修改,否则会有问题

本来equals方法和hashcode()是不相关的,但是由于我们会在集合中放入我们创建的对象,这样由于存储结构的问题,导致在查找时二者产生了关联。

我们举个例子:

class MyObject {
    private String name;

    public MyObject(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {  //重写了equals方法
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyObject myObject = (MyObject) o;
        return Objects.equals(name, myObject.name);
    }
    
//    @Override
//    public int hashCode() {
//        return Objects.hash(name);
//    }    

    public static void main(String[] args) {
        Map<MyObject, Integer> map = new HashMap<MyObject, Integer>();
        MyObject m1 = new MyObject("a");  //new MyObject("a");创建一个实例
        map.put(m1, 1);  //以该实例为key,存入一个值,1

        MyObject m2 = new MyObject("a");  //new MyObject("a");创建一个实例
        Integer value = map.get(m2);

        if (value != null) {
            System.out.println(value);
        } else {
            System.out.println("value is null");  //进入该分支,打印
        }
    }
}

程序运行结果: value is null

我们往一个HashMap中,放入一个自定义对象 MyObject(“a”)的实例,假设我们重写了equals,然后通过get()方法获取,为何结果为空?

hashmap是一个键值映射的集合类,存储结构为Key+Value,优点是方便快速查找,通过key可以快速找到Value,在存入一个Key+Valuehash元素时,需要先计算Key对应的hash值(计算过程称之为hash算法),hash值决定了最终存放的位置,我们称这个位置为HeadNode,并且如果产生hash碰撞,一般会采用链式结构扩展这个HeadNode,这个链也常称作桶,允许包含一组值。

当key是一个对象类型时,,例如MyObject,我们需要重写equals()方法,此时2个对象的值相等等价于这2个对象是同一个对象,存在MyObject的2个实例对象,m1和m2,我们认定这俩对象”相等“,先利用m1作为key,存入hashmap,值为v1,此时,我们期望通过m2也能从hashmap中得到v1,在不重写hashcode的情况下,get(m2)会返回null ,为什么?

原因是hash值的计算借助hashcode方法,该方法的默认算法实现和对象的地址有关,而m1和m2被创建后,内存地址肯定不同,因此,在没有重写hashcode()方法时,所有的对象存入hashmap后,m1的hashcode是h1,m2的hashcode是h2, h1!=h2 ,那么get(m2) 就无法找到h1位置,也就无法返回v1

重写hashcode()方法目的是确保h1=h2,那么就get(m2) 就能找到h1位置,也就可以返回v1

在这里插入图片描述
如上面的图,如果二者同时修改的话,那么 h1和h2相同,那么就可以在同一个链中找到值相同的对象;否则仅重写equals方法不重写hashcode方法,h1和h2不同,就落到不同的链上,那么就找不到了;就像牛郎和织女记错了鹊桥的位置,那么二者也无法相会,令人唏嘘。

HashMap的get()方法源码

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
//入口
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    //间接调用 getNode
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {   //计算hash,如果不重写,那么hash的值就和内存地址相关
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

3.1 Hash算法

先用一张图看下什么是Hash
在这里插入图片描述
Hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。关于散列值,有以下几个关键结论:

  • 如果散列表中存在和散列原始输入K相等的记录,那么K必定在f(K)的存储位置上 ,即多次计算hash值一定相同
  • 不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞
  • 如果两个Hash值不同(前提是同一Hash算法),那么这两个Hash值对应的原始输入必定不同

3.2 HashCode()

然后讲下什么是HashCode,总结几个关键点:

1、HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的

2、如果两个对象equals相等,那么这两个对象的HashCode一定也相同

3、如果对象的equals方法被重写,那么对象的HashCode方法也一定重写

4、如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置

即hashmap的底层实现,为了实现方便的查找,采用数组+链表,数组是hash的结果,链表是碰撞的解决方案。

这样,当在hashmap里面查找某个对象时,先通过hash快速找到对应的链表,然后在链表上逐个进行equals()比较,否则,需要在所有的元素上逐一进行比较,时间复杂度比较差。

先找链,再在链上进行值的比较

因此,如果改写了equals(),而不改写hashcode的话,Object内默认hashcode()方法必定不同的(new 出对象的地址一定不同),这样hashmap存储的2个对象,都在不同的链上,这样无法进行equals()比较。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么重写equals方法时必须重写hashcode方法 的相关文章

随机推荐

  • MySQL数据库入门实战教程

    目录 前言 一 创建建数据库 创建建数据表 查看数据库 查看数据表 二 新增 修改 删除表记录 三 基础查询 where子句查询 1 基础查询 2 WHERE子句查询 3 Like模糊查询 四 分组查询 聚合函数 排序查询 4 排序查询 5
  • 子集和问题

    子集和问题 描述 Description 问题描述 子集和问题的一个实例为 S t 其中 S x1 x2 xn 是一个正整数的集合 c是一个正整数 子集和问题判定是否存在S的一个子集S1 使得子集S1和等于c 编程任务 对于给定的正整数的集
  • 【数据结构】堆的实现(简单易懂,超级详细!!!)

    目录 1 堆的概念及结构 概念 规律 2 堆的实现 2 1结构设计 2 2接口实现 2 3 初始化 2 4堆的向下调整算法 主要思想 涉及问题 代码实现 2 5建堆 思想 代码实现 建堆的时间复杂度 2 6 堆的向上调整算法 主要思想 涉及
  • PyQt5常用模块、类、控件

    一 常用模块 QtCore 包含非核心的GUI功能 此模块用于处理时间 文件和目录 各种数据类型 流 URL MIME类型 线程或进程 QtGui 包括窗口系统集成 事件处理 二维图形 基本成像 字体和文本 QtWidgets 基本控件都位
  • Yii Framework 开发教程(33) Zii组件-Accordion示例

    Zii组件中包含了一些基于JQuery的UI组件 这些UI组件定义在包zii widgets jui中 包括CJuiAccordion CJuiAutoComplete CJuiDatePicker等 本篇介绍CJuiAccordion 显
  • ARM的37个寄存器和异常处理机制详解

    1 ARM的37个寄存器 ARM的37个寄存器中 30个寄存器是 通用 1个固定用作PC 程序控制寄存器 一个固定用作CPSR 程序状态寄存器 5个固定用作5种异常模式下的SPSR 程序状态保存寄存器 特别注意user模式和sys模式共用寄
  • SPI总线的原理

    一 简介 SPI Serial Peripheral Interface 串行外围设备接口 是Motorola公司提出的一种同步串行接口技术 是一种高速 全双工 同步通信总线 在芯片中只占用四根管脚用来控制及数据传输 广泛用于EEPROM
  • Unity PackageManager一直加载不到信息的解决方法

    关闭unity 断开网络连接 打开unity 这时候会加载出来 连接网络开始install
  • 牛客网——两数之和

    题目描述 给出一个整数数组 请在数组中找出两个加起来等于目标值的数 你给出的函数twoSum 需要返回这两个数字的下标 index1 index2 需要满足 index1 小于index2 注意 下标是从1开始的 假设给出的数组中只存在唯一
  • 解决error: subprocess-exited-with-error

    运行 pip install upgrade setuptools 即可解决
  • Playwright解决永久保存下载文件

    Playwright默认在浏览器关闭的时候 所有的临时文件都将删除 无论你是自定义位置还是默认位置 那么如何正确下载对应的文件呢 废话不多说 大家直接看以下代码即可 这里还是告诫大家一下 多研究官网的API文档 别学我慌慌张张去搞了 啥都没
  • QTP的那些事--项目实践操作案例代码--查询操作

    1 一下的代码记录的是我对于一个查询操作的自动化的思路 遗憾的是预期的结果可能需要手动输入到datatable中 以后逐步完善 将所有的预期值都自动输入到excel中 登陆系统 2 3 RunAction login loginsystem
  • HDS 存储名词解释

    DKU 扩展柜 DKC 控制柜 DKA 后端端口 CHA 前端端口 CSW 交换卡 SVP 内置服务PC 另一个含义是服务程序 CM Cache Memory数据内存 SM Share Memory共享内存 HDU Hard Disk Un
  • npoi全国计算机编程,NPOI 导入Excel

    NPOI 导入Excel public static List GetExcelToList string excelfileName List list new List ISheet sheet null PropertyInfo pr
  • Nginx进程管理

    Nginx进程管理 1 Nginx进程管理之master进程 监控进程充当整个进程组与用户的交互接口 同时对进程进行监护 它不需要处理网络事件 不负责业务的执行 只会通过管理worker进程来实现重启服务 平滑升级 更换日志文件 配置文件实
  • 利用lda对文本进行分类_使用lda进行文本分类

    利用lda对文本进行分类 LDA or Latent Dirichlet Allocation is one of the most widely used topic modelling algorithms It is scalable
  • Linux——gcc/g++以及make/Makefile的使用

    简介 在Linux的系统中 想要完成代码编译 gcc g 是不可缺少的工具 而make Makefile能否熟练应用则从一个侧面体现出一个人是否有能力独自完成一个大型工程 而本篇文章就带领大家了解一些gcc g 和make Makefile
  • 层叠上下文(stacking context)

    一 什么是层叠式上下文 层叠上下文 是HTML中的一个三维概念 如果元素具备以下任何一个条件 则该元素会创建一个新的层叠上下文 根元素 z index不为auto的定位元素 二 什么是层叠级别 同一个层叠上下文的背景色以及内部元素 谁在上谁
  • IPsec SA 创建步骤——IKE协议

    IPsec SA 创建步骤概述 IPsec SA creation steps There are two steps on the IPsec SA creation phase 1 is to creat IKE SA and phas
  • 为什么重写equals方法时必须重写hashcode方法

    文章目录 1 与 equals的区别 2 重写equals 3 为什么重写equals方法时必须重写hashcode方法 3 1 Hash算法 3 2 HashCode 相关文章 为什么重写equals方法时必须重写hashcode方法 J