前言
HashMap应该是Java集合框架中用的最多的一个集合框架了,也是每次面试面试官最喜欢问的集合框架之一。面试官喜欢问的问题我汇总如下:
- 谈谈HashMap的初始化容量是多少?
- HashMap的扩容机制是怎么样的?
- HashMap的遍历有哪几种?哪种是速度比较快的?
带着这些疑问,去解开HashMap的神秘面纱。
jdk1.8中的HashMap实现方式变成了数组+链表+红黑树的实现方式。当链表的长度超过8个node的时候采用红黑树的结构。
首先来看下源码:put方法的实现
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
首先来看下源码大致实现:
- 如果当前的table等于null,则初始化table,否则判断table[i]的首个元素是否和key一样,如果相同的话覆盖value。
- 如果table[i]是否为treeNode,即红黑树,则直接在数组插入键值对。
- 第三步table[i],判断链表长度是否大于8,大于8的话把整个tab和链表换为红黑树。
hash碰撞方法
分析源码的时候看到容量必须为2的幂次方,为什么不能是其他的数字呢,原来这里来计算该node在数组中的位置的,因为位运算比取余的速度更快。
1 | if ((p = tab[i = (n - 1) & hash]) == null) |
也可以这样写:
1 | p=tab[i=hash%(n-1)==null] |
但是这个效率太低了。至于初始容量16为什么,因为能减少碰撞的次数,加快查询的效率。16这个数字也正好是2的幂次方。
get方法获取
get方法主要通过是否通过命中节点来判断:源码大概如下:
1 | final Node<K,V> getNode(int hash, Object key) { |
- 第一次通过桶的第一个元素判断是否相等,如果相等则直接返回。
- 如果没有命中,就去红黑树中获取,否则就去链表中获取。
hashMap的扩容机制
扩容就是重新计算容量,向hashMap对象里不停的添加元素,具体来看看它的源码实现:
1 | final Node<K,V>[] resize() { |
可以看到jdk1.8的扩容是分成两个链表来进行的。对于最高位为0的数据不需要从新在hash一次,直接存放在newTab[j]中,而最高为为1的数据则放在newTab[j + oldCap] 中。这种写法非常巧妙。
遍历的方法
HashMap可以采用keySet和entrySet来遍历,其中keySet是取到所有的key,之后在通过get取到value,而entrySet则保存了key和value的值,只要取一次就够了。
jdk1.8中可以使用map.Foreach来遍历对应的数据,其对应的实现也是调用迭代器进行遍历,通常在遍历的时候采用entrySet的实现就好了。
总结
hashMap集合框架中采用了大量的位运算操作,包括hash操作和扩容时候的再次hash操作。平时开发的时候可以注意这种写法提高效率。