在计算哈希值时,使用一个乘数去乘上某个字符或数字,并累加到哈希值中,是常见的计算哈希值的方式之一。选取合适的乘数非常关键,对于大多数情况而言,31 是一个比较好的选择。
一方面,31 是个素数,相对于其它比较小的奇数来说,使用这个素数作为乘数可以减少哈希碰撞的概率。因为如果使用一个非素数,可能会与哈希表大小的公因数产生影响。
另一方面,31 可以写成 2 的 5 次方 - 1,即 31 = 2^5 - 1。这个不仅仅是个好记的数字,而且可能对于一些哈希算法来说,用相同数量的位运算(乘 31 相当于左移 5 位后减去原数,乘其它数需要更多的位运算)能够加速哈希算法的计算过程。
HashCode 是 Java Object 类的一个方法,每个 Java 对象都可以调用该方法获取一个整数类型的哈希值。底层代码如下:
public native int hashCode();
native 关键字表示该方法使用本地代码实现,即使用 C++ 或其他语言写的底层代码,具体实现方式可能因为不同的 JVM 而有所不同。
在 OpenJDK 中,可以在 JDK 源码的 OpenJDK/hotspot/src/share/vm/classfile/javaClasses.cpp 文件中找到 hashCode 方法的本地实现。该实现主要根据对象的地址和对象的字段信息生成一个哈希值。
- Java 8 版本的 OpenJDK 中 Object.hashCode() 方法底层实现的源代码:
JNIEXPORT jint JNICALL
ObjectHashCode(JNIEnv *env, jobject obj) {
oop o = JNIHandles::resolve_non_null(obj);
return markOopDesc::biased_hash_encode(o->mark());
}
可以看到,该方法的实现调用了一个叫作 markOopDesc::biased_hash_encode 的函数,该函数会返回一个整数类型的哈希值。
接下来,我们可以查看 biased_hash_encode 的实现,其实现在 hotspot/src/share/vm/oops/markOop.hpp 文件中,源代码如下:
inline unsigned int markOopDesc::biased_hash() const {
// Runtime routine (must check is_neutral() first)
return _metadata._hash;
}
static unsigned int biased_hash_shift() {
return MarkOopAlignmentInBytes + hash_bits - 1;
}
static unsigned int biased_hash_unshift(unsigned int h) {
return h << (BitsPerInt - biased_hash_shift());
}
static inline unsigned int biased_hash_encode(BasicLock* lock, unsigned int hash) {
if (UseCompressedOops && lock != NULL && !lock->is_neutral()) {
// Shift right to drop tag bits and multiple by scaling factor
hash = (hash >> biased_hash_shift()) * (unsigned int) oopDesc::biased_locker_pattern;
}
return hash;
}
static inline unsigned int biased_hash_encode(markOop m) {
if (UseCompressedOops && (address)m > MarkOopShift) {
unsigned int hash = m->metadata()._hash;
return biased_hash_encode(m->metadata().guarded_lock(), hash);
}
return m->biased_hash();
}
inline unsigned int markOopDesc::biased_hash_encode() const {
return biased_hash_encode((markOop)this);
}
函数的实现比较复杂,包含了几步操作:
- 首先,调用 markOopDesc::biased_hash() 方法获取对象的哈希值,该值存在于对象头中。
- 然后调用 biased_hash_shift() 和 biased_hash_unshift() 方法进行哈希值的编码和解码计算。这里涉及到一些位运算和数值转换的技巧。
- 最后,调用 biased_hash_encode 方法对哈希值进行编码,生成最终的哈希值。其中,该方法会判断当前对象是否启用了压缩指针(Compressed Oops)功能,如果启用了,则会对哈希值进行一定的压缩处理。
具体来说,hashCode 方法的实现逻辑包括以下几步:
-
如果对象的哈希值已经被计算过,则直接返回该值。
-
否则,使用对象地址和对象的字段信息生成一个哈希值。
-
将生成的哈希值保存在对象头中,以便下次获取哈希值时可以直接返回。
需要注意的是,由于 hashCode 方法的实现方式可能因为不同的 JVM 而有所不同,所以上述实现只是一种可能的实现方式。
哈希值是什么
哈希值是通过哈希函数对数据进行计算得到的一个固定长度的整数。哈希函数是将任意长度的输入(又称为预映射, pre-image),通过计算得到固定长度的输出(又称为哈希值,hash value)。哈希函数的输出通常具有以下特点:
相同的输入总有相同的输出,即相同的数据得到的哈希值总是相同的,因此哈希值可以作为数据的唯一标识符。
不同的输入得到的哈希值不同,即哈希函数应当尽可能避免不同的数据得到相同的哈希值,这种情况称为哈希碰撞。