0 写在前面的
在写这篇文章之前先把Java中HashMap的作者列出来,向他们致敬!
/*
* @author Doug Lea
* @author Josh Bloch
* @author Arthur van Hoff
* @author Neal Gafter
* @see Object#hashCode()
* @see Collection
* @see Map
* @see TreeMap
* @see Hashtable
* @since 1.2
*/
Doug Lea,Java util.concurrent包的作者。
Josh Bloch,Java 集合框架创办人,Effective Java的作者。
Arthur van Hoff,据说Java命名来源于开发人员名字的组合:James Gosling、Arthur Van Hoff和Andy Bechtolsheim首字母的缩写。
Neal Gafter,Google的软件工程师和Java的传道者。
1 频繁出现的位赋值操作
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
这是一个及其有参考意义的操作,初学者往往会直接给初始容量赋16。我们知道计算机只能记录二进制数,所以1<<4这种对位直接进行的操作对计算机来说更加容易处理,不然的话计算机需要将16先转成10000再进行存储。
2 神奇的阈值和默认链表长度
众所周知,HashMap有一个默认的阈值和链表长度,在默认状态下,他是这样设置的。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
可能大家不太理解为什么这里的初始阈值设置为0.75以及链表的最大长度设置为8,很多资料上说是为了达到时间和空间的平衡,使插入、查询的效率最高。这需要从泊松分布说起。
2.1泊松分布
泊松分布(Poisson distribution),是一种统计与概率学里常见到的离散机率分布(discrete probability distribution)。泊松分布的概率函数为:
2.2概率问题
源码中解释到,当λ的均值为0.5,阈值为0.75时,在忽略方差的情况下,k的预期出现次数是exp(-0.5) * pow(0.5, k) /factorial(k),他的首值为:
/*
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
*/
我们看到当阈值为0.75,如果要重复出现8次哈希冲突的概率为亿分之八,不足千万分之一。
现在我们就可以理解设计者的苦心了,0.75、8这些数字不是凭空想象出来的,而是靠很多数学理论支撑的!
3 更高效的取模运算(2的幂次)
关于哈希函数的原理,很多文章讲解地非常清楚,不再赘。只是想就一句话进行分析。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
Java是怎么把一个32位的int型哈希值均匀地散列到我们的一个大小为16的数组(暂时不考虑扩容)里的呢?一个常规的思路是这样的:
int i = hash % 16;
这样写完全没有问题,且计算的效率很低。
从源码里面我们就可以看出端倪来,只是简单的一步(n - 1) & hash。这里的n为数组长度,实现的过程如下,这里假设长度为16。16的二进制为0x10000,这个二进制减一就是把首位置零,后面的位全部置一,这里是0x1111=15。将减了1的0x1111和哈希值做与运算,我们知道不管是1或者0和0做与运算后都是0,不管是1或者0和1做与运算后都是它本身。通过这个运算,我们就可以把这个哈希值的后四位取出来了!且这个数恰好是小于等于我们的数组长度的,真是妙不可言,需要多次运算的%在这里通过一步按位运算就可以解决了。
这也是为什么我们的HashMap的容量都为2的幂次的一个关键原因。
4 简化了的哈希运算
下面是1.7和1.8中哈希函数的对比,不是很明白为什么在1.8中减少了扰动的次数,可能是设计者觉得没有必要进行这么多次的异或运算,想要提高HashMap的效率吧
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode()
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}
之所以进行扰动是为了充分利用对象的哈希值,同时也可以有效地避免过多的哈希冲突