Java:Effective java学习笔记之 覆盖equals时总要覆盖hashcode

Posted JMW1407

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java:Effective java学习笔记之 覆盖equals时总要覆盖hashcode相关的知识,希望对你有一定的参考价值。

覆盖equals时总要覆盖hashcode

1. 什么是hashcode方法?

首先我们要了解一下散列表是什么?

  • 散列表就是我们平时所说的哈希表,是根据键值对(Key - value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。

散列表的特点是综合了数据和链表的有点;

  • 数组:寻址容易,插入和删除困难;
  • 链表:寻址困难,插入和删除容易。

hashcode方法返回对象的哈希码值

  • hashcode是用来在散列存储结构中确定对象的存储地址的;
  • hashCode()的作用是获取散列码,这个散列表其实就是一个int整数,代表当前对象在散列表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java中的任何类都包含有hashCode() 函数。
  • 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利⽤到了散列码!(可以快速找到所需要的对象)

我们先以“HashSet 如何检查重复”为例⼦来说明为什么要有 hashCode:

public class test1 
 public static void main(String[] args) 
	 String a = new String("ab"); // a 为⼀个引⽤
      String b = new String("ab"); // b为另⼀个引⽤,对象的内容⼀样
      String aa = "ab"; // 放在常量池中
      String bb = "ab"; // 从常量池中查找

      if (aa == bb) 
        System.out.println("aa == bb");
        System.out.println("aa: " + aa.hashCode());
        System.out.println("bb: " + bb.hashCode());
        System.out.println(System.identityHashCode(aa));
        System.out.println(System.identityHashCode(bb));
      

      System.out.println("----------------");

      if (a == b) 
        System.out.println(a == b);
       else 
        System.out.println("a: " + a.hashCode());
        System.out.println("b: " + b.hashCode());
        System.out.println(System.identityHashCode(a));
        System.out.println(System.identityHashCode(b));
      
      System.out.println("----------------");
      if (a.equals(b)) 
        System.out.println("a.equals(b)");
        System.out.println("a: " + a.hashCode());
        System.out.println("b: " + b.hashCode());
      
 

输出

aa == bb
aa: 3105
bb: 3105
1670675563
1670675563
----------------
a: 3105
b: 3105
723074861
895328852
----------------
a.equals(b)
a: 3105
b: 3105

Object的hashCode()默认是返回内存地址的,但是hashCode()可以重写,所以hashCode()不能代表内存地址的不同

System.identityHashCode(Object)方法可以返回对象的内存地址,不管该对象的类是否重写了hashCode()方法。

在散列表中 hashCode() 的作用是获取对象的散列码,确定该对象在散列表中的位置。

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

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

参考上面的小测试

3. 为什么要覆盖hashcode

3.1、覆盖equals时总要覆盖hashCode

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

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

如果不覆盖hashCode方法,我们在需要用到hashCode的地方可能不会如我们所愿

下面看个例子,有这么一个类,我们只覆盖了equals方法,没有覆盖hashCode方法:

public class Student 
	private String name;
	private int age;
 
	public Student(String name, int age) 
		super();
		this.name = name;
		this.age = age;
	
	@Override
	public boolean equals(Object obj) 
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student other = (Student) obj;
		if (age != other.age)
			return false;
		if (name == null) 
			if (other.name != null)
				return false;
		 else if (!name.equals(other.name))
			return false;
		return true;
	
	// 省略 get,set方法...

public class hashTest 
	@Test
	public void test() 
		Student stu1 = new Student("Jimmy",24);
		Student stu2 = new Student("Jimmy",24);
		
		System.out.println("两位同学是同一个人吗?"+stu1.equals(stu2));
		System.out.println("stu1.hashCode() = "+stu1.hashCode());
		System.out.println("stu1.hashCode() = "+stu2.hashCode());
	

输出

 两位同学是同一个人吗?true
stu1.hashCode() = 379110473
stu1.hashCode() = 99550389

如果重写了 equals() 而未重写 hashcode() 方法,可能就会出现两个没有关系的对象 equals 相同(因为equal都是根据对象的特征进行重写的),但 hashcode 不相同的情况。

因为此时 Student 类的 hashcode 方法就是 Object 默认的 hashcode方 法,由于默认的 hashcode 方法是根据对象的内存地址经哈希算法得来的,所以 stu1 != stu2,故两者的 hashcode 值不一定相等。

根据 hashcode 的规则,两个对象相等其 hash 值一定要相等,矛盾就这样产生了。

《Effective Java》案例

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;

3.2、如何在覆盖equals方法时覆盖hashcode方法?

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

@Override
public int hashCode() 
	return 31;

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

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

如何设置一个好的散列函数?步骤如下:

  • 1、.为对象计算int类型的散列码c:
    • 对于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,如果这个域为null,则返回0。
    • 如果该域是数组,则要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。每个元素计算出来的hashCode,使用2.2中的公式,将hashCode组合起来。

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

3、返回result

4、写完了hashCode方法之后,问问自己"相等的实例是否都具有相等的散列码"。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。

public class PhoneNumber 
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) 
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    
    //覆盖equals方法
    @Override
    public boolean equals(Object obj) 
        if (obj == this)
            return true;
        if (!(obj instanceof PhoneNumber))
            return false;
        //必须满足如下条件,才能说明为同一个对象
        PhoneNumber pn = (PhoneNumber) obj;
        return pn.areaCode == areaCode && pn.prefix == prefix && pn.lineNumber == lineNumber;
    

	    @Override
    public int hashCode() 
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    

    public static void main(String[] args)
        Map<PhoneNumber, String> m = new HashMap<>();
        //创建两个相同的对象
        PhoneNumber p1 = new PhoneNumber(707, 867, 5309);
        PhoneNumber p2 = new PhoneNumber(707, 867, 5309);
        //添加到hashmap中
        m.put(p1, "Jenny");
        //比较对象p1和p2
        System.out.println("p1.equals(p2): " + p1.equals(p2));
        System.out.println("p2.equals(p1): " + p2.equals(p1));
        //从hashmap中去获取对象p1和p2
        System.out.println("get p1 from hashmap: " + m.get(p1));
        System.out.println("get p2 from hashmap: " + m.get(p2));
    


输出

p1.equals(p2): true
p2.equals(p1): true
get p1 from hashmap: Jenny
get p2 from hashmap: Jenny

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

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

参考

1、覆盖equals时总要覆盖hashCode
2、《Effective Java》阅读笔记9 覆盖equals时总要覆盖hashCode
3、你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode ⽅法?

以上是关于Java:Effective java学习笔记之 覆盖equals时总要覆盖hashcode的主要内容,如果未能解决你的问题,请参考以下文章

Java:Effective java学习笔记之 避免使用终结方法

Java:Effective java学习笔记之 消除过期对象引用

Java:Effective java学习笔记之 列表优先于数组

Java:Effective java学习笔记之 用enum代替int常量

Java:Effective java学习笔记之 复合优先于继承

Java:Effective java学习笔记之 接口优于抽象类