前置知识
在Java中==
比较的是对象地址
equals
默认也是对象地址,但是如果重写以后就要按照重写的逻辑来进行比较,比如String
重写了equals这个方法,他就会比较字符串字面值
提到重写equals
,就一定会提到重写hashcode
方法,这个问题本文不会展开,默认读者已经了解过这个问题了。
什么是 hashCode
简单来说,hashCode()
是一个本地方法(native method) ,定义在 java.lang.Object
类中,意味着 Java 中的每一个对象都与生俱来拥有获取 hashCode
值的能力。它返回一个整数值,这个值理论上应该尽可能唯一地标识该对象在其生命周期内的某种特征,以便在特定的数据结构中(如 HashSet
、HashMap
等基于哈希表实现的集合)能够快速定位和操作对象。
从底层实现看,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
(递归套娃),只跟字段类型和实际值有关。