java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

JMW1407 JMW1407     2023-02-23     295

关键词:

覆盖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:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

Java覆盖equals时请遵守通用约定覆盖equals时请遵守通用约定1、为什么要覆盖equals2、需要覆盖equals方法的时机2.1、不需要覆盖equals方法的情况2.2、需要覆盖equals方法的情况2.2.1、自反性2.2.2、对称性2.2.3、传递性2.2.4、一致性2.2.5、... 查看详情

java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

Java覆盖equals时总要覆盖hashcode覆盖equals时总要覆盖hashcode1.什么是hashcode方法?2.hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)3.为什么要覆盖hashcode3.1、覆盖equals时总要覆盖hashCode3.2、如何在覆盖... 查看详情

java:effectivejava学习笔记之避免使用终结方法(代码片段)

Java避免使用终结方法避免使用终结方法1、finalize()基本概念2、finalize()的执行过程3、为什么要避免覆盖并使用finalize方法?4、如果类中的资源确实需要被释放,我们应该怎么做?5、终结方法的利弊5.1、终结方法的好... 查看详情

java:effectivejava学习笔记之复合优先于继承(代码片段)

Java复合优先于继承复合优先于继承1、实现继承和接口继承2、在实际开发中继承的缺点2.1、子类依赖于其超类中特定功能的实现细节3、什么是复合?3.1、书上案例4、复合相比较于继承的优点和缺点5、何时使用继承,何时使用... 查看详情

java:effectivejava学习笔记之优先考虑泛型和泛型方法(代码片段)

Java优先考虑泛型和泛型方法1、优先考虑泛型2、优先考虑泛型方法参考1、优先考虑泛型下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。publicclassStackprivateObject[]elem... 查看详情

effectivejava学习笔记之创建和销毁对象

一、考虑用静态工厂方法代替构造器1、此处的静态工厂方法是指返回指为类的对象的静态方法,而不是设计模式中的静态工厂方法。2、静态工厂方法的优势有:a、使用不同的方法名称可显著地表明两个静态工厂方法的不同,而... 查看详情

effectivejava学习笔记之不可实例化的类

在没有显式声明一个类的构造方法时,编译器会生成默认的无参构造方法,在设计工具类时,我们通常将方法设置成静态方法,以类名.方法名的形式调用,此时这个类就没有必要创建实例,我们知道抽象类不可以被实例化,但... 查看详情

java:java学习笔记之java单例模式的简单理解和使用(代码片段)

...单例的实现如下:2、懒汉式终极版本:volatile3、EffectiveJava1——静态内部类4、5.2EffectiveJava2——枚举参考Java单例模式1、饿汉式单例的实现如下://饿汉式实现publicclassSingleBprivatestaticfinalSingleBINSTA 查看详情

java:effectivejava学习笔记之考虑实现comparable接口(代码片段)

Java考虑实现Comparable接口考虑实现Comparable接口1、Comparable接口2、为什么要考虑实现Comparable接口3、compareTo方法的通用约定4、何时以及如何实现Comparable接口4.1、多重比较5、实现Comparable接口所需满足的需求6、总结参考考虑实现Compa... 查看详情

java:effectivejava学习笔记之接口只用于定义类型类层次优于标签类(代码片段)

Java接口只用于定义类型1、接口只用于定义类型1.1、常量接口2、类层次优于标签类1、接口只用于定义类型当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明可以对这个类的实... 查看详情

java:effectivejava学习笔记之列表优先于数组(代码片段)

Java列表优先于数组列表优先于数组1、协变与不可变类型2、运行时检验与编译器检验3、可具体化与不可具体化4、无法很好混用列表和数组5、案例分析参考列表优先于数组1、协变与不可变类型1、数组是协变类型,指继承关... 查看详情

java:effectivejava学习笔记之请不要在新代码中使用原生态类型(代码片段)

Java请不要在新代码中使用原生态类型1、请不要在新代码中使用原生态类型参考1、请不要在新代码中使用原生态类型1、在没有泛型之前,从集合中读取到每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象&#x... 查看详情

java:effectivejava学习笔记之避免创建不必要的对象(代码片段)

Java避免创建不必要的对象避免创建不必要的对象1、采用更合适的API或工具类减少对象的创建2、重用相同功能的对象3、小心自动装箱(autoboxing)4、用静态工厂方法而不是构造器5、正则表达式6、补参考避免创建不必要的... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

java:effectivejava学习笔记之接口优于抽象类(代码片段)

Java接口优于抽象类接口优于抽象类1、接口和抽象类2、接口优点3、骨架类3.1、demo参考接口优于抽象类1、接口和抽象类Java中抽象类和接口的区别2、接口优点1、现有的类可以很容易的被更新,以实现新的接口。如果你前期编... 查看详情

java:effectivejava学习笔记之静态工厂方法的简单理解和使用(代码片段)

Java静态工厂方法静态工厂方法一、什么是静态工厂方法?二、静态工厂方法的优势1、静态工厂方法与构造器不同的第一优势在于,它们有名字2、静态工厂方法不用在每次调用的时候都创建一个新的对象3、静态工厂方法... 查看详情

java:effectivejava学习笔记之通过私有构造器强化不可实例化的能力(代码片段)

Java通过私有构造器强化不可实例化的能力通过私有构造器强化不可实例化的能力参考通过私有构造器强化不可实例化的能力并非所有的类都是需要实例化的。有时候,我们可能需要编写至包含静态方法和静态域的类。这些类... 查看详情

java:effectivejava学习笔记之遇到多个构造器参数时要考虑用构建器(代码片段)

Java遇到多个构造器参数时要考虑用构建器构建器1、构建一个对象的几种方式2、定义和使用场景3、优势和不足3、使用参考构建器1、构建一个对象的几种方式1、多构造函数:这种方式虽然最方便,但也最繁琐,假设... 查看详情