前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >踩坑集锦之hashcode计算

踩坑集锦之hashcode计算

作者头像
大忽悠爱学习
发布2023-05-09 10:04:28
6720
发布2023-05-09 10:04:28
举报
文章被收录于专栏:c++与qt学习c++与qt学习

踩坑集锦之hashcode计算

问题

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

  • 这个需求很简单,或许大家很快就可以写出答案:
代码语言:javascript
复制
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实例中都具有相同的哈希码,并且在对象状态发生变化时能够正确地更新哈希码。


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

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

下面是一个具体的例子:

代码语言:javascript
复制
class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public 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)); // true
        p.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()方法:

代码语言:javascript
复制
class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public 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)); // true
        p = 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。然后按位与运算,将两个二进制数对应位上的数字进行逻辑与运算。结果如下:

代码语言:javascript
复制
  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的范围内,这就是该代码段的最终结果。


最终解决方案

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

代码语言:javascript
复制
(targetObject.hashCode() & 0x7FFFFFFF) % 100 + 1

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 踩坑集锦之hashcode计算
  • 对象hashcode怎么计算出来的
    • HotSpot虚拟机是如何计算出对象hashcode的
      • 如何根据对象内存地址计算出对象的hashcode
    • 可变对象加哈希缓存导致的错误问题
      • 如何解决
  • hashcode的取值范围
    • & 0x7FFFFFFF 这个操作有什么作用
      • 实例演示
      • 最终解决方案
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档