当前位置: 首页 > 后端技术 > Java

面试要点:Java中hashCode()和equals()的关系

时间:2023-04-01 15:21:50 Java

Java中hashCode()和equals()的关系是面试中常见的考点。很难回答。除了应对面试,了解两者的关系,有助于我们写出高质量准确的代码。一、基础知识:hashCode()和equals()简介在学习hashCode()和equals()的关系之前,我们需要分别了解一下它们的特点。equals()方法用于比较两个Object是否相等,这与==相等比较运算符有本质区别。在万物皆对象的Java系统中,系统赋予了程序员判断对象是否相等的权力。具体措施是将equals()方法写到Object类中,让所有的类都继承Object类。这样,程序员就可以重写自定义类中的equals()方法,实现自己的比较逻辑。hashCode()hashCode()表示哈希值,哈希值是哈希函数运算后得到的结果,哈希函数可以保证相同的输入可以得到相同的输出(哈希值),但不能保证不同的输入总是产生不同的输出。当输入的样本量足够大时,会发生哈希冲突,即不同的输入产生相同的输出。撇开冲突不谈,相同的输入可以产生相同的输出这一事实非常有价值。它使系统在时间复杂度O(1)的情况下,仅通过简单的计算就可以得到数据的映射关系。根据这一特点,哈希表应运而生。一个主流的哈希表实现是:用一个数组作为哈希函数的输出域,输入值通过哈希函数计算得到哈希值。然后根据哈希值在数组中找到对应的存储单元。当发生冲突时,对应的存储单元以链表的形式存储冲突的数据。2.谈:先了解hashCode()和equals()的关系下面我们从宏观的角度来讨论一下hashCode()和equals()的关系。在大多数编程实践中,它归结为数据访问问题。在汇编语言时代,每个数据操作都需要老老实实的写访问语句。时代发展到今天,我们都是用更方便灵活的高级语言编写代码,比如Java。Java以面向对象为核心思想,封装了一系列操作数据的API,降低了数据操作的复杂度。但是在我们对数据进行操作之前,必须先将数据按照一定的数据结构保存在存储单元中,否则就没有办法对数据进行操作了。但是,不同的数据结构各有特点。在存储数据时,我们需要选择合适的数据结构进行存储。Java提供了丰富的基于不同数据结构的容器类,方便程序员选择适合业务的容器类进行开发。通过继承关系图,我们可以看出Java的容器类分为两类:Collection和Map,而Collection又可以进一步分为List和Set。其中,Map和Set都不允许元素重复。严格来说,Map存储的是key-value对,不允许重复的key-value。值得注意的是,大部分Map和Set的实现类底层都会使用哈希表结构。说到这里,我们提取出两个不允许重复的关键字和哈希表结构。回头看看hashCode()和equals()的特点,是不是想到了什么?3.解密:深入理解hashCode()和equals()的关系。当equals()会无能为力时,上面说了Set和Map不存储重复的元素(键)。这些容器在存储元素时必须存储元素。判断一下:当前容器中是否有与新元素相同的元素?你可能会想:这很简单,直接调用元素对象的equals()方法进行比较不就可以了吗?如果容器中存储的对象数量很少,这确实是个好主意,但是如果容器中存储的对象达到一定大小,调用容器中所有对象的equals()方法就不是一个好主意了与新元素进行比较的容器。这很容易。即使equals()方法的比较逻辑极其简单,一般也是一个时间复杂度为O(n)的操作。hashCode()事半功倍,但基于哈希表,判断“新对象是否与现有对象相同”就容易多了。由于每个对象都有自己的hashCode(),这个hashCode将作为哈希表哈希函数的输入。hashCode是通过hash函数计算得到一个hash值,新的对象会存储到相应的内存单元中。我们不妨假设,对于两个相同的对象,其hashCode()一定是相同的,这样hash函数的强大就体现出来了。由于相同的输入肯定会产生相同的输出,如果新对象与容器中现有对象相同,则新对象计算的哈希值会与现有对象的哈希值发生冲突。这时容器可以判断:新增的元素已经存在,需要单独处理:覆盖原有元素(key)或丢弃。按照这个思路,如果这个元素计算出的hash值对应的内存单元没有冲突,也就是没有重复的元素,那么就可以直接插入。因此,使用hashCode()时,判断是否存在相同元素的代价仅为一次哈希计算,时间复杂度为O(1),大大提高了数据存储性能。Java设计equals()、hashCode()约定规则我们前面也提到:当输入的样本量足够大时,不同的输入会产生相同的输出,即形成hash冲突。这很麻烦。我们设定的“如果有冲突,就说明两个对象是一样的”这个规则瞬间被打破了,因为冲突的很可能是两个不同的对象!可喜的是,除了hashCode()方法,我们还有一张王牌:equals()方法。也就是说,当两个不同的对象发生哈希冲突时,我们可以通过equals()方法进一步判断这两个对象是否相同。这时候equals()方法就很重要了。在这种情况下,它必须能够确定这两个对象不是同一对象。说到这里就引出了Java编程中的一个重要原则:如果两个对象相等,那么它们的equals()方法应该返回true,而它们的hashCode()需要返回相同的结果。但是有时候面试不会问的那么直接,他会问你:两个对象的hashCdoe()是一样的,它的equals()方法一定返回true吧?这个答案肯定不对。因为我们不能保证每个程序员都会遵循编码约定。有可能两个不同对象的hashCode()返回相同的结果,但由于它们是不同的对象,它们的equals()方法将返回false。如果理解了上面的内容,这个问题就很容易回答了,我们再复习一下:如果两个对象的hashCode()相同,以后哈希表就会发生哈希碰撞,但不一定是同一个对象。当发生哈希冲突时,我们还得通过equals()方法进一步判断两个对象是否相同,而equals()方法不一定返回true。这也是为什么Java官方建议我们在一个类中同时重写hashCode()和equals()方法的原因。4、验证:结合HashMap的源码和官方文档,验证两者的关系。以上文字是我思考后得到的。有一定的依据,但不完全可靠。下面我们根据HashMap的源码(JDK1.8)和官方文档来验证这些推论是否正确。通过阅读JDK8的官方文档,我们发现在介绍equals()方法的最后有这么一段话:注意,一般在重写这个方法的时候都要重写hashCode方法,这样才能维护hashCode方法的一般协定,规定相等的对象必须具有相等的哈希码。官方文档提醒我们,重写equals()方法时,最好也重写hashCode()方法。也就是说,如果我们通过重写equals()方法来判断两个对象相同,那么它们的哈希码也应该相同,这样hashCode()方法才能发挥作用。那么它具体能做什么呢?我们进一步分析一些比较常用的HashMap源码。(像HashSet底层也是通过HashMap实现的)put()方法无疑是HashMap中使用最多的。下面是put()的源码:publicVput(Kkey,Vvalue){returnputVal(hash(key),key,value,false,true);}我们可以看到put()方法实际上调用putVal()方法,继续跟进:finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node[]tab;节点p;诠释n,我;//当我们创建HashMap对象时,内存中并没有为HashMap分配表空间,直到HashMapput添加元素时,调用resize()方法初始化表if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;//同时确定表的长度//((n-1)&hash)确定放置元素的位置,如果要插入的地方为空,可以直接插入。如果((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{//如果有冲突,则在冲突位置的链表尾部插入元素Nodee;Kk;if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))//key!!!判断新增元素是否与已有元素相同时,先判断hash值,后面调用equals()方法。如果hash值不同,则直接跳过e=p;elseif(pinstanceofTreeNode)//如果冲突解决变成了红黑树,按照红黑树的策略添加节点。e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);else{//解决冲突的方式还是链表for(intbinCount=0;;++binCount){//找到链表的末尾,插入。if((e=p.next)==null){p.next=newNode(hash,key,value,null);if(binCount>=TREEIFY_THRESHOLD-1)//-1for1sttreeifyBin(tab,hash);//插入后需要判断链表长度。如果达到一定的值,就可能转化为红黑树。休息;}//在遍历过程中,会不断判断当前key是否与传入key相同,判断第一个条件还是hash值。如果(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}if(e!=null){//键VoldValue=e.value的现有映射;如果(!onlyIfAbsent||oldValue==null)e.value=值;节点访问后(e);返回旧值;}}++modCount;//增加修改map的次数节点插入后(逐出);returnnull;}我们可以看到,每当判断key相同时,会先判断hash值,如果hash值相同(发生冲突),再判断,同理,最后判断会通过equals()方法进行。如果key的hash值不同,则不执行后面的判断,直接判断两个对象不同。if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;5、希望大家对结尾的hashCode()和equals()方法有更深的理解,理解其背后的设计思想和原理。之前有一个疑问,也许大家看了这篇文章后会有疑问:我平时使用equals()方法,所以我知道它除了和hashCode()方法关系密切之外,还有其他用途。但是hashCode()呢?除了与equals()方法密切相关之外,它还有其他用途吗?在网上搜索了一下,目前我给出的答案是否定的。也就是说,hashCode()只在哈希表中有用,其他情况下没用。当然,如果这个回答不正确,或者有其他想法,欢迎留言与我交流~hashCode()和equals()你了解了吗?