踩坑集锦之hashcode计算

news/2024/10/17 22:13:46/

踩坑集锦之hashcode计算

  • 问题
  • 对象hashcode怎么计算出来的
    • HotSpot虚拟机是如何计算出对象hashcode的
      • 如何根据对象内存地址计算出对象的hashcode
    • 可变对象加哈希缓存导致的错误问题
      • 如何解决
  • hashcode的取值范围
    • & 0x7FFFFFFF 这个操作有什么作用
    • 实例演示
  • 最终解决方案


问题

需求: 针对java对象hashcode取余,计算出一个1-100之间的数字。

  • 这个需求很简单,或许大家很快就可以写出答案:
targetObject.hashCode() % 100 + 1

但是这个答案存在问题,因为没有考虑到hashcode出现负数的情况,为什么hashcode会出现负数呢?

我们需要从hashcode的计算方式进行推导。


对象hashcode怎么计算出来的

在Java中,每个对象都有一个默认的hashCode()方法,它返回一个int类型的哈希码(hashcode),表示对象的散列值。

hashCode()方法的实现方式可以由具体的类自行决定,但通常情况下,它是根据对象的内部状态计算出来的。一个好的hashCode()实现应该具备以下特性:

  1. 对于同一个对象,多次调用hashCode()方法应该返回相同的值。

  2. 对于不同的对象,它们的hashCode()值应该尽可能地不同,以便于散列表等数据结构的高效操作。

  3. 如果两个对象的equals()方法返回true,那么它们的hashCode()方法返回的值应该相同。

通常情况下,hashCode()方法的实现都会使用对象的内部状态来计算出一个整数值。例如,如果一个对象包含多个字段,那么它的hashCode()方法可能会将这些字段的值组合起来计算出一个散列值。在计算散列值时,通常会使用位运算、乘法和异或等操作来混淆散列值,以增加哈希码的随机性和均匀性。

需要注意的是,hashCode()方法的实现方式可以因不同的JVM、不同的操作系统或不同的Java版本而有所不同。因此,在需要对哈希码进行散列操作的场景中,建议使用专业的哈希算法,如MD5或SHA等算法,以确保哈希码的唯一性和安全性。


HotSpot虚拟机是如何计算出对象hashcode的

在HotSpot虚拟机中,hashCode()方法的计算规则如下:

  1. 如果该对象的哈希码已经被计算出来,则直接返回该哈希码。

  2. 如果该对象的哈希码尚未被计算出来,则根据对象的内存地址计算出一个哈希码,并将其缓存起来。

  3. 如果该对象被标记为“轻量级锁”(Lightweight Locking),则哈希码的计算方式稍有不同。此时,哈希码由线程ID、对象头信息和对象的内存地址组成。

需要注意的是,由于哈希码是根据对象的内存地址计算出来的,因此在不同的JVM实例中,相同的对象可能具有不同的哈希码。此外,由于哈希码是缓存起来的,因此在对象的状态发生变化时,哈希码也不会自动更新,这可能会导致哈希表等数据结构无法正常工作。

为了避免这种问题,建议在实现hashCode()方法时,不要依赖于对象的内存地址或缓存的哈希码,而应该根据对象的内部状态计算出一个稳定的、唯一的哈希码,以确保对象在不同的JVM实例中都具有相同的哈希码,并且在对象状态发生变化时能够正确地更新哈希码。


如何根据对象内存地址计算出对象的hashcode

在HotSpot中,如果对象的哈希码尚未被计算出来,则根据对象的内存地址计算出一个哈希码。具体计算方式如下:

  1. 首先,将对象的内存地址右移4位(也就是除以16),以去掉低4位的偏移量。
  2. 然后,将去掉偏移量后的地址与一个固定的随机数进行异或运算,以增加哈希码的随机性和均匀性。
  3. 最后,将异或运算的结果作为对象的哈希码返回。

由于哈希码是根据对象的内存地址计算出来的,因此在不同的JVM实例中,相同的对象可能具有不同的哈希码。这可能会影响到一些基于哈希表的数据结构,如HashMap和HashSet等,因为这些数据结构的性能和正确性通常依赖于对象的哈希码。

为了避免这种问题,建议在实现hashCode()方法时,不要依赖于对象的内存地址或缓存的哈希码,而应该根据对象的内部状态计算出一个稳定的、唯一的哈希码,以确保对象在不同的JVM实例中都具有相同的哈希码,并且在对象状态发生变化时能够正确地更新哈希码。


可变对象加哈希缓存导致的错误问题

一个典型的例子是将可变对象放入哈希表中。如果哈希表的实现是基于对象的哈希码的,那么当可变对象的状态发生变化时,它的哈希码也会发生变化,但哈希表中存储的哈希码并不会自动更新。这样就会导致哈希表中的对象数量不稳定,甚至可能出现哈希冲突等问题。

下面是一个具体的例子:

class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic int hashCode() {return Objects.hash(name, age);}
}public class Test {public static void main(String[] args) {Set<Person> set = new HashSet<>();Person p = new Person("Tom", 20);set.add(p);System.out.println(set.contains(p)); // truep.age = 30;System.out.println(set.contains(p)); // false}
}

在这个例子中,我们创建了一个Person类,它有两个属性nameage,并且实现了hashCode()方法,该方法基于nameage属性的值计算出哈希码。然后,我们将一个Person对象加入到HashSet中,并检查该对象是否存在于HashSet中。这时,HashSet会根据对象的哈希码和相等性检查来查找该对象。

接着,我们修改该对象的age属性,然后再次检查该对象是否存在于HashSet中。由于age属性的变化导致哈希码的变化,所以HashSet无法正确地查找该对象,最终返回了false。这个问题的根本原因是,Person对象的哈希码是基于对象的属性计算出来的,而属性值的变化会导致哈希码的变化,从而破坏了哈希表的正确性。


如何解决

为了解决上面提到的哈希冲突等问题,可以采用以下两种方法:

  1. 不要将可变对象放入哈希表中。如果需要将可变对象作为哈希表的键值,可以考虑将对象中不可变的部分作为哈希码的计算因子,或者使用其他的数据结构来代替哈希表。

  2. 重写hashCode()equals()方法。在重写hashCode()方法时,要保证对象的哈希码是不变的;在重写equals()方法时,要保证相等的对象具有相等的哈希码。这样,当可变对象的状态发生变化时,其哈希码也会自动更新,从而保证了哈希表中对象的正确性。

下面是上面提到的Person类的修订版,我们将其改为不可变的类,并重写了hashCode()equals()方法:

class Person {private final String name;private final int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic int hashCode() {return Objects.hash(name, age);}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;Person person = (Person) obj;return age == person.age && Objects.equals(name, person.name);}
}public class Test {public static void main(String[] args) {Set<Person> set = new HashSet<>();Person p = new Person("Tom", 20);set.add(p);System.out.println(set.contains(p)); // truep = new Person("Tom", 30);System.out.println(set.contains(p)); // false}
}

在这个修订版中,Person类被改为了不可变的类,即nameage属性被声明为final,从而保证了对象的不变性。同时,重写了hashCode()equals()方法,其中hashCode()方法的计算只依赖于不可变的属性,而equals()方法也只比较不可变的属性。这样,当Person对象的状态发生变化时,其哈希码也不会变化,从而保证了哈希表中对象的正确性。


hashcode的取值范围

在Java中,Object类的hashCode()方法返回的值是一个32位的整数,它可以是任何整数,包括负数和零。

通常情况下,hashCode()方法返回的值都是根据对象的内部状态计算出来的,因此对于不同的对象,它们的hashCode()值也应该不同。但是,由于hashCode()方法的实现方式可能会因不同的JVM、不同的操作系统或不同的Java版本而有所不同,因此在某些情况下,hashCode()方法可能返回负数。

如果hashCode()方法返回负数,那么在使用该值进行位运算或其他计算时,就需要特别注意。在进行位运算时,需要使用& 0x7FFFFFFF将负数转换为正数,以确保计算结果的正确性。


& 0x7FFFFFFF 这个操作有什么作用

& 0x7FFFFFFF 是一个按位与操作符,它的作用是将targetObject.hashCode()的符号位置零,以确保结果为正数。

在Java中,hashCode()方法返回的是一个32位的整数值,它的最高位表示符号位,如果该位为1,则表示该值为负数,否则表示该值为非负数。因此,如果hashCode()返回的值为负数,那么进行位与操作的结果就是将最高位的1变为0,即将符号位变为0,从而得到一个非负数的结果。

在这段代码中,使用0x7FFFFFFFhashCode()进行位与操作,相当于将二进制表示中最高位的1变为0,因为0x7FFFFFFF的二进制表示为0111 1111 1111 1111 1111 1111 1111 1111。这个操作可以确保位与运算的结果为正数,从而保证了结果的正确性。


实例演示

假设targetObject.hashCode()返回的值是-123456789,也就是它的二进制表示为1111 1111 1001 1101 1101 0001 1101 0011(其中最高位为符号位,值为1表示负数)。

进行按位与操作& 0x7FFFFFFF时,先将0x7FFFFFFF转换为二进制表示,即0111 1111 1111 1111 1111 1111 1111 1111。然后按位与运算,将两个二进制数对应位上的数字进行逻辑与运算。结果如下:

  1111 1111 1001 1101 1101 0001 1101 0011 (targetObject.hashCode())
& 0111 1111 1111 1111 1111 1111 1111 1111 (0x7FFFFFFF)
结果:  0111 1111 1001 1101 1101 0001 1101 0011

可以看到,按位与运算的结果是一个非负数,其二进制表示为0111 1111 1001 1101 1101 0001 1101 0011,即2147483659,它是原来负数的补码表示形式。

然后,对这个结果进行模运算% 100,得到59,即将结果映射到0到99的范围内。

最后,将结果加1,得到60,即将结果映射到1到100的范围内,这就是该代码段的最终结果。


最终解决方案

经过上面的分析,最终可以得出下面这行代码作为答案:

(targetObject.hashCode() & 0x7FFFFFFF) % 100 + 1

先对对象的hashCode进行位与运算,将结果的符号位置零,以确保结果为正数,然后对结果取模得到介于0和99之间的数值,最后加上1以将结果转换为介于1和100之间的整数。


http://www.ppmy.cn/news/59364.html

相关文章

低代码平台组件间事件交互

事件的分类 我们主要依托于事件来进行组件间的交互。为了满足组件与组件、组件与系统、组件与服务端的交互&#xff0c;我们大致可以将事件分为三个类别&#xff1a; 组件方法&#xff1a;每个组件都会暴露出一些方法供其他组件进行调用。例如表格组件&#xff0c;我们可以暴…

第三十章 金马弹灵

巴哥奔此时才觉察到&#xff0c;在这一横一竖两个椭圆交叉而成的怪厅内&#xff0c;五六位身着白裙的小孩正围着一个蓝色滴形水池绕圈圈。水池上方倒挂着两根相互缠绕着的石笋&#xff0c;刚才那滴水正是从两笋之间的弧形嘴中落下的。 水池的高度与他们的头顶平齐&#xff0c;水…

【PowerDesigner】一款超好用的E-R图工具,快速构建出高质量的数据库结构,提高开发效率和代码质量

博主简介&#xff1a;努力学习的大一在校计算机专业学生&#xff0c;热爱学习和创作。目前在学习和分享&#xff1a;数据结构、Go&#xff0c;Java等相关知识。博主主页&#xff1a; 是瑶瑶子啦所属专栏: Mysql从入门到精通 近期目标&#xff1a;写好专栏的每一篇文章 文章目录…

YOLOv5 训练自己的数据集

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 &#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 ● 难度&#xff1a;夯实基础⭐⭐ ● 语言&#xff1a;Python3、Pytorch3 ● 时间&#xff1a;5月1日-5月6日 &#x1f37a;要求&#xff1…

C语言atoi函数详解

一、atoi&#xff08;&#xff09;基本概念 atoi是C/C语言中一个常用的字符串转整数的函数&#xff0c;其原型定义在stdlib.h头文件中。它的作用是将一个字符串表示的数字转换为对应的整数。 函数原型&#xff1a; int atoi(const char* str); 参数&#xff1a; str&#x…

Vue电商项目--vuex模块开发

vuex状态管理库 vuex是什么&#xff1f; vuex是官方提供的一个插件&#xff0c;状态管理库&#xff0c;集中式管理项目中组件共有的数据。 切记&#xff0c;并不是全部的项目都需要Vuex,如果项目很小&#xff0c;完全不需要vuex,如果项目很大&#xff0c;组件很多&#xff0…

【python】NameError: No such file or directory 问题解决

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 1. 问题 最近有小伙伴经常问到这个报错&#xff0c;今天来分享一下具体怎么解决。 [Errno 2] No such file or directory: ./mnist_image_label/mnist_train_jpg_60000.txt这个没有查找到子文件或者子文件夹的问题 2. 解决…

【华为OD机试 2023最新 】简单的自动曝光、平均像素值(C语言题解 100%)

文章目录 题目描述输入描述输出描述备注用例题目解析代码思路C语言题目描述 一个图像有n个像素点,存储在一个长度为n的数组img里,每个像素点的取值范围[0,255]的正整数。 请你给图像每个像素点值加上一个整数k(可以是负数),得到新图newImg,使得新图newImg的所有像素平均…