为什么要使用hashCode
参考 https://zhuanlan.zhihu.com/p/43001449
https://www.jianshu.com/p/eb9ab4211163
equals()用于判断两个对象是否相等
hashcode()被设计是用来使得哈希容器能高效的工作
为什么这么说?在Java中,有一些哈希容器,比如Hashtable,HashMap等等。当我们调用这些容器的诸如get(Object obj)方法时,容器的内部肯定需要判断一下当前obj对象在容器中是否存在,然后再进行后续的操作。一般来说,判断是够存在,肯定是要将obj对象和容器中的每个元素一一进行比较,要使用equals()才是正确的。
但是如果哈希容器中的元素有很多的时候,使用equals()必然会很慢。这个时候我们想到一种替代方案就是hashCode():当我们调用哈希容器的get(Object obj)方法时,它会首先利用查看当前容器中是否存在有相同哈希值的对象,如果不存在,那么直接返回null;如果存在,再调用当前对象的equals()方法比较一下看哈希处的对象是否和要查找的对象相同;如果不相同,那么返回null。如果相同,则返回该哈希处的对象。
int的hashcode值是其本身
查看Integer 里面的hashCode即可得知
public static int hashCode(int value) {
return value;
}
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的char数组中的数乘以31(这里是叠加乘) 再加上对应值。
为什么是用31呢?
在名著 《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:
之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。
HashMap的hash算法(为什么要右移16位和异或)
好了,知道了 hashCode 的生成原理了,我们要看看今天的主角,hash 算法。
其实,这个也是数学的范畴,从我们的角度来讲,只要知道这是为了更好的均匀散列表的下标就好了,我们来看看 HashMap 的 hash 算法(JDK 8).
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
乍看一下就是简单的异或运算和右移运算,但是为什么要异或呢?为什么要移位呢?而且移位16?
在分析这个问题之前,我们需要先看看另一个事情, HashMap 如何根据 hash 值找到数组中的对象,我们看看 get 方法的代码:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
我们看看代码中注释下方的一行代码:first = tab[(n - 1) & hash])。
使用数组长度减一 与运算 hash 值。这行代码就是为什么要让前面的 hash 方法移位并异或。
我们分析一下:
首先,假设有一种情况,对象 A 的 hashCode 为 1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000。
如果数组长度是16,也就是 15 与运算这两个数, 你会发现结果都是0。这样的散列结果太让人失望了。很明显不是一个好的散列算法。
但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置相反,则结果为1,反之为0),这样的话,就能避免我们上面的情况的发生。
总的来说,使用位移 16 位和 异或 就是防止这种极端情况。但是,该方法在一些极端情况下还是有问题,比如:10000000000000000000000000 和 10000000001000000000000000 这两个数,如果数组长度是16,那么即使右移16位,在异或,hash 值还是会重复。但是为了性能,对这种极端情况,JDK 的作者选择了性能。毕竟这是少数情况,为了这种情况去增加 hash 时间,性价比不高。