深入揭秘hashcode:为何它不等于对象地址?

前置知识

在Java中==比较的是对象地址

equals默认也是对象地址,但是如果重写以后就要按照重写的逻辑来进行比较,比如String重写了equals这个方法,他就会比较字符串字面值

提到重写equals,就一定会提到重写hashcode方法,这个问题本文不会展开,默认读者已经了解过这个问题了。

什么是 hashCode

简单来说,hashCode() 是一个本地方法(native method) ,定义在 java.lang.Object 类中,意味着 Java 中的每一个对象都与生俱来拥有获取 hashCode 值的能力。它返回一个整数值,这个值理论上应该尽可能唯一地标识该对象在其生命周期内的某种特征,以便在特定的数据结构中(如 HashSetHashMap 等基于哈希表实现的集合)能够快速定位和操作对象。

从底层实现看,hashCode 的生成算法与对象的内存地址、对象的状态(成员变量的值)或者二者的结合相关,不同的 JVM 实现可能略有差异。但对于开发者而言,重要的是理解如何合理地重写 hashCode 方法,以满足程序在功能和性能上的需求。

本文不会把重心放在如何使用hashcode上,而是hashcode到底是什么,它是怎么来的,来揭秘hashcode

下面让我们开始吧

HashCode的存储与生成机制

在Java中,每个对象的hashCode并非直接使用内存地址。实际上,hash值存储在对象的对象头(Object Header) 中。当首次调用hashCode()方法时,若对象头中尚未存储hash值,JVM会通过get_next_hash方法生成并存储该值。

一个关键细节是:调用本地方法hashCode()会触发偏向锁失效。因为偏向锁需要利用对象头中的特定位置存储线程ID,而生成hashCode会占用这一空间,导致锁升级为轻量级锁。

HashCode的生成策略

如果没有重写hashcode,使用默认的hashcode,那它就是一个本地方法,需要去看c/c++的代码逻辑

具体的实现可以参考 jdk/src/hotspot/share/runtime/synchronizer.cpp(源码可以到 GitHub 上 OpenJDK 的仓库中下载)。get_next_hash() 方法会根据 hashCode 的取值来决定采用哪一种哈希值的生成策略。

如果没有 C++ 基础的话,不用细致去看每一行代码,我们只通过表面去了解一下 get_next_hash() 这个方法就行。其中的 hashCode 变量是 JVM 启动时的一个全局参数,可以通过它来切换哈希值的生成策略。

可通过以下JVM参数验证策略:

bash 复制代码
-XX:hashCode=4 # 强制使用内存地址策略
  • hashCode==0,调用操作系统 OS 的 random() 方法返回随机数
  • hashCode == 1,在 STW(stop-the-world)操作中,这种策略通常用于同步方案中。利用对象地址进行计算,使用不经常更新的随机数GVars.stw_random)参与其中。
  • hashCode == 2,使用返回 1,用于某些情况下的测试。
  • hashCode == 3,从 0 开始递增计数
  • hashCode == 4,与创建对象的内存位置有关,原样输出。
  • hashCode == 5,默认值,支持多线程,使用线程状态参数 (如线程ID、系统时间等)经过确定性算法的位运算 得来的伪随机数。在JDK版本6和7中,它是随机生成的数字,在版本8中,它是基于线程状态的数字。

我们怎么理解呢?JDK1.8中hashcode的本地默认实现就是一个跟线程状态有关的随机数

重写hashcode

我们来看一下IDEA默认的重写策略

java 复制代码
 @Override
 public int hashCode() {
     return Objects.hash(age, name);
 }

Objects类的 hash() 方法可以针对不同数量的参数生成新的 hashCode() 值。

java 复制代码
public static int hashCode(Object a[]) {
 if (a == null)
     return 0;

 int result = 1;

 for (Object element : a)
     result = 31 * result + (element == null ? 0 : element.hashCode());

 return result;
}

代码似乎很简单

注意:31 是个奇质数,不大不小,一般质数都非常适合哈希计算,偶数相当于移位运算,容易溢出,造成数据信息丢失。

如果属性是基础类型,比如int,会先包装成Integer然后再计算hashcode,因为Integer也重写了hashcode,它会直接使用int的字面值,所以在这里只要属性的值一样,那它的hashcode就一样

那么让我们思考一个现象:两个不一样的类,属性个数相同,名字可以不同,但是类型能一一对应(顺序一样),那么new出俩对象,让每一个字段的内容一样,在同一个线程中生成hashcode,这俩对象生成的hashcode应该是一样的

《Java 编程思想》这本圣经中有一段话,对 hashCode() 方法进行了一段描述。

设计 hashCode() 时最重要的因素就是:无论何时,对同一个对象调用 hashCode() 都应该生成同样的值。如果在将一个对象用 put() 方法添加进 HashMap 时产生一个 hashCode() 值,而用 get() 方法取出时却产生了另外一个 hashCode() 值,那么就无法重新取得该对象了。所以,如果你的 hashCode() 方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode() 就会生成一个不同的哈希值,相当于产生了一个不同的键。

也就是说,如果在重写 hashCode()equals() 方法时,对象中某个字段容易发生改变,那么最好舍弃这些字段,以免产生不可预期的结果。

总结

我们常见的hashcode一共只有两种:

  • 本地方法:默认实现是一个跟线程状态有关的随机数
  • 重写方法:Objects类的hashcode()方法,会把对象的属性一层层拆开调用hashcode(递归套娃),只跟字段类型和实际值有关。
相关推荐
学c真好玩1 分钟前
Spring
java·后端·spring
沉默王二4 分钟前
更快更强!字节满血版DeepSeek在IDEA中真的爽!
java·前端·程序员
2301_807449205 分钟前
字符串相乘——力扣
java·算法·leetcode
小五Z10 分钟前
RabbitMQ高级特性--消息确认机制
java·rabbitmq·intellij-idea
Kevinyu_21 分钟前
Maven
java·maven
nickxhuang26 分钟前
【基础知识】回头看Maven基础
java·maven
日月星辰Ace1 小时前
jwk-set-uri
java·后端
xiao--xin1 小时前
LeetCode100之二叉搜索树中第K小的元素(230)--Java
java·算法·leetcode·二叉树·树的统一迭代法
钢板兽1 小时前
Java后端高频面经——Spring、SpringBoot、MyBatis
java·开发语言·spring boot·spring·面试·mybatis
钢板兽2 小时前
Java后端高频面经——JVM、Linux、Git、Docker
java·linux·jvm·git·后端·docker·面试