重写equals()方法之后一定要重写hashcode()方法? 附HashMap简要介绍

面试中极其常见的问题,写篇文章,留作复盘时用

问题的提出

之前看(背)面经的时候,记住的是这是Java的要求,所有Java提供的类库中只要重写了equals()那么他一定都重写了hashcode(),于是对于这个问题的原因我便没有深究。

现在再仔细想想这个问题,首先想到的原因就是,equals()方法的实现需要使用到hashcode()方法的结果,但是显然这是不对的,equals()的实现目的是根据使用者的需求,在类中重写继承自Object类的equals()方法使其满足使用者的相同比较规则。以String类中重写的equals()方法为例,其比较的是String中value字符数组值两两是否相等。所以equals()方法的实现并不依赖于hashcode()。

那么equals()与hashcode()之间到底有什么关系呢,才会要求重写equals()方法之后一定要重写hashcode()方法?

先分别介绍下equals()与hashcode()

equals()方法

equals常用来比较两个对象是否相同
是Object父类中的方法,所以所有子类都会继承这个equals()方法。在Object中的equals方法只是简单的比较两个对象的引用是否相同,相同则返回true,不同则返回false。
当然在不同的使用场景中,对于equals比较的规则不尽相同,所以不同的类会根据需求重写equals()方法,以使其符合本类中比较的场景,上述提到的String类便是重写的equals()方法。

hashcode()方法

hashcode用来返回对象的hash值

这样看来equals()与hashcode()之间似乎没有什么关系。但是我们查看Object类源码,在hashcode()方法的注释里开发人员写到
下面截取了一小部分的注释,想看完整注释的可以直接打开Object类,搜索hashcode便能找到了

/**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * .......
     */
    public native int hashCode();

This method is supported for the benefit of hash tables
翻译过来是,hashcode()方法是为了提升哈希表的性能

所以我们知道了离开哈希表来谈论这个“重写equals()方法之后一定要重写hashcode()方法”问题,是没有意义的。

要试图搞清楚为什么“重写equals()方法之后一定要重写hashcode()方法”,那么我们必须去了解Java中哈希表的存储结构。

HashMap

下面以Java中最为常见的哈希表的实现HashMap为例,介绍一下equals()与hashcode()是如何在键值对插入时发挥作用的。
(这里只会对HashMap的结构,插入操作做简要介绍,后面估计会写一篇文章详细的介绍HashMap。估计~)

HashMap的结构

HashMap的实现结构是数组+链表+红黑树(链表是用来处理hash冲突的,而红黑树是jdk1.8之后引入的,用以解决链表过长的情况下查询效率的问题,可以先只关注数组部分)。

下面是HashMap的结构示意图
在这里插入图片描述

大家都知道HashMap中存储的是一对信息,即键值对,而键值对是存储在一个叫做Entry的接口中的。HashMap内部的数组元素被视作一个一个的桶,而Entry的对象加上一个一个指针,一起被存放在一个数组元素里,这样不同的键值对放入不同的桶中则组成了HashMap。

首先我们知道,用来存放大量相同类型的数据我们可以用数组来存储。但数组的缺点很明显,我们可以按照顺序的向数组中插入我们要存放的数据,但当我们要找某一个数据的时候,我们除了挨个遍历数组中的元素,似乎没有更好的方法。

这个时候散列表(哈希表)的思想便被提了出来,我们可以根据要存储的数据,根据特定的规则计算出一个hash值来,然后根据该数据的hash值我们来确定其在数组中唯一的插入位置,现在这个计算hash值的方法便被命名为hashcode(),并被定义在了Java中所有类的父类Object类中。

当然,作为哈希表在Java中最为常用的实现类HashMap,也遵循的上述的思想。向HashMap中插入键值对的时候,首先要找到在数组中的插入位置,而插入位置的得来便会使用到key类的hashcode()方法。

下面贴几段HashMap的源码

//斗胆自己写下注释
//这是我们最常用的向HashMap中插入键值对的方法
//他其实调用的是putVal方法
//而putVal中使用到了一个叫做hash(key)的方法来计算出一个处理过的hash值

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

//这就是hash(key)方法的具体实现
//不难看出使用到了key重写的hashcode方法
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

由上述代码我们知道了hashcode()方法是如何在HashMap中发挥作用的。但是还是没有回答问题“重写equals()方法之后一定要重写hashcode()方法”,下面我来回答这个疑问。

解答

在我们没有重写equals()方法之前,如果两个对象利用equals()方法比较相同,那么他们用hashcode()方法计算出来的hash值也一定是相同的。

但是如果我们重写了equals()方法并没有重写hashcode()方法则可能会出现这样的情况,当依照我们给定的规则,两个对象进行比较,对象1.equals(对象2),得到的结果是两个对象相同,但调用hashcode()方法分别计算两个对象的hash值可能会是不同的。

而这时如果我们将这两个对象插入HashMap中,HashMap插入第一个对象时并不会出现问题,正常插入。

当插入第二个对象的时候,HashMap会先去判断Map中是否已经有了这个元素(因为HashMap中是不允许存放相同元素的),他会先根据第二个插入元素的hash值计算出第二个元素本来应该插入的位置,然后去查看这个位置上是否已经有元素了,如果有便会调用equals()方法判断这两个元素是否相同(因为可能会存在hash冲突的情况,使得多个不同的对象被散列到数组的同一个位置,构成链表或者红黑树),如果不相同就插入元素,如果相同则用这个元素覆盖之前的元素。

当然这是在重写了hashcode()方法,能够找到元素插入的正确位置的前提下,才能实现的。而现在情况是,虽然你的两个对象是相同的,按照规则HashMap是不允许插入相同元素的,而你们两个对象hashcode()方法计算得到的hash值却不同,上述的元素是否重复的检测逻辑根本就无法正常实现,你就会将两个相同的对象都插入了HashMap中,并存放在不同的位置。

这就是为什么,我们重写完equals()方法之后一定要重写hashcode()方法,用以保证equals()方法认为相同的对象,他们的hashcode()方法计算得到的hash值一定相同。

(上面逻辑写的有点混乱,大家将就着看吧~)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值