hashCode()剖析

本文深入探讨了HashSet的底层实现,解释了HashCode在对象比较和去重中的作用。首先,HashSet基于HashMap实现,存储元素时通过hashCode确定位置,判重时先比较hashCode再用equals。接着,分析了HashCode的特性,包括与对象物理地址的区别,以及为何对象比较先比较hashCode。文章还指出,包装类和字符串自动去重是因为它们重写了hashCode和equals方法。最后,强调了设计HashCode时应确保其稳定性和对象属性的关系,避免依赖易变属性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写作缘由

在使用Set集合时,我们知道,Set存的数据都是不重复的,但是我们又发现,为什么我们存基本类型的数据可以实现去重,但是存引用类型的数据时(String除外),我们设置对象的属性值完全一样,发现并不能实现去重。怀着这个问题,研究了一下Set集合去重的原理,发现hashCode()方法在其中扮演了很重要的作用,于是就对它进行了一下研究。

HashSet的底层实现

在这里插入图片描述

  • HashSet的底层存储是HashMap
  • 往集合中存储元素时,会把元素的值作为key,然后调用hash方法,拿到当前元素对象的hashCode值
  • 对存储元素进行判重时,先判断元素的hash值,如果hash值正确,再通过equals()判断添加的元素与哈希值一样的元素是否相同。

以上是HashSet存储元素的大致思路,我们不禁好奇。在我们主观意识上,两个对象的所有属性值相同时,就可以看成是两个相同的对象,通过重写equals()方法判断即可,为什么还要进行一次hash的比较,hash比较之后才进行equals()比较呢?

引出问题

通过查看HashSet的实现,我产生了以下疑问。

  1. HashCode是什么?HashCode有什么用?
  2. 为什么对象的比较要先比较HashCode值,再通过equals()比较?
  3. 同为引用类型,为什么包装类和字符串HashSet会自动去重,而对象就不可以了呢?
1.HashCode是什么

在这里插入图片描述

Hash:Hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。
在这里插入图片描述
Hash表:通过hash算法得到的hash值就在这张hash表中,也就是说,hash表就是所有的hash值组成的。
HashCode:hash算法得到的hash值,每一个hash值在hash表中都有一个对应的位置,称为HashCode。

综上:HashCode就是通过Hash算法生成的Hash值在Hash表中存放的位置。

HashCode和对象的物理地址区别

首先一个对象肯定有物理地址,对象的物理地址跟这个hashcode地址不一样,hashcode代表对象的地址说的是对象在hash表中的位置,物理地址说的对象存放在内存中的地址,那么对象如何得到hashcode呢?
通过对象的内部地址(也就是物理地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashcode。所以,hashcode是什么呢?就是在hash表中对应的位置。这里如果还不是很清楚的话,举个例子,hash表中有 hashcode为1、hashcode为2、(…)3、4、5、6、7、8这样八个位置,有一个对象A,A的物理地址转换为一个整数17(这是假如),就通过直接取余算法,17%8=1,那么A的hashcode就为1,且A就在hash表中1的位置。

物理地址——转换成整数——hash算法处理——hashcode。

HashCode特性

1、如果散列表中存在和散列原始输入K相等的记录,那么K必定在f(K)的存储位置上

2、不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞

3、如果两个Hash值不同(前提是同一Hash算法),那么这两个Hash值对应的原始输入必定不同

4、如果两个对象不相同,他们的HashCode可能相同,如果两个对象相同,他们的HashCode一定相同。

HashCode作用

HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的(后半句说的用hashcode来代表对象就是在hash表中的位置)

为什么hashcode就查找的更快,比如:我们有一个能存放1000个数这样大的内存中,在其中要存放1000个不一样的数字,用最笨的方法,就是存一个数字,就遍历一遍,看有没有相同得数,当存了900个数字,开始存901个数字的时候,就需要跟900个数字进行对比,这样就很麻烦,很是消耗时间,用hashcode来记录对象的位置,来看一下。hash表中有1、2、3、4、5、6、7、8个位置,存第一个数,hashcode为1,该数就放在hash表中1的位置,存到100个数字,hash表中8个位置会有很多数字了,1中可能有20个数字,存101个数字时,他先查hashcode值对应的位置,假设为1,那么就有20个数字和他的hashcode相同,他只需要跟这20个数字相比较(equals),如果每一个相同,那么就放在1这个位置,这样比较的次数就少了很多,实际上hash表中有很多位置,这里只是举例只有8个,所以比较的次数会让你觉得也挺多的,实际上,如果hash表很大,那么比较的次数就很少很少了。 通过对原始方法和使用hashcode方法进行对比,我们就知道了hashcode的作用,并且为什么要使用hashcode了。

2. 为什么对象的比较要先比较HashCode值,再通过equals()比较?

刚我们看了HashCode的特性,两个对象相同时,他们的HashCode一定相同。但是像个不同的对象的HashCode可能不同。

因此,决定对象相同的两个条件:1.两个对象的HashCode相同。2.两个对象的equals()比较相同。

因此,这也是为什么Java建议我们重写equals方法的时候,HashCode也要跟着重写。

至于为什么先比较HashCode,再比较equals(),这也就很容易解释了。虽然两个不同的HashCode可能重复,但是概率还是很小的,并且在Hash表中获取到某个HashCode的位置的速度,远比在一大群对象了,一个个对比属性值来判定相同要快的多。
所以总结出来就是一句话:通过HashCode迅速找到HashCode相同的对象,再进行equals()比较属性值。提升效率。

3. 同为引用类型,为什么包装类和字符串HashSet会自动去重,而对象就不可以了呢?

源码(以Integer为例)

对于包装类来说,Java本身已经重写了它的HashCode()和equals()方法,因此往集合中添加时,就已经判断了。

Integer类型的数据,它的HashCode值就是他们本身。

    public static int hashCode(int value) {
        return value;
    }

    /**
     * Compares this object to the specified object.  The result is
     * {@code true} if and only if the argument is not
     * {@code null} and is an {@code Integer} object that
     * contains the same {@code int} value as this object.
     *
     * @param   obj   the object to compare with.
     * @return  {@code true} if the objects are the same;
     *          {@code false} otherwise.
     */
    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

在这里插入图片描述

HashCode设计原则

通过上面的介绍,我们明白了,判断对象全等的两个要素:1.HashCode相等。2.equals()相等。

在实际的设计中,我们重写hashCode()方法时,我们一定要注意,重写方法内使用的变量一定是不常会发生改变的,最好是不会改变。(id,idcard)等这些,而不要设计成name。

举个栗子:

public class Student {
    private String name;
    private int age;
    @Override
    public int hashCode() {
        return name.hashCode() + age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                name.equals(student.name);
    }
}

测试

       Student student1 = new Student();
        student1.setName("张三");
        student1.setAge(18);
        Map<Student,Integer> a = new HashMap<>();
        a.put(student1,18);
        System.out.println(a.get(student1)); //18
        student1.setAge(19);
        System.out.println(a.get(student1)); //null

原因很简单,我们修改了student1的属性,因此此时student1的hash值发生了改变,和Map中保存的student1的hash值不同。因此获取不到。

正确方法是为student设置id,equals和hashcode的重写方法内,只添加对id操作,因为id是唯一的,且不可更换的。

public class Student {

     private Integer id;
     private String name;
    private int age;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Student student= (Student) o;
        return Objects.equals(id, student.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

hashCode()和System.identityHashCode()的区别

  • 一个对象在其生命期中HashCode(identity hash code) 必定保持不变;
  • 二者都是返回对象的HashCode编码,在不重写hashCode()方法的前提下,二者返回是一样的。
  • 重写hashCode()方法后,对象的hashCode()发生变化,但是真实的地址并没有发生改变。可以通过System.identityHashCode()获取。

总结

  • HashCode是对象的物理地址经Hash算法处理后,在Hash表中存放的位置。
  • 两个不同的对象可能有相同的HashCode,但是两个对象相同,其HashCode一定相同。
  • 重写equal方法时,必须重写HashCode方法。HashCode相等,equals不一定相等。但equals相等,HashCode一定相等。
  • 设计HashCode时,无论对象怎么改变,都必须保证该对象的HashCode值保持不变。HashCode不能依赖对象易变的属性。

参考博文

  1. 浅谈Java中的hashcode方法
  2. hashcode详解
  3. 谈谈HashCode的作用
  4. java重写hashcode_正确重写hashCode的办法
  5. public native int hashCode()解析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

See you !

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值