
深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)
介绍
HashMap原理是JAVA和Android面试中经常会遇到的问题,这篇文章将通过HashMap在JDK1.7和1.8 中的源码来解析HashMap的原理。
相关概念
数组
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
线性链表
对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
红黑树
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3. 每个叶节点(NIL节点,空节点)是黑色的。
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
哈希表
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
哈希冲突
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
HashMap在JDK1.8中的源码
首先我们看下源码中的注释:
HashMap的继承关系
我们看到HashMap继承自AbstractMap实现了Map,Cloneable,Serializable接口。
HashMap的属性
这里有一点就是默认为什么容量大小为16,加载因子为0.75.我们通过注释来看:
As a general rule, the default load factor (.75) offers a good
tradeoff between time and space costs. Higher values decrease the space
overhead but increase the lookup cost (reflected in most of the
operations of the HashMap class, including get and put). The expected
number of entries in the map and its load factor should be taken into
account when setting its initial capacity, so as to minimize the number
of rehash operations. If the initial capacity is greater than the
maximum number of entries divided by the load factor, no rehash
operations will ever occur.
大致意思就是 16和0.75是经过大量计算得出的最优解,当设置默认的大小和加载因子时,进行的rehhash此书后最少,性能上最优。
HashMap的构造方法
我们看到HashMap的构造方法有四个,
第一个:空参构造方法,使用默认的负载因子为0.75;
第二个:设置初始容量并使用默认加载因子;
第三个:设置容量和加载因子,第二个构造方法最终还是调用了第三个构造方法;
第四个:将一个Map转换为HashMap。
下面我们看下第四个构造方法的源码:
这里我们看到构造函数中传入了一个Map,然后把该Map转换为hashMap,这里面还调用了resize()进行扩容,下面我们会详细介绍。在上面的entrySet方法会返回一个Set<Map.Entry<K,V>>,泛型为Map的内部类Entry,它是一个存放key-value的实例,为什么要用这种结构就是上面我们说的hash表的遍历,插入效率高。构造函数基本已经讲完了,下面我们重点看下HashMap是如何将key和value存储的。下面我们看HashMap的put(K key,V value)方法.
HashMap的put方法
我们看到这里调用了putVal之前调用了hash方法;
我们看到这里是将键值的hashCode做了异或运算,至于为什么这么复杂,目的大致就是为了减少哈希冲突。
下面我们看看putVal方法的源码:
可以看到这里主要有以下几步:
1、根据key计算出在数组中存储的下标
2、根据使用的大小,判断是否需要扩容。
3、根据数组下标判断是否当前下标已存储数据,如果没有则直接插入。
4、如果存储了则存在哈希冲突,判断当前entry的key是否相等,如果相等则替换,否则判断下一个节点是否为空,为空则直接插入,否则取下一节点重复上述步骤。
5、判断链表长度是否大于8当达到8时转换为红黑树。
下面我们看下HashMap的扩容函数resize()
HashMap的扩容函数resize()
前面主要介绍了, HashMap的结构为数组+ 链表(红黑树)。
总结一下上面的逻辑就是:
1、对数组进行扩容,
2、扩容后重新计算hashCode也就是key的下标,将原数据塞到新扩容后的数据结构中。
3、当存在hash冲突时,在数组后面以链表的形式追加到后面,当链表长度达到8时,就会将链表转换为红黑树。
那么对于红黑树新增一个节点 ,我们考虑到前面所说的红黑树的性质。就需要对红黑树做调整,是红黑树达到平衡。这种平衡就是红黑树的旋转。下面我们看看红黑树的旋转:
红黑树的旋转
红黑树的旋转分为左旋和右旋,以某个节点为圆心向左或向右旋转,具体我们通过下面的图来看下[https://www.cnblogs.com/CarpenterLee/p/5503882.html]。
左旋
HashMap中红黑树的左旋
右旋
HashMap中红黑树的右旋
红黑树新增节点的例子
TreeMap的结构也是红黑树,它新增节点的过程如下:这里跟HashMap的红黑树的新增原理一样
我们通过这个例子有差不多已经了解了红黑树的原理。我们回到 resize()方法,里面我们看
//✨✨✨把此树进行转移到newCap中✨✨✨
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
HashMap中TreeNode.split
这个方法中我们重点看treeify
HashMap中treeify
我们重点看这个方法balanceInsertion(root, x)这个方法就是使红黑树达到平衡。我们接着继续看,要平衡红黑树就得左右旋转。
HashMap中balanceInsertion
看到这里基本思想已经明白了,我们下面总结一下:
总结
HashMap 的存储结构
我们通过下面一副图来看,数组+链表+红黑树
HashMap的扩容
我们通过下面的图来看看HashMap的扩容过程
以上就是本文主要讲解的HashMap 的核心思想,如有不对请指证。
作者:紫雾凌寒
来源:CSDN
