HashMap源码分析(1)-hashCode
一、HashCode
讲HashMap绝对绕不过HashCode,简单来说,一般重写equals都要重写hashcode,两个内容一样的对象,hashcode也一样,但两个hashcode一样的对象,却不一定是同样内容。以下是Java object类中关于HashCode的描述(版本1.8)
由官方描述可以知道以下几点:
- 每次在同一对象上多次调用
hashCode()
方法时,该方法必须返回相同的整数,前提是对象没有被改动过,具体要看hashcode的实现方法,该整数无需每次启动程序都保持一致。 - 如果根据equals方法两个对象相等,则对两个对象中每一个调用
hashcode
方法必须产生相同结果(当然也可以自己实现一个类让他产生不同结果,但这就很皮了)。 - 如果两个对象根据
equals
方法得出是不相等的,那么调用hashcode
方法得到的结果也应该不同,这样有助于哈希表的性能。
JVM默认实现hashcode是内存地址的映射,自然不同对象就不可能相同,但实际上不是所有的类都是直接用的Object.hashCode()
,所以根据算法不同是有hashcode相同的。
由于无法搞到SUN-JDK,Open-Jdk的JVM实现hashcode源码如下
static inline intptr_t get_next_hash(Thread* current, oop obj) {
intptr_t value = 0;
if (hashCode == 0) {
// This form uses global Park-Miller RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random();
} else if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
} else if (hashCode == 2) {
value = 1; // for sensitivity testing
} else if (hashCode == 3) {
value = ++GVars.hc_sequence;
} else if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj);
} else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = current->_hashStateX;
t ^= (t << 11);
current->_hashStateX = current->_hashStateY;
current->_hashStateY = current->_hashStateZ;
current->_hashStateZ = current->_hashStateW;
unsigned v = current->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
current->_hashStateW = v;
value = v;
}
value &= markWord::hash_mask;
if (value == 0) value = 0xBAD;
assert(value != markWord::no_hash, "invariant");
return value;
}
这个方法是从FashHashCode
走进来的,FashHashCode
其中有多个分支结构,可惜没看懂。。。以后再说吧。。。不过结合网上的资料最后还是走到这个方法来了。。
FashHashCode
方法如果这个对象本来就有个hash就直接返回,没有的话就走进get_next_hash
方法.
对get_next_hash
方法分析
根据代码的分支结构可得,一共有六种模式可以得到hashcode,在globals.hpp
中发现jvm默认使用的是模式5
product(intx, hashCode, 5, EXPERIMENTAL, \
"(Unstable) select hashCode generation algorithm")
下面的hashCode只是一个模式,不是正常理解的hashCode
- hashCode == 0: 返回随机数
- hashCode == 1: 返回内存地址做移位运算后与一个随机数异或而得到的值
- hashCode == 2: 固定值1,用于测试
- hashCode == 3: 返回一个自增序列的当前值
- hashCode == 4: 直接返回当前内存地址
- hashCode == 5: 完全没看懂,不过根据网上资料,是通过和当前线程有关的一个随机数+三个确定值,运用Marsaglia’s xorshift scheme随机数算法得到的一个随机数。这也是Java8默认的hashCode计算模式。
该模式分析参考 https://zhuanlan.zhihu.com/p/348612455
二、HashMap分析
简要看完HashCode以后,来看看HashMap中的HashCode方法
HashMap中的HashCode分析
通过断点测试,首先实例化一个HashMap,代码如下
public static void main(String[] args) {
HashMap<Integer,String> hashMap = new HashMap<>(4);
hashMap.put(2,"1");
int code = hashMap.hashCode();
System.out.println(code);
}
由代码可知,实例化了一个Key为Integer,Value为String的HashMap,随后进入hashMap.hashCode()
方法,该方法的代码如下
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}
在通过断点进入i.next().hashCode()
以后发现,他们实际上进入的是HashMap下的类中类Node
的hashCode方法,因为是通过迭代器获取了下一个节点。而类中类Node
的hashCode方法如下:
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
是调用的Objects
类的hashCode()
方法,讲key和value的hashcode异或后得出hashCode。
而Objects
类的hashCode()
方法如下:
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
可知他是根据这个Object的类型来调用对应的hashCode方法的,比如HashMap的key如果是个Integer,那就调用Integer的hashCode,而Integer的hashCode()
方法是直接返回值,那就直接返回值。如果HashMap的value是一个String,那就调用String的HashCode()
。
String的hashCode()
如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
由官方注释可知,String的hashCode计算方式为:
s
[
0
]
∗
3
1
(
n
−
1
)
+
s
[
1
]
∗
3
1
(
n
−
2
)
+
.
.
.
+
s
[
n
−
1
]
s[0]*31^{(n-1)} + s[1]*31^{(n-2)} + ... + s[n-1]
s[0]∗31(n−1)+s[1]∗31(n−2)+...+s[n−1]
至于为何选用31,有如下几个解答:
-
解答1:
- 31 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出
- 另外在二进制中,2个5次方是32,那么也就是 31 * i == (i << 5) - i。这主要是说乘积运算可以使用位移提升性能,同时目前的JVM虚拟机也会自动支持此类的优化。
-
解答2:这是在
小傅哥-面经手册
中得到的答案(来源于stackOverFlow),当时使用超过五千个单词计算hashCode,使用31、33、37、39和41作为乘积,得到的碰撞结果,31被使用就很正常了,使用31时碰撞率最低。
图片来源https://bugstack.cn/
具体计算碰撞率参考