Java 中hashCode() 、equals() 和==深入解析

Java 笔记 专栏收录该内容
8 篇文章 0 订阅

一、==和equals()

1.面试题:==和equals()有什么区别?


最大不同

equals是方法,==是运算符

= =

  • 基本类型:比较值是否相同
  • 引用类型:比较引用是否相同

equals

  • 本质上就是==,但不能用于基数据本类型变量的比较,如果没有对equals方法重写,则比较的是引用类型变量所指向的对象的地址
  • 大多数类如String、Integer等都重写了equals方法,先根据==判断,如果不等,再根据值判断

2.equals方法存在的意义


我们可能会想,Java中已经提供有了==来判断数据是否相同,为什么还要多出来个equals方法呢?

因为Java是面向对象的编程语言,所以对象是Java的核心,如果只存在==,那么它只能比较两个对象指向的地址是否相同,而一个Java对象中包含的内容可以是众多的,有时候我们并不希望仅靠地址来比较,而是通过对象中的某些数据(属性)来比较,所以equals()方法出现了,它专门比较的是两个对象是否相同,而这个相同可以牵扯到这个对象中的很多数据,所以不能设计一个统一的规范,于是Java把判断对象是否相等的权力给了我们编程人员,它通过在所有类的基类Object中设计一个简单的equals方法,而后我们创建的所有类都可以重写这个方法,由我们编程人员自己来设定equals策略

Object类中的equals方法

public boolean equals(Object obj) {
        return (this == obj);
    }

可以看到,Object中的默认equals方法还是通过==来比较的,如果我们没重写equals方法,他比较的就是两个对象指向的地址是否相同,然而像String和基本数据类型的包装类基本都重写了equals方法,从而比较的是对象中的值是否相同

Integer中的equals方法

public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

可以看到Integer中的equals方法是先将两个比较的对象拆箱成基本数据类型,然后比较两个基本数据类型的值是否相同

关于包装类的自动拆箱和装箱请参考:https://blog.csdn.net/qq_40911298/article/details/104861093

二、equals()和hashcode()

1.面试题:两个对象的hashCode()结果相同时,它们进行equals()比较返回一定为true吗?


是不对的,两个对象通过hashCode()返回相同的内容并不能证明两个对象是相同的对象,因为两个不同的对象也可能产生相同的hashcode(下面的hashCode()规范第三条),这里说的相同对象,是我们进行自行定义的equals()方法比较返回true

hashCode()是对象内存地址的映射(后面会讲到),所以当我们没有重写hashCode()方法时,不同内存地址的对象的hashcode肯定不同;但是正因为我们可以重写hashCode()方法,每个人不可能完全的遵守hashCode()方法规范,或者设计不出完美的hashCode()方法时,不同的对象(equas为false)就可能产生相同的hashcode了

equals和hashcode()的关系

  • 如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
  • 如果两个对象不equals,他们的hashcode有可能相等。
  • 如果两个对象hashcode相等,他们不一定equals。
  • 如果两个对象hashcode不相等,他们一定不equals。

2.Java中关于hashCode()的规范


  • 每当在Java应用程序的执行过程中多次对同一对象调用hashCode()方法时,hashCode()方法必须一致地返回相同的整数,前提是没有修改在对象的equals比较中使用的信息。从应用程序的一次执行到同一应用程序的另一次执行,此整数不必保持一致。
  • 如果根据equals(Object)方法,两个对象相等,那么对两个对象中的每个对象调用hashCode方法必须产生相同的整数结果。
  • 根据equals(java.lang.Object)方法,如果两个对象不相等,则对这两个对象中的每一个调用hashCode方法不要求必须产生不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

虽然可以不按照上述要求做,但是会引起一系列问题,比如对于使用hash存储的系统(HashMap、HashSet等),如果hashcode频繁冲突(即不同的对象产生相同的hashcode)会造成存取性能急剧下降

Object类中的hashCode()方法

public native int hashCode();

它是一个native方法,是对对象内存地址的一个映射值,内存中不同的对象,该方法返回的值肯定不同。它跟equals方法一样,可以重写,以满足hashCode()方法规范

关于native:说明该方法不是用java语言实现的,一般是调用操作系统底层的c/c++函数完成(JNI技术)

重写规则

为了满足以上规范,我们应该在重写equals(Object obj)方法后,当equals方法的返回值和==运算返回的结果不相同时(但既然我们重写了equals方法,那么就表示我们希望它和==运算的结果不同),我们就有必要重写hashCode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值

比如:String类不仅重写了equals方法,也重写了hashCode()方法,使得两个String类型的对象只要值相同(equals为true),那么它们的hashcode也一定相同

public class Test {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = new String("hello");
        System.out.println(str1 == str2);
        System.out.println(str1.equals(str2));
        System.out.println(str1.hashCode() == str2.hashCode());
    }
}
/**output
false
true
true
**/

3.面试题:重写了equals方法为什么要重写hashCode()方法?

回答这个问题你必须要对HashMap结构非常熟悉,这是前提

前面已经提到,重写了equals方法,之所以要重写hashCode是为了满足hashCode()规范,即equals()相同的两个对象,hashCode()也应该相同,equals不同的两个对象,hashCode()尽可能相同

但是面试官肯定不会管你那个规范,我们需要举出实际的例子,其实面试官考的是hashCode()的应用场景,而hashCode和HashMap的关联可以说是最深的,说白了,面试官考的就是HashMap,在HashMap存取自定义对象时重写hashCode()最为重要,我们应该知道重写hashCode()方法就是为了满足HashMap的存取效率和正确性,从下面两个方面回答

  • 如果equals重写后,只要对象的某个属性相同则equals返回true,那么两个equals返回为true的对象,此时我们认为这两个对象相同,而如果没有重写hashCode()方法,那么hashCode()方法返回的就是对象的地址的映射,不同同地址得到的hashcode肯定不同,此时我们新建两个我们认为相同的对象作为键存到HashMap中,根据HashMap的不重复特性,后者应该覆盖前者,但是事实却不一定如此,我们存元素到HashMap中会根据元素键的hashcode计算hash值,再经过散列函数求出元素位于哈希桶数组的位置,此处我们不考虑hash冲突,即不同的hash值一定会得到不同的位置,我们认为相同的对象因为是新建的对象所以有不同的内存空间,所以hashcode不同,导致最终HashMap存储到哈希桶数组上的位置不同,(就算有hash冲突,定位到了相同的位置,但是在遍历链表的时候,首先就会判断hash值是否相同,如果不同就是不同的对象,会直接存储,不会覆盖),因此,相同的元素无法覆盖,HashMap中就有了两个我们认为相同的对象(但HashMap认为是不同的),然而我们本意是想进行覆盖操作;并且我们取数据的时候也会出错,它必定会返回一个null值,因为不同的hashcode会定位到不同的数组位置,或者定位到了相同的位置却又因为key的hashcode的不同在链表中找不到key所对应的value。这是影响到了HashMap存取的正确性,举例如下:
public class Test {
    public static void main(String[] args) {
        Person p1 = new Person("小明");
        HashMap<Person, String> map = new HashMap<>();
        map.put(p1, "还没开学");
        Person p2 = new Person("小明");
        System.out.println(p1.equals(p2));//输出true,证明我们认为p1和p2是同一个对象
        System.out.println(map.get(p2));//输出null,证明没有重写hashCode()时,我们通过两个equals为true的对象无法得到原本的值
    }
}
class Person {
    private String name;
    Person(String name) {
        this.name = name;
    }
    @Override
    public boolean equals(Object obj) {//重写equals方法,只要名字相同,我们就认为是同一个对象
        if(this == obj){
            return true;
        }
        if(obj instanceof Person) {
            return this.name == ((Person) obj).name;
        }
        return false;
    }
}
  • 而我们重写的hashCode()的规范性又会影响到HashMap的存取效率,如果我们重写的hashCode()不规范,导致大量的不同的数据有相同的hashcode值,会使hash冲突的概率大大增加,即众多不同的数据都定位到哈希桶数组的同一个位置,会导致该位置的链表结构非常的长,而存储和取出HashMap中的元素都可能会遍历查找链表中的值,太长的链表会严重影响效率,虽然JDK1.8对长度大于8的链表会转换成红黑二叉树, 提升了查找效率,但不管怎样长度增加还是会影响我们遍历查询的效率

4.hashCode()存在的意义


hashCode()有这么麻烦的约束,那么它存在的意义到底是什么呢?

其实,前面也已经提到,hashcode主要用于使用hash存储的系统,比如HashMap和HashSet(底层就是HashMap)

hashcode的作用就是定位元素在hash系统中的位置

拿HashMap来说,它里面的数据结构是一个哈希桶数组,数组中的元素是链表或红黑二叉树(JDK1.8改进的),当我们往HashMap中put元素的时候(这里假设不是重复元素)简单说一下流程:

  • 先根据key的hashCode()重新计算hash值(利用的HashMap中的hash()方法,相同的hashcode计算出的hash值也一定是相同的)
  • 根据hash值再经过散列函数的计算(计算方式为(n-1)&hash, n为数组长度)得到这个元素在数组中的位置(即下标)
  • 如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放
  • 如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上

再者,我们想想HashMap的特点:无序、键不重复

而这个键不重复特点的实现本质就是利用hashcode()和equals()方法

首先我们应想到为什么不直接使用equals方法来判断是否重复呢?这里主要是效率问题,假如HashMap中有很多元素,那是不是我们要挨个挨个的equals呢?这会严重影响HashMap的存储效率,所以我们先用hashcode经过散列函数得到元素在哈希桶数组中的位置,如果这个数组位置上没有元素(即没有产生过hash冲突),那么可以直接存取,如果有元素,那么这些元素是以链表形式存储的,我们还需要遍历链表,在存储的时候来判断是否有重复元素

这里截取了HashMap判断键是否重复的代码:
其中,p为要判断的节点

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))

我们先判断元素的hash值是否相同,因为hash值是靠键的hashCode()得到的不同hashcode的对象一定不相同,可以作为判断重复的先决条件,但相同hashcode的对象不一定相同,所以判断重复元素也不能只靠hash()方法,对象不同可以靠hash方法判断,但对象相同却不能仅靠hash方法判断,所以对象到底是否相同还是要靠equals()来进行判断

判断键是否重复的简单流程就是:

  • 先根据hashcode通过散列函数定位元素在哈希桶数组中的位置
  • 再看该位置上是否存在元素,无元素直接存储,只有有一个元素直接比较键是否重复,有多个元素需要遍历链表
  • 遍历过程为判断要添加元素的键的hash值和链表中每个元素键的hash值是否相同
  • 不同则存储元素
  • 相同则再判断equals方法是否返回值是否为true
  • 不为true则存储元素
  • 为true则覆盖元素

5.总结


所以,总结下来,hashCode()方法主要有两个作用:

  • hashCode()方法主要是来确定元素在hash存储系统中的位置
  • 和equals()方法结合来判断元素是否相同,达到hash系统中数据不重复的特点

并且为了保持hash存储系统的存取效率,便要避免hash冲突,而尽可能满足hashCode()方法规约,就是一个重要的手段


– – 若欲攀登高峰,莫以彩虹为梯

  • 3
    点赞
  • 2
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值