我们都知道,数组的查询效率非常高,而链表的插入效率非常高;那么有没有什么是将他们结合起来使用的呢?当然就是我们要说的hashmap做到了这点。
简介
在JDK1.7版本中hashmap就是通过位桶+链表的形式来实现的。可以理解为首先由一个数组组成,在数组中每个元素下面都要一个链表;这样就是我们1.7版本实现hashmap的方式。到了JDK1.8的时候,加入了红黑树进行实现。
一、hashmap实现原理
当我们往hashmap中加入一个元素的时候,会先调用到put方法,put方法中会去调用putVal方法,我们插入元素的实现内容都在putVal方法中。去计算出key的hash值,用来确认插入到数组中的哪个链表下面,此时如果该链表上没有值就直接插入,如果有值,先判断他们的值是否一致,一致就将value进行替换;不一致去判断是否为红黑树,是红黑树就放入红黑树,不是就插入在链表的尾部,详见下图putVal的流程图。
(此图转自他出,如有侵权,联系我进行删除)
二、加入红黑树之后有什么优缺点呢?
当我们往hashmap中存入大量的数据的时候 红黑树是一棵接近于平衡的一种二叉树,他的时间复杂度为O(lgn),而链表的时间复杂度达到了O(n),显而易见加入红黑树后提高了我们的查询效率。我们自然要考虑到他是否有什么弊端,加入红黑树有什么缺点呢?每次我们往红黑树中加入元素的时候都很有可能需要对红黑树进行重置,维持再平衡,左旋右旋重新着色等操作;这样就会导致插入变得很麻烦。既然有这样的缺点为什么还要使用红黑树呢?(解释在第三个问题中)
三、为什么链表长度达到8转换为红黑树呢?
根据上面所提出的问题,设计者当然也考虑到了这点,在hashmap源码中有这样一些注解,如下图:
上面的注解通过了解之后发现,他是遵循统计学的泊松分布规律 (exp(-0.5) * pow(0.5, k) ;所得出的插入链表的第1,2,3...个数的几率,我们可以看到当链表的长度超过8插入的几率小于亿分之一,所以其实我们在使用hashmap的时候,真正用到红黑树的情况其实并不多,所以JDK1.8的hashmap的效率也仅仅比1.7中提高了8%到10%。也真是因为在8的时候转换在插入链表的几率如何低,所以选择链表长度达到8的时候将链表转换为红黑树。
四、为什么要使用加载因子为0.75?
加载因子为0.75是什么意思呢?当hashmap的数组填充比(利用率)很大的话,说明利用的空间很多,如果不对数组进行扩容的话,会增加元素碰撞的几率,其中的链表就会越来越长,链表变长就会导致查询效率降低。为什么选择0.75而不是0.6,0.8或者其他呢?通常,加载因子需要在时间和空间成本上寻求一种折中。加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时增加的是查询时间成本;加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低;所以加载因子为0.75是一个对空间与时间的折中。这样好像并不能说服为什么一定是0.75呢?还有一个原因,还是要看到上面说的泊松分布那块注解,如下图:
为什么会得出上面所说的插入链表的数超过8的几率达到亿分之一的情况呢?就是建立在加载因子为0.75的前提下。否则如果加载因子过大必然会导致当大量数据插入时,增大链表长度超过8时的概率。
五、hash容器为什么一定是2的幂次方
首先我们存放元素的时候当然最理想的状态是希望元素存放的更均匀一些,能够尽量保证存放在每个链表的几率尽量接近。那么我们就要想办法使得计算出来的hash值能够有机会进入每一个链表。如下代码所示:
static int indexFor(int h, int length) {
return h & (length-1);
}
h是hashCode通过hash算法算出来的值,后面的length-1是数组长度-1,因为长度必须为2的幂次方,所以长度值必然为10000这种形式的,那么将它减一之后就是类似于0111,01111这种除了首位为0,其余位数都为1的形式。又因为需要进行位与运算,与运算只有当上下都为1的时候才为1。如果不是0111这种情况,中间有0出现,类似于00110,00101,01010,01001等这种情况,那么不论h的二进制为什么,在有0的位上面计算出的结果将永远为0,那么就会导致数组上有部分位置将永远不可达,浪费了空间,也增加了元素碰撞的几率,使其他数组下的链表压力增大。所以只有保证数组长度为2的幂次方才能保证有机会进入到数组中的每一个链表。
(hashmap中还有许多值得去探讨研究的问题,本人小白一枚,写的不好,理解不深,还望见谅;希望通过这种方式让自己能够对知识进行深刻的学习)