HashMap概述
HashMap时常用的Java集合之一,是基于哈希表的Map接口的实现。HashMap的底层是哈希数组,数组元素为Entry,HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过“拉链法”解决冲突。
HashMap数据结构
在jdk1.8之后,解决哈希冲突的方式有了较大变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间,原本Map.Entry接口的实现类Entry改名为Node,转化为红黑树时改用另一种实现TreeNode。
Node类
1 | static class Node<K,V> implements Map.Entry<K,V> { |
TreeNode类
1 | static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { |
HashMap就是这样一个Entry(包括Node和TreeNode)数组,Node对象中包含键、值和hash值,next指向下一个Entry,用来处理哈希冲突。TreeNode对象包含指向父节点、子节点和前一个节点(移除对象时使用)的指针,以及表示红黑节点的boolean标识。
HashMap源码分析
1、定位哈希桶数组索引位置
不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是“数组+链表+红黑树”的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表/红黑树,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。
1 | // 代码1 |
方法解读:
对于任意给定的对象,只要它的hashCode()返回值相同,那么计算得到的hash值总是相同的。我们首先想到的就是把hash值对table长度取模运算,这样一来,元素的分布相对来说是比较均匀的。
但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此JDK团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道HashMap底层数组的长度总是2的n次方,并且取模运算为”h mod table.length”,对应上面的公式,可以得到该运算等同于”h & (table.length - 1)”。这是HashMap在速度上的优化,因为&比%具有更高的效率。
在JDK1.8的实现中,还优化了高位运算的算法,将hashCode的高16位与hashCode进行异或运算,主要是为了在table的length较小的时候,让高位也参与运算,并且不会有太大的开销
2、主要属性
1 | transient Node<K,V>[] table; //哈希数组 |
3、构造方法
1 | /** |
4、数据存取
putAll方法
1 | public void putAll(Map<? extends K, ? extends V> m) { |
put方法
1 | public V put(K key, V value) { |
treeifyBin方法
1 | final void treeifyBin(Node<K,V>[] tab, int hash) { |
5、get查找
1 | public V get(Object key) { |
6、resize扩容
1 | /** |
7、HashMap红黑树存储
1 | final void treeify(Node<K,V>[] tab) { |
上面主要做的是红黑树的insert,我们知道红黑树insert后是需要修复的,为了保持红黑树的平衡,我们来看下红黑树平衡的几条性质:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NULL节点)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
当insert一个节点之后为了达到平衡,我们可能需要对节点进行旋转和颜色翻转(上面的balanceInsertion方法)。
1 | static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, |