这篇文章是为了一个人介绍一下hashMap的结构,这个人就是笨蛋小拽女
一、hashMap的数据结构 (简单,小拽女好好看看)
java1.7:数组 + 链表
java1.8:数组 + 链表 + 红黑树
咱就看java1.8,只看数组 + 链表,先不考虑链表转红黑树的情况
直接上图
1、下面只是简单的介绍下hashMap的结构和put过程
2、hashMap.get(key)方法获取数据流程(和上面put()方法寻址的方式一样)
1)先根据key通过hash算法计算出hash值,再与数组下标取余操作计算出key所在的下标
2)通过equest()判断该下标下是否有和入参key相同的key,有就返回该key对应的value,没有就返回null
二、下面讲一下底层逻辑(也不难)
1、hash算法步骤
1)通过key.hashCode()方法计算出hashCode
2)将hashCode右移16位
3)拿原hashCode值与右移16位之后的值相异或(异或运算)
说个前提:String中的hashCode()方法是继承Object里的hashCode()之后重写的
举例:你的key的hashCode值是 0001 0111 0001 1111
右移 >>16
右移16位之后是 0000 0000 0001 0111
底层源码
2、上面根据异或运算计算出hash值,下面根据hash值计算出数组下标
(n - 1)& hash
看下上面的代码,n表示数组的长度,hash代表刚才计算给出的hash值,&与运算符。
因为新建的hashMap,如果你没有指定数组长度,默认长度是16
如上图所示,(n - 1)& hash = 8,所以当前键值对应该放在数组下标为8的位置
注意:为啥n要减去1呢,16 = 0000 0000 0001 0000,15 = 0000 0000 0000 1111,可以看出2的4次幂16转成二进制里面只有一个1,而15后四位都是1。这样能保证与运算的结果最大程度上和每个key计算出的hash值有关,减少hash冲突。你想,(n - 1)转成二进制的后几位都是1,而与运算规则又是对应位都为1才输出1,所以这样决定权就交给每个key的hash值了,能保证不同的key均匀分布在每个数组下标里。
提示:(n - 1)& hash 与 hash % (n - 1)结果一样,前者是刚才说的位运算,后者是余运算,两者的计算结果相同,因为位运算的效率比较高(因为是二进制计算,所以快嘛,计算机里的数据就是二进制存储的),所以使用的位运算(n - 1)& hash
思考:为啥数组的长度都是2的次幂呢,后面在数组扩容的时候会讲
3、下面结合源码讲下put的过程
上图显示,hashMap的put方法里调用的是putVal()方法,putVal方法的第一个入参传入的是key通过hash算法返回的hash值,下面介绍putVal方法
补充:上面漏掉了一部分代码
4、下面讲下扩容,resize()方法
扩容分为两步:(1)计算新的容量;(2)重新计算key所对应新容量的下标进行填充数据
第一步代码解析:
第二部代码解析:
说一下为啥每次扩容都是2的n次幂呢,上面的注意里就给出了解释,2的n次幂转换成二进制只有一个位有1,而2的n次幂减1,低位全为1。
例如:16 = 0000 0000 0001 0000,15 = 0000 0000 0000 1111
注意:还有就是,就算你创建HashMap对象时指定了容量,构造方法中的tableSizeFor()也会根据你传的值,计算出一个最小2的幂。
例如:new HashMap<String, Object>(10), tableSizeFor()会计算出比入参大的最小的2的次幂是2的4次幂16
下面以10为例讲一下tableSizeFor()这个方法:
首先 int n = cap - 1 = 10 - 1 = 9,下面看看9的二进制数
9 = 0000 0000 0000 1001
n |= n >>> 1
n = n | n >>> 1
n = 0000 0000 0000 1001| 0000 0000 0000 1001>>> 1
n = 0000 0000 0000 1001| 0000 0000 0000 0100
n = 0000 0000 0000 1101
n | = n >>> 2
n = n | n >>> 2
n = 0000 0000 0000 1101 | 0000 0000 0000 1101 >>> 2
n = 0000 0000 0000 1101 | 0000 0000 0000 0011
n = 0000 0000 0000 1111
n | = n >>> 4
n = n | n >>> 4
n = 0000 0000 0000 1111 | 0000 0000 0000 1111 >>> 4
n = 0000 0000 0000 1111 | 0000 0000 0000 0000
n = 0000 0000 0000 1111
n | = n >>> 8
n = n | n >>> 8
n = 0000 0000 0000 1111 | 0000 0000 0000 1111 >>> 8
n = 0000 0000 0000 1111 | 0000 0000 0000 0000
n = 0000 0000 0000 1111
n | = n >>> 16
n = n | n >>> 16
n = 0000 0000 0000 1111 | 0000 0000 0000 1111 >>> 16
n = 0000 0000 0000 1111 | 0000 0000 0000 0000
n = 0000 0000 0000 1111
n = 15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
因此 tableSizeFor(10) = 16
这个方法作用就是将入参 -1 转成二进制之后,将最高位的1以下的数字都设成1,最后结果再 +1 就是比入参大的最小的2的幂
这篇文章就到这里,后续文章会记录下链表和红黑树的结构。