HashMap是基于哈希算法的键值对存储容器,底层使用散列桶实现,当发生碰撞时使用链表存储,java8引入了红黑树来提升性能。HashMap中有很多设计需要我们取考虑,比如通过hash()计算index;在put()和get()时,equal()和hashCode()都有什么作用;如何综合考虑空间利用率和查找效率,等等。本文就从这些方面来聊聊HashMap。
1.HashMap基本原理
table即为散列桶,当put操作时,通过哈希算法计算索引(index),决定元素添加到哪个散列桶下。当不同的元素key的hashCode相同时,就发生了哈希碰撞。这时HashMap通过链表来解决哈希碰撞问题。为了综合考虑空间利用率和查找效率,HashMap定义了负载因子(loadFactor)和容量(capacity)。当size > (loadFactor * capacity),就会扩容2倍,然后重新哈希(resize)。
2.在put()和get()时,equal()和hashCode()都有什么作用
put
- 1.对key的hashCode()做hash,然后计算index [index = (n-1)&hash]
- 2.如果没有哈希碰撞就直接放到bucket[index]里(bucket[index]中没有元素)
- 3.如果碰撞了,以链表的形式存入bucket[index] (java8中可能使用treeMap)
- 4.如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换为红黑树 聊聊红黑树
- 5.如果节点已经存在,就替换旧值(key以存在,保证key的唯一性)
- 6.如果bucket满了(超过load factor*current capacity),就要resize。
key.hashCode()是用来计算index的,决定元素存入哪个散列桶;key.equals()用来判断散列桶中是否已经存在该键值(保证key的唯一性)
get
- 1.对key的hashCode()做hash,然后计算index [index = (n-1)&hash]
- 2.如果桶中没有元素,返回null
- 3.如果桶中有元素,通过key.equals()查找链表/treeMap
3.hash()实现
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
p = tab[i = (n - 1) & hash]
可以看到这个函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或。
设计者的解释是(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
对此有人通过信息论角度解释到,这是为了利用高位的信息。
4.HashMap如何扩容
为了综合考虑空间利用率和查找效率,HashMap定义了负载因子(loadFactor)和容量(capacity)。当size > (loadFactor * capacity),就会扩容2倍,然后重新哈希(resize)。
HashMap如何resize的问题上其实设计也很巧妙:当resize时,因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
怎么理解呢?例如我们从16扩展为32时,具体的变化如下:
假设bucket大小n=2^k,元素在重新计算hash之后,因为n变为2倍,那么新的位置就是(2^(k+1)-1)&hash。而2^(k+1)-1=2^k+2^k-1,相当于2^k-1的mask范围在高位多1bit(红色)(再次提醒,原来的长度n也是2的次幂),这1bit非1即0。如图:
所以,我们在resize的时候,不需要重新定位,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话位置没变,是1的话位置变成“原位置+oldCap”。
5.至此,我们对HashMap的讨论基本结束了,接下来对hashmap的其他内容做简要介绍:
1.HashMap可以接受null键值和null值(HashTable不能);HashMap是线程不安全的,多线程环境下,推荐使用ConcurrentHashMap。
2.String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。
参考资料:
Java HashMap工作原理及实现
HashMap的工作原理