Java面试基础问题之(九)—— 也说hashCode() 和 equals()

一 为什么还要写

关于这两个方法的比较,解释特别多,但是大部分都是复制粘贴,把hashCode()的JavaDoc一条一条翻译过来,但如果问为什么有这些规定,这些规定有什么意义?恐怕很多作者自己也不知道为什么。这里尽我的理解好好写一篇二者的介绍与比较,顺便自己捋捋思路。

二 最初的起源

这两个方法最初都来源于Object:

hashCode():

equals:

可以看到,二者都是public,所以可以被子类覆写,分开来看:

1)hashCode()

hashCode()的方法是native,返回值是int,所以如果直接调用Object的hashCode()或者调用一个没有覆写hashCode()的类的hashCode(),那么输出的结果是“程序员无法控制的”(native)的int类型的数字,e.g:

Object:

自定义的Student,没有覆写hashCode()方法:

这里,stu.hashCode()实际上调用的是其父类Object的hashCode方法。

那么hashCode()返回的这个int类型值是怎么计算出来的呢,代表什么意思呢,两个不同对象的hashCode()会一样吗?看官方JavaDoc:

很清楚:hashCode()的返回值是由对象内部地址(internal address)转化而来的,不同的对象返回不同的整数值(由Java本身来保证)。

值得注意的是,Object的另外一个方法——toString()调用了hashCode()方法:

从代码可以看出,toString()方法的返回值是String类型,并且格式为“className@hashCode(16进制)”的形式。我们知道System.out.println(oj)会自动调用oj的toString()方法,二者一结合,下面的结果在预料之中:

很明显,从代码知道,输出结果中@后面的数字就是该对象的hashCode()返回值。

当然,如果一个类(必然是Object子类)没有覆写toString(),则打印(默认调用toString())时必然调用的还是Object的toString()即“className@hashCode(16进制)”,这一点和hashCode()相似:

 

2)equals()

实现非常简单,但是这个:

又是什么意思呢, “==” 的确切含义是什么,换言之,“this == obj”的值什么为true,什么时候为false呢。

当然,作为马后炮,我们基本都知道这样的“结论”——如果两个引用指向同一个对象,则值为true,否则为false。但是思考一下这样的问题:

如果一个父类引用指向其子类对象,另外一个子类引用同样指向该对象,则二者相等吗?实验说话:

其实这个代码有点冗余,为了形象化“指向同一个对象”的意思,用恶汉式的单例模式返回一个创建好的Student对象,保证父类引用obj和子类引用stu指向的都是一个“new Student()”,其实下面的代码和上面完全是等效的:

(废话,“Object obj = stu"了,stu.equals(obj)即 stu == obj 的结果当然是true了)

那么,为什么结果是true呢 —— “==”的确切含义是啥?

或者说我们之前那个结论到底怎么来的——“如果两个引用指向同一个对象,则“”==“值为true,否则为false”的理论依据在哪?

还是看equals的JavaDoc说明:

说的很清楚:当且仅当 x,y指向同一个对象时,x == y的值为true。当然,x,y不为null(指向了对象)。

这才是学习的正确姿势——没有什么理所当然的结论,凡事讲依据。在没有办法干预和检查“==”具体实现的情况下,参考官方文档,这是Java设计者们给客户端程序员的官方依据。

好了,到这里两个方法的最基本的情况介绍完了。接下里就是经典的“hashCode() 和 equals()的覆写规则”问题。

二 hashCode() 和 equals()的覆写规则

关于这个问题有很多“问法”,e.g.

equals为true的对象hashCode()一定相等吗,hashCode()相等的对象equals()一定为true吗?

hashCode()相等的对象一定是同一个对象吗,equals()为true的对象一定是同一个对象吗?

上面还是比较规范的表达,更为常见的类似这样—— hashCode() 相等的对象一定相等吗,equals()相等的对象一定相等吗?

这种读起来就很难受的问法在很多所谓的“面试题总结”里都有——“hashCode() 相等的对象一定相等吗”,这里你甚至都不知道第二个“相等”的定义是啥,没头没尾的抛出个“若XXX则两个对象相等吗”的问题是非常模糊而又不专业的。

表达方式就不多谈,看一下一般给出的“答案”:

equals()相等的两个对象他们的hashCode()肯定相等,hashCode()相等的两个对象他们的equals()不一定相等。

或者“难受”的表达方式:

对象相等,hashCode()一定是相同的,但是hashCode()相同,对象不一定相等。

那么这是怎么来的呢?很多人可能会说,Java设计者们设计好的,你管那么多干嘛,记住结论就好了,你为什么不问为啥0是一个圈而不是个三角呢,这是设计者们“定义”好的!

真的是这样吗,从之前的内容我们看到hashCode()和equals()都是Object的public方法,我们可以并且经常被“强烈建议”覆写它们。问题来了—— 如果我们覆写了hashCode()和equals()——按照自己的想法覆写了hashCode()和equals(),其返回值是我们自己控制的,又怎么会有上面“equals()相等的两个对象他们的hashCode()肯定相等,hashCode()相等的两个对象他们的equal()不一定相等”的“定律”呢?

看一下面这个极端的例子:

对于这样一个自定义的Student类,两个不同的Student对象equals()方法总是返回true,hashCode()却不一定相等(实际上上相等的几率为1/100)。

那前面那个“金科玉律”呢?—— equals()相等的两个对象他们的hashCode()肯定相等,hashCode()相等的两个对象他们的equal()不一定相等

答案是,根本没有这样的“金科玉律”存在!

那么为什么出现这样的“结论”?

答案是 —— 生搬JavaDoc的内容,而忽略了没有其上下文环境,实际上这是Java设计者给程序员覆写hashCode()和equals()的“建议”,而不是“结论”。

上面很重要,记住:没有这样的结论!

 

那么,一.为什么Java设计者强烈建议要覆写hashCode()和equals()?二.为什么覆写要遵循这些原则?

记住,没有任何想当然的结论,既然这么建议,肯定有原因。

因为不管有意还是无意,我们都很有可能使用到底层依赖于hashCode()和equals()实现的数据结构——Set和Map。关于更“愚蠢的错误”——想当然地调用“equals”方法判断两个对象是否相等——我们把想当然地认为:Java已经为我们实现好了equals(),并且就是“我们想的”那个equals()——我们自己脑子中的对于两个对象相等的定义,这些就不提了。

那么问题来了,为什么使用Set和Map(当时指的是具体的实现类,种类太多用接口代指)就要覆写hashCode()和equals(),覆写遵从什么规则,为什么要遵从这些规则?

我们不妨自顶向下来看,先看Java给的覆写hashCode()和equals()的建议

先看equals覆写建议:

覆写equal()和hashCode()关系(equals()方法注释中):

即:任何时候,覆写equals方法时,一定要覆写hashCode()方法。

hashCode()覆写建议:

这就是之前那个臭名昭著的结论 —— “对象相等,hashCode()一定是相同的,但是hashCode()相同,对象不一定相等”的来源。

但是注意一行字:are equal/unequal according to the {@code equals(Object)}

即这个字面的上相等/不相等(equal/unequal)是根据equals(Object)来精确判定的,而不是口语化模糊的相等/不相等。

当然,个人认为JavaDoc有一点做的不好:无论是equals列出来的“四大特性”+一定要同时覆写hashCode(),还是覆写hashCode()的三条规约(contract),都没有说明,这是Java设计者的“建议”,而不是Java已经实现了的对程序员的“保证”。注释中没有用should,suggest等这样的建议性词汇,相反都是描述性的陈述句,就像我们给一个方法添加的普通注释一样——一般都是对这个方法用法,特性的说明,这样很容易给阅读者特别是初学者造成误解:这是这个方法本身固有的特性。

再看equals()中:

即equals和the same object具有等价性(内部由“==”保证的)。

此时结合二者就很容易得出:                      

同一个对象 < —— > equals为true(if only if——双向约束)—— >  hashCode()相同(注释中只正向约束)                       

即Java要求(强烈建议),是否为同一个对象和equals()是否为true具有等价性,如果是同一对象(equal()为true),则hashCode()一样,反之不做要求。

好了,到这里hashCode()和equals()覆写时Java给的规范(强烈建议)基本清晰了,理顺之后就成了:

equals()为true,则hashCode()一定相等,反之不成立。

三 为什么equals()为true,则hashCode()一定相等

凡事都有个为什么,之前已经提到,Java对hashCode()和equals()的约束其实都是应为Set和Map的底层实现依赖于二者的实现。那么就让我们看源码一探究竟。

直接看HashMap(HashSet也是利用HashMap实现)的put方法:

看putVal():

① ②就不多说了,看③。

首先注意③的进入条件:table[]初始化完毕(非①),并且  tab[(n-1) & hash])  != null,(n-1) & hash其实是取hash的低k位,k刚好为table[]的长度。之所以用这样方式,因为已经保证了每次resize时n都是2的幂次方,&的方式取hash的低k位比取余操作 hash%n要快的多。好了这里不多说,继续看代码。

也就是说,在检查到由hash算出来的位置上已经有元素时,进入③。

注意,两个元素hash算出来相等(因为已经占据这个位置的元素hash值必然等于当前桶下标),不代表元素的Key就相等。别忘了,hash这个值并不是Key,而是由Key算出来的。我们使用HashMap put元素时,put进去的是对象,e.g

然后hash根据K(Student对象)被算出来:

看hash方法:

hash是使用该对象的hashCode()方法算出来的。

回到原来的问题:

这里进入了③,只能说明当前要put进来的K(对象类型),它的hashCode()和之前占据桶的hashCode()是一样的。至于二者其他属性是不是一样,并不知道

而对于一个设计正常的哈希结构,此时要判断“到底是塞进来同一个元素(和之前占据桶的元素一模一样,或者说就是占据桶的那个元素),还是只是hash值(也就是hasCode())一样而已”,对于前者,策略是直接不管了,不进行操作;对于后者,策略是使用拉链法或者红黑树塞在已经占据桶元素的后面。

再看红框的部分:

注意它的判断条件:if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

除去赋值的部分,翻译一下,即:

if (p.hash == hash && (p.key == key || (key != null && key.equals(p.key))))

即,如果已经检测到当前桶被元素占据(进入了③),则首先判断:

1)二者hash是否相同(也是hashCode是否相同,其实这一步应该是加强判断,结果必然我true的)

2)hash相同,则判断二者是不是同一个对象(p.key == key)如果是同一个对象,则可以确定后面不用处理了

3)如果二者不是同一个对象,则使用equals()方法判断二者是否“相等”。如果相等,则认为二者是“一样的元素”——不用重复put的元素。故后面不用处理了。

如果都不是,则说明要put的元素和原来占据桶的元素不等(由equals()定义的不等),只是二者hash(可以认为就是hashCode())相同而已,即发生了哈希冲突,后面用相应的哈希冲突解决方法来解决就好——以某种方式塞入当前这哦Key。

这里有个问题,既然equals()方法“规定”中已经有了反身性:x.equals(x)必然为true,对象的引用和自己equals的结果肯定是true,怎么为什么还要判断p.key == key呢?:

原因在于,这样可以减少由于程序员没有满足反身性的equals写法带来的性能开销。

首先对于反身性,它也只是Java的强烈建议,并不是“结论”,说白了,equals()的具体实现有程序员负责,四大特性只是Java对程序员的强烈建议和指导规范——你的equals写出来最好符合这个四大特性,这样你用我的工具时才不会出错。但是对于没有满足反身性的equals方法,我HashMap可以稍微宽容你一下,我宽容你的原因在于反身性其实也只是达到快速判断的效果,对于equals()本身的逻辑没有太大影响。

上面说的太抽象,先看一个正面例子,String类的equals方法:

看到没,这个就是反身性,并且放在第一句,其实可以看到,对于

String str = new String("Hello");

str.equals(str);

删掉第一句反身性的判断,结果仍然为true,因为真正的判断逻辑中,必然要比较两个对象的各个属性等,一个对象自身的属性跟自身的属性必然是相等的。只是这样浪费了时间——只用判断首地址就好了:this == anObject,根本不用进去了。

当然,那种逆天的equals不再讨论范围之内,如:

或者:

此时加不加反身性绝对会改变:

Student stu = new Student ("Tom","male");

stu.equals(stu);

的结果,但是这种情况下满不满足反身性(开头加不加this == anObject)又有什么区别呢,它还不满足传递性呢。BTW,String的equals()也满足了 x.equals(null)的结果为false,平时写的时候要注意这一点,至于为什么null能作为参数传递进来,那是因为(任意类型)null都是可以的,换言之,Object.equal(null)能被系统识别为调用的是Object.equals(Object o)的方法

回到原来的话题:

这个(k = p.key) == key只是为了防止程序员忘了写反身性,而做的多余工作而已。

好了,现在再回看这个put方法,即hashCode()相同不能直接不管,而是要比较equals()是否为true,如果是true才认为两个对象是“相等”的,不用重复塞入。如果为false则需要塞入。即equals定义了两个对象的“相等”,而hashCode()不是。

这时候再看:

equals()为true,则hashCode()一定相等,反之不成立。

就很明白了,这都是HashMap的实现逻辑决定的,并不是什么天经地义。如果说有一个HashMap用的不是equals方法而是isSame()方法,那么equals和hashCode()根本就没有了约束关系。

 

至此,已经全部结束。可以稍微总结一下:

1)Object的hashCode()可以看做转为int值的对象地址(internal address),由Java保证不同对象的hashCode()不同,Object的equals()实现仅仅是 return obj == this。由Java来保证 == 的判断就是是否指向同一个对象(the same object)。可以这么说,Object的hashCode()和equals()具有等价性。

2)Java只提供了覆写hashCode()和equals()的“指导性规范”,具体的实现由程序员实现,但是程序员应该是自己的程序满足这些要求,以保证程序运行特别是使用了HashMap之后的健壮性。

 

如果只是面试题:

如果两个对象相等,则equals()的结果是?hashCode()相等吗?

如果hashCode()相等,则equals()结果?如果equals()为true,则hashCode()相等吗?

答:这个要看具体实现,所有类都可以按自己需求覆写hashCode()和equals(),换言之,程序员完全写出没有任何约束关系的hashCode()和equals()。但是为了使用HashMap等数据结构,Java给了指导性规范,具体有:xxxxxx,并且对于二者的约束,有:

equals()为true,则hashCode()一定相等,反之不成立。

这也是由HashMap的底层实现决定的,具体有:XXXX。

 

 

题外话,其实这篇文章对于初学者还有点用,对于已经了解了Java的“建议”的程序员们可能就太小儿科了,又臭又长。文章的全部内容其实都来自源码及注释文档,认真看注释就一切都明白了,根本不用这么啰嗦地看这么多。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值