从一道面试题彻底搞懂hashCode与equals的作用与区别及应当注意的细节

参考文章:

从一道面试题彻底搞懂hashCode与equals的作用与区别及应当注意的细节


       最近去面试了几家公司,被问到hashCode的作用,虽然回答出来了,但是自己还是对hashCode和equals的作用一知半解的,所以决定把它们研究一下。

以前写程序一直没有注意hashCode的作用,一般都是覆盖了equals,缺没有覆盖hashCode,现在发现这是埋下了很多潜在的Bug!今天就来说一说hashCode和equals的作用。

       先来试想一个场景,如果你想查找一个集合中是否包含某个对象,那么程序应该怎么写呢?通常的做法是逐一取出每个元素与要查找的对象一一比较,当发现两者进行equals比较结果相等时,则停止查找并返回true,否则,返回false。但是这个做法的一个缺点是当集合中的元素很多时,譬如有一万个元素,那么逐一的比较效率势必下降很快。于是有人发明了一种哈希算法来提高从该集合中查找元素的效率,这种方式将集合分成若干个存储区域(可以看成一个个桶),每个对象可以计算出一个哈希码,可以根据哈希码分组,每组分别对应某个存储区域,这样一个对象根据它的哈希码就可以分到不同的存储区域(不同的桶中)。如下图所示:

                                            

       实际的使用中,一个对象一般有key和value,可以根据key来计算它的hashCode。假设现在全部的对象都已经根据自己的hashCode值存储在不同的存储区域中了,那么现在查找某个对象(根据对象的key来查找),不需要遍历整个集合了,现在只需要计算要查找对象的key的hashCode,然后找到该hashCode对应的存储区域,在该存储区域中来查找就可以了,这样效率也就提升了很多。说了这么多相信你对hashCode的作用有了一定的了解,下面就来看看hashCode和equals的区别和联系。

       在研究这个问题之前,首先说明一下JDK对equals(Object obj)和hashCode()这两个方法的定义和规范:Java中任何一个对象都具备equals(Object obj)和hashCode()这两个方法,因为他们是在Object类中定义的。 equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。 hashCode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 

下面是官方文档给出的一些说明:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">hashCode 的常规协定是:     
  2. 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。     
  3. 如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。     
  4. 以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。     
  5. 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)     
  6.     
  7. 当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。</span>  

下面是我查阅了相关资料之后对以上的说明做的归纳总结:

1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。

2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。

3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。

4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。

5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。

6.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。


       想要弄清楚以上六点,先要知道什么时候需要重写equals和hashCode。一般来说涉及到对象之间的比较大小就需要重写equals方法,但是为什么第一点说重写了equals就需要重写hashCode呢?实际上这只是一条规范,如果不这样做程序也可以执行,只不过会隐藏bug。一般一个类的对象如果会存储在HashTable,HashSet,HashMap等散列存储结构中,那么重写equals后最好也重写hashCode,否则会导致存储数据的不唯一性(存储了两个equals相等的数据)。而如果确定不会存储在这些散列结构中,则可以不重写hashCode。但是个人觉得还是重写比较好一点,谁能保证后期不会存储在这些结构中呢,况且重写了hashCode也不会降低性能,因为在线性结构(如ArrayList)中是不会调用hashCode,所以重写了也不要紧,也为后期的修改打了补丁。

下面来看一张对象放入散列集合的流程图:



       从上面的图中可以清晰地看到在存储一个对象时,先进行hashCode值的比较,然后进行equals的比较。可能现在你已经对上面的6点归纳有了一些认识。我们还可以通过JDK中得源码来认识一下具体hashCode和equals在代码中是如何调用的。

HashSet.java 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">  public boolean add(E e) {  
  2.     return map.put(e, PRESENT)==null;  
  3.     }</span>  

HashMap.java

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">    public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }  
  15.   
  16.         modCount++;  
  17.         addEntry(hash, key, value, i);  
  18.         return null;  
  19.     }</span>  


最后再来看几个测试的例子吧:

测试一:覆盖equals(Object obj)但不覆盖hashCode(),导致数据不唯一性

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public class HashCodeTest {  
  2.     public static void main(String[] args) {  
  3.         Collection set = new HashSet();  
  4.         Point p1 = new Point(11);  
  5.         Point p2 = new Point(11);  
  6.   
  7.         System.out.println(p1.equals(p2));  
  8.         set.add(p1);   //(1)  
  9.         set.add(p2);   //(2)  
  10.         set.add(p1);   //(3)  
  11.   
  12.         Iterator iterator = set.iterator();  
  13.         while (iterator.hasNext()) {  
  14.             Object object = iterator.next();  
  15.             System.out.println(object);  
  16.         }  
  17.     }  
  18. }  
  19.   
  20. class Point {  
  21.     private int x;  
  22.     private int y;  
  23.   
  24.     public Point(int x, int y) {  
  25.         super();  
  26.         this.x = x;  
  27.         this.y = y;  
  28.     }  
  29.   
  30.     @Override  
  31.     public boolean equals(Object obj) {  
  32.         if (this == obj)  
  33.             return true;  
  34.         if (obj == null)  
  35.             return false;  
  36.         if (getClass() != obj.getClass())  
  37.             return false;  
  38.         Point other = (Point) obj;  
  39.         if (x != other.x)  
  40.             return false;  
  41.         if (y != other.y)  
  42.             return false;  
  43.         return true;  
  44.     }  
  45.   
  46.     @Override  
  47.     public String toString() {  
  48.         return "x:" + x + ",y:" + y;  
  49.     }  
  50.   
  51. }  
  52. </span>  


输出结果:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">true  
  2. x:1,y:1  
  3. x:1,y:1  
  4. </span>  

原因分析:

(1)当执行set.add(p1)时(1),集合为空,直接存入集合;

(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同hashCode值的对象,因此jdk默认不同hashCode值,equals一定返回false,所以直接存入集合。

 (3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,继续判断equals是否返回true,因为是同一对象所以返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。


测试二:覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性

修改Point类:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">class Point {  
  2.     private int x;  
  3.     private int y;  
  4.   
  5.     public Point(int x, int y) {  
  6.         super();  
  7.         this.x = x;  
  8.         this.y = y;  
  9.     }  
  10.   
  11.     @Override  
  12.     public int hashCode() {  
  13.         final int prime = 31;  
  14.         int result = 1;  
  15.         result = prime * result + x;  
  16.         result = prime * result + y;  
  17.         return result;  
  18.     }  
  19.   
  20.     @Override  
  21.     public String toString() {  
  22.         return "x:" + x + ",y:" + y;  
  23.     }  
  24.   
  25. }  
  26. </span>  

输出结果:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">false  
  2. x:1,y:1  
  3. x:1,y:1</span>  

原因分析:

(1)当执行set.add(p1)时(1),集合为空,直接存入集合;

(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals是否相等,因为这里没有覆盖equals,默认使用'=='来判断,所以这里equals返回false,jdk认为是不同的对象,所以将p2存入集合。

 (3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,并且equals返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。


综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。
(注意:在HashSet中插入同一个元素(hashCode和equals均相等)时,会被舍弃,而在HashMap中插入同一个Key(Value 不同)时,原来的元素会被覆盖。)

测试三:在内存泄露问题
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public class HashCodeTest {  
  2.     public static void main(String[] args) {  
  3.         Collection set = new HashSet();  
  4.         Point p1 = new Point(11);  
  5.         Point p2 = new Point(12);  
  6.   
  7.         set.add(p1);  
  8.         set.add(p2);  
  9.           
  10.         p2.setX(10);  
  11.         p2.setY(10);  
  12.           
  13.         set.remove(p2);  
  14.   
  15.         Iterator iterator = set.iterator();  
  16.         while (iterator.hasNext()) {  
  17.             Object object = iterator.next();  
  18.             System.out.println(object);  
  19.         }  
  20.     }  
  21. }  
  22.   
  23. class Point {  
  24.     private int x;  
  25.     private int y;  
  26.   
  27.     public Point(int x, int y) {  
  28.         super();  
  29.         this.x = x;  
  30.         this.y = y;  
  31.     }  
  32.   
  33.   
  34.     public int getX() {  
  35.         return x;  
  36.     }  
  37.   
  38.   
  39.     public void setX(int x) {  
  40.         this.x = x;  
  41.     }  
  42.   
  43.   
  44.     public int getY() {  
  45.         return y;  
  46.     }  
  47.   
  48.   
  49.     public void setY(int y) {  
  50.         this.y = y;  
  51.     }  
  52.   
  53.   
  54.     @Override  
  55.     public int hashCode() {  
  56.         final int prime = 31;  
  57.         int result = 1;  
  58.         result = prime * result + x;  
  59.         result = prime * result + y;  
  60.         return result;  
  61.     }  
  62.   
  63.   
  64.     @Override  
  65.     public boolean equals(Object obj) {  
  66.         if (this == obj)  
  67.             return true;  
  68.         if (obj == null)  
  69.             return false;  
  70.         if (getClass() != obj.getClass())  
  71.             return false;  
  72.         Point other = (Point) obj;  
  73.         if (x != other.x)  
  74.             return false;  
  75.         if (y != other.y)  
  76.             return false;  
  77.         return true;  
  78.     }  
  79.   
  80.   
  81.     @Override  
  82.     public String toString() {  
  83.         return "x:" + x + ",y:" + y;  
  84.     }  
  85.   
  86. }  
  87. </span>  

运行结果:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">x:1,y:1  
  2. x:10,y:10</span>  

原因分析:
       假设p1的hashCode为1,p2的hashCode为2,在存储时p1被分配在1号桶中,p2被分配在2号筒中。这时修改了p2中与计算hashCode有关的信息(x和y),当调用remove(Object obj)时,首先会查找该hashCode值得对象是否在集合中。假设修改后的hashCode值为10(仍存在2号桶中),这时查找结果空,jdk认为该对象不在集合中,所以不会进行删除操作。然而用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。解决该问题的办法是不要在执行期间修改与hashCode值有关的对象信息,如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。

总结:
       1.hashCode是为了提高在散列结构存储中查找的效率,在线性表中没有作用。
       2.equals和hashCode需要同时覆盖。
       3.若两个对象equals返回true,则hashCode有必要也返回相同的int数。

       4.若两个对象equals返回false,则hashCode不一定返回不同的int数,但为不相等的对象生成不同hashCode值可以提高哈希表的性能。

       5.若两个对象hashCode返回相同int数,则equals不一定返回true。

       6.若两个对象hashCode返回不同int数,则equals一定返回false。

       7.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。


  • 32
    点赞
  • 122
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值