java HashMap解析
HashMap是java中常用且相对重要的类之一。了解此类的数据结构及储存原理对我们写程序有莫大帮助。java8中又对此类底层实现进行了优化,比如引入了红黑树的结构以解决哈希碰撞。今天我们就从底层解析一下HashMap,希望对大家有所帮助。
HashMap的数据结构
1. HashMap整体结构
Map是java中的储存键(key)、值(value)对数据结构。而HashMap即是通过key的hash值确定value的储存位置。在理想情况下,仅需要O(1)的时间就可以通过key定位到value值。不过,这里一个显著的问题是,不同的key也可能有相同的哈希值,HashMap采用数组+链表解决。
如图,HashMap的主结构类似于一个数组,添加值时通过key确定储存位置。每个位置是一个Node
这种数组+链表的组合形式大部分情况下都能有不错的性能效果,java6、7就是这样设计的。然而,在极端情况下,一组(比如经过精心设计的)键值对都发生了冲突,这时的哈希结构就会退化成一个链表,使HashMap性能急剧下降。
所以在java8中,HashMap的结构实现变为数组+链表+红黑树。如图:
当链表达到一定长度,会将链表转为红黑树。我们知道链表的查询时间为O(n),而红黑树的查询时间为O(logN)。当长度大到一定程度时,红黑树的优势会更加明显。
2. 类概览
在具体实现上,HashMap有许多内部类、方法及字段。下面列举一些比较重要的。
1 |
|
3. Node<K,V>结构
Node<K,V>是HashMap的内部类,也是其键值对的底层实现。类声明如下:
1 | static class Node<K,V> implements Map.Entry<K,V> { |
如此,HashMap的数组+链表结构就大致成形了,Node[]为数组,而Node又可连成链表。
4. TreeNode<K,V> 红黑树结构
TreeNode<K,V> 是红黑树的结构实现,类声明如下:
1 | static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { |
红黑树结构包含前、后、左、右节点,以及标志是否为红黑树的字段。此结构是java8新加的。
HashMap的实现
Put的实现
某一键值对<K,V>,添加到map中。
工作流程可概括为以下几点:
- 根据K的哈希算法确定该键值对在数组(HashMap)中的索引位置x。
- 若索引位置x为空,将<K,V>添加于此,结束。若x不为空,转向3
- 判断x处的值是否等于V,若等于,用V覆盖原值。结束。否则,转向4
- 在x处遍历链表,并在尾部插入<K,V>。判断链表长度是否大于
TREEIFY_THRESHOLD
,若小于,结束。若大于,将该链表转为红黑树结构,结束。
下面我们结合代码详细分析一下此过程。
1. 通过hash值定位元素位置
对于通过hash定位储存的Map,哈希算法对其性能有很大影响。好的哈希算法可以尽可能避免冲突的发生,使读取效率保持在O(1),下面是HashMap的哈希过程。
为表述方面,键值对设为(“hello”,”world”)。put方法源码为
1 |
|
由此可见,先对hello
进行哈希操作。hash()源码为
1 | static final int hash(Object key) { |
随后,put()过程中有一步异或操作。
i = (n - 1) & hash
n是HashMap底层数组的长度,当n为2的次方时,(n-1)&hash
等价于hash%n
,可确保得到的值落在数组索引范围内。
例如,对hello
进行哈希计算为99163451
。进行索引计算为11
,即(hello
,world
)会落在数组索引为11的位置。
2. put过程
废话不说,先上代码
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
3. 扩容
当初始化数组或数组大小到达一定程度时,都会引发扩容机制。
1 | final Node<K,V>[] resize() { |
有关红黑树及链表重新扩容的算法在下篇文章中会有介绍,HashMap扩容的大致流程如上面注解那样,需考虑当前容量及数据结构。
4.java8的性能优化
HashMap经java8的优化后,解决了哈希碰撞的问题。在哈希均匀分布的情况下,java7和java8对HashMap的性能测试中表现类似,而在哈希极端分布的情况下,java8的HashMap具有明显的性能优势。所以,如果可以的话,应选用java8的HashMap。
————–全文完——————
参考文章