Hash算法就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。在Java中,所有的对象都有一个int hashCode()
方法,用于返回hash码。根据官方文档的定义:
The hashCode() Method
The value returned by hashCode() is the object’s hash code, which is the object’s memory address in hexadecimal. By definition, if two objects are equal, their hash code must also be equal. If you override the equals() method, you change the way two objects are equated and Object’s implementation of hashCode() is no longer valid. Therefore, if you override the equals() method, you must also override the hashCode() method as well.
Object.hashCode() 函数用于这个函数用于将一个对象转换为其十六进制的地址。根据定义,如果2个对象相同,则其hash码也应该相同。如果重写了 equals() 方法,则原 hashCode() 方法也一并失效,所以也必需重写 hashCode() 方法。
通过阅读 Java1.8中对String的源码,发现这个函数实现算法如下所示:
h a s h = s [ 0 ] ∗ 3 1 ( n − 1 ) + s [ 1 ] ∗ 3 1 ( n − 2 ) + . . . + s [ n − 1 ] hash = s[0]*31^{(n-1)} + s[1]*31^{(n-2)}+ ... + s[n-1] hash=s[0]∗31(n−1)+s[1]∗31(n−2)+...+s[n−1]
其中 s [ i ] s[i] s[i] 表示字符串 s s s 的第 i i i 个字符, n n n 表示字符串的长度。其中空字符串为0,而null会抛出 java.lang.NullPointerException
异常。
由于在算法中虽然最多会有 3 1 n − 1 31^{n-1} 31n−1 这样的指数运算,直接运算会导致算法的复杂度变为 O ( a n ) O(a^n) O(an),所以在实现时进行了优化,形式如下所示:
h a s h = s [ 0 ] ∗ 3 1 ( n − 1 ) + s [ 1 ] ∗ 3 1 ( n − 2 ) + . . . + s [ n − 1 ] = ( . . . ( s [ 0 ] ∗ 31 + s [ 1 ] ) ∗ 31 + s [ 2 ] ) ∗ 31 + . . . ) + s [ n − 1 ] hash = s[0]*31^{(n-1)} + s[1]*31^{(n-2)}+ ... + s[n-1] = (... (s[0] * 31 + s[1]) * 31 + s[2]) * 31 + ...) + s[n-1] hash=s[0]∗31(n−1)+s[1]∗31(n−2)+...+s[n−1]=(...(s[0]∗31+s[1])∗31+s[2])∗31+...)+s[n−1]
这样,在实现时,每次都做一个加乘运算,所以时间复杂度仍然为 O ( n ) O(n) O(n) 这种采用多项多的方式计算起来较方便。
其源代码如下所示:
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;
}
另外,我们可以看到,计算中使用了31这个质数作为权进行计算。可以尽可能保证数据分布更分散,但是不能保证不重复。比如:
public static void main(String[] args) throws Exception{
char c3 = (char) 3;
char c4 = (char) 4;
String[] strs = { c3 + "" + c4, "a" };
for(String s: strs) {
System.out.printf("\"%s\": %d\n", s, s.hashCode());
}
}
则结果如下,都是97。
"": 97
"a": 97
产生相同结果的原因是因为基础字符有256个,而权只有31个,所以在计算的时候有重叠的部分,因此会产生相同的hsah值。所以hsah计算是属于压缩映射,即输出的hsah值的空间要小于输入的空间(即string的值的组合可能要远大于hash值的可能性),所以不可能从散列值来确定唯一的输入值。所以在使用的时候要注意,这个编码可以作为区分码来使用,能够在一定程度上区分对象,但是在使用的时候要考虑重复的情况。
关于为什么使用31的文章很多,其中[1]比较有代表性,基本有以下几点结论:
另外,基本数据的hashCode函数的写法都相对比较简单,可以参见[2]。
[1] 科普:String hashCode 方法为什么选择数字31作为乘子, https://yq.aliyun.com/articles/665663?utm_content=m_1000022808
[2] Java基本数据类型的 hashCode源码, https://blog.csdn.net/g_y_x_/article/details/83059297