《Effective Java》阅读笔记9 覆盖equals时总要覆盖hashCode

·  阅读 52

1. 什么是hashcode方法?

hashcode方法返回对象的哈希码值
复制代码
  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有改变,那么对于这同一个对象调用多次,hashcode方法都必须返回同一个整数。
  • hashcode的存在主要用于查找的快捷性,如Hashtable,HashMap等,hashcode是用来在散列存储结构中确定对象的存储地址的。

2. hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)

  • 如果两个对象相同,那么两个对象的hashcode也必须相同。

  • 如果两个对象的hashcode相同,并不一定表示两个对象就相同,也就是不一定适合equals方法,只能够说明两个对象在散列表存储结构中,“存放在同一个篮子里”。

3. 为什么要覆盖hashcode

每个覆盖 equals 方法的类中,也必须覆盖 hashCode 方法。如果不这样做的话,就会违反 Object.hashCode 的通用约定,这个约定的内容如下: 摘自 Object 规范[JavaSE6]:

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一的返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该都知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的整体性能

如果不覆盖hashCode方法,我们在需要用到hashCode的地方可能不会如我们所愿,下面看个例子,有这么一个类,我们只覆盖了equals方法,没有覆盖hashCode方法:

package com.atguigu.nio;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Tesz {
    private final String field01;
    public Tesz(String field01) {
        this.field01 = field01;
    } //覆盖equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o){
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Tesz myObject = (Tesz) o;
        return (Objects.equals(field01, myObject.field01));
    }

    public static void main(String[] args) {
        Map<Object, Object> map = new HashMap<>();
        map.put(new Tesz("123"), "123");
        System.out.println(map.get(new Tesz("123")));
    }
}
复制代码

通过运行的结果我们可以看到key是new MyObject("123")时,value是null,从而我们知道即使覆盖了equals方法后还是不能保证相等,原因在于该类违反了hashCode的约定,由于MyObject没有覆盖hashCode方法,导致两个相等的实例拥有不相等的散列码,put方法把此对象放在一个散列桶中,get方法从另外一个散列桶中查找这个对象,这显然是无法找到的。

在这里插入图片描述

当我们加入hashCode方法后就正确显示结果了。

//至于hashCode方法怎么写,返回的哈希值参考是什么,可以参考:http://blog.csdn.net/zuiwuyuan/article/details/40340355
@Override
public int hashCode() {
    int result = field01.hashCode() * 17;
    return result;
}
复制代码

在这里插入图片描述

源码分析 为什么会出现这种情况呢,下面我们看一看Map的源码就清楚了:

final Node<K,V> getNode(int hash, Object key) {
 
     Node<K,V>[] tab; 
     Node<K,V> first, e; 
     int n; 
     K k;
 
     /**
      * 检查table是否为空,table为HashMap实例中的一个存放数据的数组
      * 检查第一个元素的hash码是否为null
      */
     if ((tab = table) != null && (n = tab.length) > 0 &&
         (first = tab[(n - 1) & hash]) != null) {
 
        /*
         * 效率方面的考虑
         * 总是将第一个对象拿出来比较,如果符合要求直接返回
         * 避免执行循环准备工作的一系列代码
         */
         if (first.hash == hash && // always check first node
             ((k = first.key) == key || (key != null && key.equals(k))))
             return first;
 
         //进入循环之前的一系列准备以及检查工作
         if ((e = first.next) != null) {
             if (first instanceof TreeNode)
                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
             do {
                //循环内部利用&&运算符的惰性,如果hash码不相同就直接跳过了= =
                 if (e.hash == hash &&
                     ((k = e.key) == key || (key != null && key.equals(k))))
                     return e;
             } while ((e = e.next) != null);
         }
     }
     return null;
 }

复制代码

put方法把电话号码对象存放在一个散列桶(hash bucket)中,get方法却在另外一个散列桶里面查找。即使这两个实例正好被放到了同一个散列桶里面,get方法也一定会返回null,因为HashMap做了一项优化,将每个项相关联的散列码存放起来,如果hash码不匹配,则不会去检验对象的等同性,充分利用了逻辑与运算的惰性(&&)。

4. 如何在覆盖equals方法时覆盖hashcode方法?

实际上,问题很简单,只要我们重写hashcode方法,返回一个适当的hash code即可。

@Override
public int hashCode() {
return 42;
}
复制代码

这样的确能解决上面的问题,但实际上,这么做,会导致很差的性能,因为它总是确保每个对象都具有同样的散列码。因此,每个对象都被映射到同一个散列桶中,使得散列表退化成链表。

一个好的散列函数通常倾向于“为不相等的对象产生不同的散列码”。 理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。但实际上,要达到这种理想的情形是非常困难的。

如何设置一个好的散列函数?

a. 为对象计算int类型的散列码:

  • -对于boolean类型,计算(f?1:0)
  • -对于byte,char,short,int类型,则计算(int)f
  • -对于long类型,计算(int)(f^(f>>>32))
  • -对于float类型,计算Float.floatToIntBits(f)
  • -对于Double类型,计算Double.doubleToLongBits(f),然后再按照long类型处理
  • -对于对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashcode
  • -对于数组,则把每一个元素当作单独的域来处理。

b. 将获取到的c合并:result = 31 * reuslt + c;

c. 返回result

比如,我们可以优化上面的hashcode方法:

 @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
复制代码

如果一个类是不可变类,并且计算散列码的开销也比较大,就应该考虑吧散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码

private volatile static int hashcode;

    @Override
    public int hashCode() {
        int result = hashcode;
        if (result == 0){
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
            hashcode = result;
        }
        return result;
    }
复制代码

为什么要选31?

因为它是个奇素数,另外它还有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:

31*i == (i<<5)-i
复制代码

4. 参考文献

www.cnblogs.com/Tony-Anne/p… www.jianshu.com/p/40ee40f15…

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

7.你们要的免费书来了

分类:
后端
标签:
分类:
后端
标签: