面试官:为什么要重写hashcode和equals方法?

 一个几乎必问的面试题 

在面试 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函数计算后, 78会有相同的Hash值,对此Java的HashMap对象采用的是"链地址法"的解决方案。效果如下图所示

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

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



 为毛要重写equals和hashCode方法 

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

在其中的第 2到第 18行,我们定义了一个 Key类;在其中的第 3行定义了唯一的一个属性 id。当前我们先注释掉第 9行的 equals方法和第 16行的 hashCode方法。

  1. 1 import java.util.HashMap;
  2. 2 class Key {
  3. 3 private Integer id;
  4. 4 public Integer getId()
  5. 5 { return id; }
  6. 6 public Key(Integer id)
  7. 7 { this.id = id; }
  8. 8 //故意先注释掉equals和hashCode方法
  9. 9 // public boolean equals(Object o) {
  10. 10 // if (o == null || !(o instanceof Key))
  11. 11 // { return false; }
  12. 12 // else
  13. 13 // { return this.getId().equals(((Key) o).getId());}
  14. 14 // }
  15. 15
  16. 16 // public int hashCode()
  17. 17 // { return id.hashCode(); }
  18. 18 }
  19. 19
  20. 20 public class WithoutHashCode {
  21. 21 public static void main(String[] args) {
  22. 22 Key k1 = new Key(1);
  23. 23 Key k2 = new Key(1);
  24. 24 HashMap<Key,String> hm = new HashMap<Key,String>();
  25. 25 hm.put(k1, "Key with id is 1");
  26. 26 System.out.println(hm.get(k2));
  27. 27 }
  28. 28 }

main函数里的第 2223行,我们定义了两个 Key对象,它们的 id都是 1,就好比它们是两把相同的都能打开同一扇门的钥匙。

在第 24行里,我们通过泛型创建了一个HashMap对象。它的键部分可以存放 Key类型的对象,值部分可以存储String类型的对象。

在第 25行里,我们通过 put方法把 k1和一串字符放入到 hm里;而在第 26行,我们想用 k2去从HashMap里得到值;这就好比我们想用 k1这把钥匙来锁门,用 k2来开门。这是符合逻辑的,但从当前结果看, 26行的返回结果不是我们想象中的那个字符串,而是 null

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

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

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

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

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

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

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

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

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

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

由于我们在 Key对象里没有定义 equals方法,系统就不得不调用 Object类的 equals方法。由于 Object的固有方法是根据两个对象的内存地址来判断,所以 k1k2一定不会相等,这就是为什么依然在 26行通过 hm.get(k2)依然得到 null的原因。

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



 再次强调 

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

最后再强调一下:如果大家要在HashMap的 “键” 部分存放自定义的对象,一定要在这个对象里用自己的 equalshashCode方法来覆盖 Object里的同名方法。

原文地址:https://www.cnblogs.com/eryun/p/12150255.html

时间: 2024-08-24 09:29:00

面试官:为什么要重写hashcode和equals方法?的相关文章

为什么要重写hashcode和equals方法?初级程序员在面试中很少能说清楚。

我在面试 Java初级开发的时候,经常会问:你有没有重写过hashcode方法?不少候选人直接说没写过.我就想,或许真的没写过,于是就再通过一个问题确认:你在用HashMap的时候,键(Key)部分,有没有放过自定义对象?而这个时候,候选人说放过,于是两个问题的回答就自相矛盾了. 最近问下来,这个问题普遍回答不大好,于是在本文里,就干脆从hash表讲起,讲述HashMap的存数据规则,由此大家就自然清楚上述问题的答案了. 1 通过Hash算法来了解HashMap对象的高效性 我们先复习数据结构里

(转)为什么要重写 hashcode 和 equals 方法?

作者丨hsm_computer cnblogs.com/JavaArchitect/p/10474448.html 我在面试Java初级开发的时候,经常会问:你有没有重写过hashcode方法?不少候选人直接说没写过.我就想,或许真的没写过,于是就再通过一个问题确认:你在用HashMap的时候,键(Key)部分,有没有放过自定义对象?而这个时候,候选人说放过,于是两个问题的回答就自相矛盾了. 最近问下来,这个问题普遍回答不大好,于是在本文里,就干脆从hash表讲起,讲述HashMap的存数据规则

重写hashCode与equals方法的作用

为了阐明其作用,我们先来假设有如下一个Person类. class Person { public Person(String name, int age) { this.name = name; this.age = age; } private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; }

HashMap中使用自定义类作为Key时,为何要重写HashCode和Equals方法

之前一直不是很理解为什么要重写HashCode和Equals方法,才只能作为键值存储在HashMap中.通过下文,可以一探究竟. 首先,如果我们直接用以下的Person类作为键,存入HashMap中,会发生发生什么情况呢? public class Person { private String id; public Person(String id) { this.id = id; } } import java.util.HashMap; public class Main { public

如何重写hashCode()和equals()方法

hashCode()和equals()方法可以说是Java完全面向对象的一大特色.它为我们的编程提供便利的同时也带来了很多危险.这篇文章我们就讨论一下如何正解理解和使用这2个方法. 如何重写equals()方法 如果你决定要重写equals()方法,那么你一定要明确这么做所带来的风险,并确保自己能写出一个健壮的equals()方法.一定要注意的一点是,在重写equals()后,一定要重写hashCode()方法.具体原因稍候再进行说明. 我们先看看 JavaSE 7 Specification中

java集合框架(hashSet自定义元素是否相同,重写hashCode和equals方法)

/*HashSet 基本操作 * --set:元素是无序的,存入和取出顺序不一致,元素不可以重复 * (通过哈希值来判断是否是同一个对象) * ----HashSet:底层数据结构是哈希表, * 保证数据唯一性的方法是调用存入元素的hashCode()方法 * 和equals(Object obj)方法 * HashCode值相同,才会调用equals方法 * * */ 1 import java.util.HashSet; 2 import java.util.Iterator; 3 publ

重写HashCode和equals规范

hashcode 和 equals 方法是用来鉴定2个对象是否相等. 一般来讲,equals这个方法是给用户调用的,如果你想判断2个对象是否相等,你可以重写equals方法,然后在代码中调用 ,就可以判断他们是否相等了.简单来讲,equals方法主要是用来判断从表面上看或者从内容上看,2个对象是不是相等. 举个例子,有个学生类,属性只有姓名和性别,那么我们可以认为只要姓名和性别相等,那么就说这2个对象是相等的. 如下: package utils; public class Student {

hashcode和equals方法

分析: 1:Person类 1:姓名和年龄 2:重写hashCode和equals方法 1:如果不重写,调用Object类的equals方法,判断内存地址,为false 1:如果是Person类对象,并且姓名和年龄相同就返回true 2:如果不重写,调用父类hashCode方法 1:如果equals方法相同,那么hashCode也要相同,需要重写hashCode方法 3:重写toString方法 1:不重写,直接调用Object类的toString方法,打印该对象的内存地址 Person类 cl

Java 中正确使用 hashCode 和 equals 方法

在这篇文章中,我将告诉大家我对hashCode和equals方法的理解.我将讨论他们的默认实现,以及如何正确的重写他们.我也将使用Apache Commons提供的工具包做一个实现. 目录: hashCode()和equals()的用法 重写默认实现 使用Apache Commons Lang包重写hashCode()和equals() 需要注意记住的事情 当使用ORM的时候特别要注意的 hashCode()和equals()定义在Object类中,这个类是所有java类的基类,所以所有的jav