为什么要重写equals()和hashCode()

前言

在面试 Java的时候,经常会被问的一个问题是:你有没有重写过 hashcode方法?不少候选人直接说没写过。或许真的是没写过,于是还可以再通过一个问题确认:你在用 HashMap的时候,键( Key)部分,有没有放过自定义对象?而这个时候,候选人说放过,于是两个问题的回答就自相矛盾了。

其实很多人这个问题普遍回答得都不大好,于是在本文里,就干脆 从 hash表讲起,讲述HashMap的存数据规则,由此大家就自然清楚上述问题的答案了。


Hash算法

先复习一下数据结构里的一个知识点:在一个长度为 n(假设是 10000)的线性表(假设是ArrayList)里,存放着无序的数字;如果我们要找一个指定的数字,就不得不通过从头到尾依次遍历来查找。

我们再来观察Hash表(这里的Hash表纯粹是数据结构上的概念,和Java无关)。它的平均查找次数接近于 1,代价相当小,关键是在Hash表里,存放在其中的数据和它的存储位置是用Hash函数关联的。

我们假设一个Hash函数是 x * x % 5。当然实际情况里不可能用这么简单的Hash函数,这里纯粹为了说明方便,而Hash表是一个长度是 11的线性表。如果我们要把 6放入其中,那么我们首先会对 6用Hash函数计算一下,结果是 1,所以我们就把 6放入到索引号是 1这个位置。同样如果我们要放数字 7,经过Hash函数计算, 7的结果是 4,那么它将被放入索引是 4的这个位置。这个效果如下图所示。

这样做的好处非常明显。比如我们要从中找 6这个元素,我们可以先通过Hash函数计算 6的索引位置,然后直接从 1号索引里找到它了。

不过我们会遇到“Hash值冲突”这个问题。比如经过Hash函数计算后, 7和 8会有相同的Hash值,对此Java的HashMap对象采用的是"链地址法"的解决方案。效果如下图所示:

具体的做法是,为所有Hash值是 i的对象建立一个同义词链表。假设我们在放入 8的时候,发现 4号位置已经被占,那么就会新建一个链表结点放入 8。同样,如果我们要找 8,那么发现 4号索引里不是 8,那会沿着链表依次查找。

虽然我们还是无法彻底避免Hash值冲突的问题,但是Hash函数设计合理,仍能保证同义词链表的长度被控制在一个合理的范围里。这里讲的理论知识并非无的放矢,大家能在后文里清晰地了解到重写hashCode方法的重要性。


为什么要重写equals和hashCode算法

当我们用 HashMap存入自定义的类时,如果不重写这个自定义类的equals和hashCode方法,得到的结果会和我们预期的不一样。我们来看 WithoutHashCode.java这个例子。

我们定义了一个 Key类;在其中定义了唯一的一个属性 id。当前我们先注释 equals()方法和 hashCode()方法。

在 main函数里,我们定义了两个 Key对象,它们的 id都是 1,逻辑上我们就认为它们是两个相同的对象;就好比它们是两把相同的钥匙,都能打开同一扇门。

在 main函数里,我们通过泛型创建了一个HashMap<Key, String>的对象。它的键部分(HashMap的key)可以存放 Key类型的对象,值部分可以存储String类型的对象。

在 main函数里,我们通过 put方法把 key1和一个字符串放入到 hashMap里;并且我们想用 key2去从HashMap里得到值;这就好比一把锁有两把一模一样的钥匙,用key1可以打开锁,用key2也可以打开锁。这是符合逻辑的,但从当前结果看, 返回结果不是我们想象中的那个字符串,而是 null。

原因有两个:一是没有重写hashCode方法二是没有重写equals方法

当我们往HashMap里放 key1时,首先会调用 Key这个类的 hashCode()方法计算它的 hash值,随后把 key1放入hash值所指引的内存位置。

关键是我们没有在 Key里定义 hashCode方法。这里调用的仍是 Object类的 hashCode()方法(所有的类都是Object的子类),而 Object类的 hashCode()方法返回的 hash值其实是 key1对象的 内存地址(假设是1000)。

如果我们随后是调用 hashMap.get(key1),那么我们会再次调用 hashCode方法(还是返回 key1的地址 1000),随后根据得到的 hash值,能很快地找到 key1

但我们这里的代码是 hashMap.get(k2),当我们调用 Object类的 hashCode方法(因为 Key里没定义)计算 key2hash值时,其实得到的是 key2的内存地址(假设是 2000)。由于 key1key2是两个不同的对象,所以它们的内存地址一定不会相同,也就是说它们的hash值一定不同,这就是我们无法用 key2hash值去拿 kwy1的原因。

当我们把 hashCode方法的注释去掉后,会发现它是返回 id属性的 hashCode值,这里 key1key2id都是1,所以它们的 hash值是相等的。

我们再来更正一下存 key1和取 key2的动作。存 key1时,是根据它 idhash值,假设这里是 100,把 key1对象放入到对应的位置。而取 key2时,是先计算它的 hash值(由于 key2id也是 1,这个值也是 100),随后到这个位置去找。

但结果会出乎我们意料:明明 100号位置已经有 key1,但输出结果依然是 null。其原因就是没有重写 Key对象的 equals方法。

HashMap是用链地址法来处理冲突,也就是说,在 100号位置上,有可能存在着多个用链表形式存储的对象。它们通过 hashCode方法返回的 hash值都是100。

当我们通过 key2hashCode100号位置查找时,确实会得到 key1。但 key1有可能仅仅是和 key2具有相同的 hash值,但未必和 key2相等( key1key2两把钥匙未必能开同一扇门),这个时候,就需要调用 Key对象的 equals方法来判断两者是否相等了。

由于我们在 Key对象里没有定义 equals方法,系统就不得不调用 Object类的 equals方法。由于 Object的固有方法是根据两个对象的内存地址来判断,所以 key1key2一定不会相等,这就是为什么输出结果依然是 null的原因。

为了解决这个问题,我们需要取消 equals方法的注释。在这个方法里,我们认为只要两个对象都是 Key类型,而且它们的 id相等,它们就相等。


小结

由于在项目里经常会用到HashMap,所以在面试的时候几乎一定会问这个问题:你有没有重写过 hashCode方法?你在使用HashMap时有没有重写 hashCode和 equals方法?你是怎么写的?

最后再强调一下:如果大家要在HashMap的 “键” 部分存放自定义的对象,一定要重写 equals和 hashCode方法来覆盖 Object里的同名方法。

注意:Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。 例如, 如果用定义的 User.equals 比较的是User的 ID, 那么 hashCode 方法就需要散列 ID,而不是User的name或address。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355