一、HashMap使用的数据结构是哈希表
哈希表在JDK 8之前由数组和链表组成,JDK 8之后由数组、链表和红黑树组成
我们先来简单了解下哈希表的原理,下图中有一个数组,数组元素是键值对(为了对接HashMap,哈希表不一定存储键值对,其实可以简单的理解为:存储键值对的哈希表就是HashMap),
一般来说,不同的键计算出来的哈希值是不相同的,但是也会有相同的情况,这种情况我们称为哈希碰撞,此时就会使用到链表将多个拥有相同哈希值的键值对连接起来
JDK 8以前:新元素存入数组,老元素挂在新元素后面
JDK 8以前:新元素直接挂在老元素后面
而在JDK 8之后,为了防止链表过长,导致降低查找效率,当链表长度大于8并且数组长度大于等于64时,链表就会自动的转化为红黑树
为什么需要哈希表?
我们想,如果上面的数组是一个水果的价格表,当我们要查找某个水果时,我们是不是只能从上到下逐一查看,才能找到我们要的那个水果的价格,此时的时间复杂度为O(n),如果通过哈希函数,直接计算出这个水果的哈希值,就直接确定了该水果在价格表中的位置,此时的时间复杂度就是O(1),效率大大提高
二、HashMap源码分析
上面讲到了HashMap的基本原理,那具体该如何通过代码实现呢?接下来就分析HashMap源码
HashMap 有个静态内部类 Node ,在HashMap中每个元素都是一个 Node ,并且 Node 实现了 Map中的Entry接口,Node中有四个变量
- hash:通过键计算出来的哈希值
- key、value:键、值
- next:用于形成链表的时候指向下一个Node
当链表转换为红黑树后,Node也会转换为TreeNode
- parent、left、right:分别记录父节点、左节点、右节点(prev先不用了解)
- red:记录当前节点是红色还是黑色
并且 TreeNode 继承了 LinkedHashMap 里面的 Entry,而LinkedHashMap.Entry又继承了HashMap.Node 因此 TreeNode 内部也含有键值对
在HashMap中有一个成员变量table,是一个Node数组,对应的就是上面讲的那个数组,
接下来看HashMap中定义的几个常量
- DEFAULT_INITIAL_CAPACITY:table的初始长度,1 << 4 , 1左移4位,就是16
- MAXIMUM_CAPACITY:table的最大长度
- DEFAULT_LOAD_FACTOR:扩容因子,0.75 表示当table的使用量达到 3/4 时,就进行扩容,扩容到之前的两倍
下面是HashMap的无参构造函数,只进行了一个赋值操作,并未创建table数组
table数组是在 put 的时候创建的,put方法调用了putVal方法,在putVal方法中传入了hash(key),这是调用hash方法计算出key的哈希值,参数onlyIfAbsent 选择 false 表示当传入一个表中已存在的键(值可以不同)时,覆盖掉原来的键值对,选择true不覆盖
方法的返回值是V,发生覆盖的时候,put返回被覆盖的值,没有发生覆盖则返回null
putVal的源码如下, 我在源码中通过注释进行了一些说明
为什么在HashMap里面就有一个全局table,在这还要一个局部的tab呢?
这是因为全局的table在对象中,需要跑到堆中访问,而方法中的局部变量存在于栈中,访问较块
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { //定义一个局部变量,用来记录哈希表中数组的地址值 Node<K,V>[] tab; //临时的第三方变量,用来记录键值对对象的地址值 Node<K,V> p; //表示当前数组长度 int n; //表示索引 int i; //这一步将哈希表中数组的地址值,赋值给局部变量tab if ((tab = table) == null || (n = tab.length) == 0) /** 数组对象的创建就是在resize方法中完成 resize方法主要完成了以下几个工作: 1.如果当前是第一次添加数据,底层会创建一个长度为16,加载因子为0.75的数组 2.如果不是第一次添加数据,会看数组中的元素是否到达了扩容的条件 如果没达到,不做任何操作, 如果达到,会新创建一个大小为原先两倍的新数组,并把数据全部转移到新数组中 */ n = (tab = resize()).length; /** i = (n - 1) & hash 将 数组的长度减一 和 键的哈希值 进行 与运算, 得到当前键值对对象在数组中应存入的位置 如果该位置是null,则通过newNode方法创建Node对象直接放入数组 如果不是则走else */ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //判断要添加的键值对的键是否和数组中原本的键值对的键一样 //如果相等则说明需要覆盖,用e记录下被覆盖的键值对 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判断数组中获取出来的键值对是不是红黑树中的节点 //如果是,则调用方法putTreeVal,把当前的节点按红黑树的规则添加到树当中 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //如果不是红黑树,那就是链表,则按照下面的方法添加 else { //这个for就是不断的往后查看节点 for (int binCount = 0; ; ++binCount) { //如果已经走到最后一个节点,则直接创建节点挂在后面 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); /** 判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin treeifyBin方法的底层会继续判断数组的长度是否大于等于64 如果同时满足这两个条件,就会把这个链表转成红黑树 */ if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //判断当前正在查看的键值对的键跟要插入的键值对的键是否一样 //如果一样,则表明需要进行覆盖,执行break,此时e记录要被覆盖的键值对 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果e为null,表示当前不需要覆盖任何元素 //如果e不为null,那么记录的就是要被覆盖的键值对 //然后按照以下的方法进行覆盖,return 被替换的 值 if (e != null) { // existing mapping for key V oldValue = e.value; //onlyIfAbsent 就是传入的第四个参数 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //跟并发修改异常有关 ++modCount; //threshold:记录的就是数组的长度 * 0.75 ,可以理解为哈希表的扩容时机 if (++size > threshold) resize(); //跟LinkHashMap有关 afterNodeInsertion(evict); //表示当前没有覆盖任何元素,返回null return null; }