1. 基本数据结构
HashMap的底层数据结构是一个Hsah表,哈希表允许Java通过一个Hash函数将一个对象映射到一个数组的索引上,从而达到近似O(1)的访问和插入效率。
① 底层数组
那么Hash表的底层就是一个数组和一个哈希函数。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
transient Node<K,V>[] table;
Node是HashMap中的基本节点,而table则是存储Hash表的底层数组
② 哈希函数(干扰函数)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
其实在java中的哈希函数基本都是通过对象的hashCode函数实现的,Java并没有选用一些特别的hash算法。而是将这个实现能力几乎交给了用户对象自己实现各自相应的算法。
上述的实现是Java对于对象的hashCode算法的一个干扰函数。其会将哈希二进制编码的高位扩散到哈希二进制编码低位,以使得哈希吗的高位也可能参与到哈希计算中,以增强哈希吗计算的随机性,以及减少相似的哈希码会产生的冲突。
举一个场景:如果哈希吗 2414 1001 和 5127 1001 两个编码(并非二进制)进行哈希运算,虽然两个编码的高位差异非常大,但是如果我们的哈希掩码是 0000 1111,那么其实哈希值的高位永远不可能参与到计算中。其实这样对于数字范围较大的数字,即是减少了哈希函数的随机性。
③ 掩码运算
在通常我们会认为HashMap的掩码运算就是HashMap的哈希函数——即很多人会认为,HashMap的哈希函数就是对hash值取余。其实我认为这个观点是错误的。取余只是一个掩码运算而并非一个哈希函数,在每一个哈希函数运算之后,得到一个索引只会,都会使用一个掩码对哈希值取余,受限于我们的内存永远是有限的内存,我们永远都希望能够尽量使用更少的内存,几乎所有的哈希函数计算完成之后都需要进行取余操作。
tab[(n - 1) & hash]
因此HashMap需要获取一个元素时,就需要像上述一样计算数组的下标,否则就一定会越界。
④ 常见的哈希函数——字符串
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;
}
以上代码是Java字符串的哈希函数,这个哈希函数的逻辑非常简单,计算方式是将字符串中的每一个char,乘以31,加上下一个char,再用结果乘以31,依次循环得到最后的哈希值。
这是Java中基于字符串实现的一个简单哈希计算的函数。
2. 操作详解
① 表容量计算函数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n