HashMap深入剖析笔记

散列表

HashMap的核心是散列表,散列表是一个基于数组实现实现的数据结构,使用的是数组支持随机下标访问的特性,通过将一个key通过hash函数转化为数组下标。
但是不同的key算出来的hash值可能相同,这就导致了hash冲突(也叫hash碰撞)
对于hash碰撞有两种解决方法(目前学到的,如果有后续再补充)

  1. 开放寻址法
    开放寻址法是指,当发生hash冲突的时候(存入数组时,发现这个位置已经被占用了),那么就向后寻找,到了末尾还没有找到,再从头开始向后遍历,如果遍历到了空位,就将该数据插入进去,但是散列表中空闲位置不多时,hash冲突的概率还是大,所以就有负载因子(load factor),负载因子=已有元素数量/散列表长度,用来表示空位的多少

如图:蓝色表示已有数据在这里插入图片描述

  1. 链表法
    而链表法则是指,当发生hash冲突时,以当前位置的数组中对象作为链表中的节点,将要存储的对象作为链表节点存入
    在这里插入图片描述

jdk1.7的底层数据结构

jdk1.7的底层就是通过数组+链表来实现的
HashMap默认长度为16
负载因子默认为0.75
通过Entry对象封装key,value作为节点
在put新数据的时候,才初始化数组

1.7的扩容:当HashMap的数据数量超过了阈值(容量x负载因子)且待插入数据的数组位不为null时(也就是待插入数据将作为节点插入链表),就扩容为原容量的2倍
为什么是2倍呢,而不是其他倍数?这和后面写的HashMap的位运算有关
插入的方式为头插法,这里应该是考虑的后插入的数据可能会被先使用,所以每次插入的数据位置,都是链表的头结点,也就是在数组内

jdk1.8的底层数据结构

1.8的底层较1.7发生了很大的变化
数组生成和1.7一样,调用构造方法时不会生成数组,而是在put的时候才对数组初始化,HashMap的默认数据也和1.7的一样,不过多了关于红黑树的参数

1.8中数据的插入方式变成了尾插法
1.8的底层数据结构是数组+链表+红黑树,当发生hash冲突时,时,首先采用数组+链表的结构,当链表节点数超过8个,且HashMap容量超过64时,链表就转为红黑树
1.8的扩容:当数据数量超过阈值时,就进行扩容
当红黑树节点小于等于6的时候,红黑树又会转变回链表结构
在这里插入图片描述
红黑树是一种弱平衡二叉树:

  • 节点可以是红色或者黑色
  • 根节点必须是黑色
  • 叶子节点必须是黑色
  • 红色节点的两个子节点必须是黑色
  • 一个节点到任意节点的路径上,黑色节点数一定相同

红黑树的插入和查找都是O(logn)
为什么选择8为转变值呢?
官方说法是考虑了泊松分布,因为取到8的几率并不高,所以并不需要经常树化,虽然红黑树的效率高,但是链表和红黑树之间的转换也是很消耗空间和时间的,在节点数量少的情况下,遍历链表的操作更方便

 * 0:    0.60653066
 * 1:    0.30326533
 * 2:    0.07581633
 * 3:    0.01263606
 * 4:    0.00157952
 * 5:    0.00015795
 * 6:    0.00001316
 * 7:    0.00000094
 * 8:    0.00000006
 * more: less than 1 in ten million

HashMap中的一些位运算

hash值的计算

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
   

传入一个key,调用Object类中的hashcode方法,这是一个native方法
然后将获取到的hash值无符号右移16位再进行异或运算(相同为1,不同为0),因为int类型32位,最高为符号位,所以无符号右移16位也就是将高位16位变成低位,再与原来进行异或运算,相当于是高位和低位进行异或运算

			  h:0000 1101 1010 0100 0000 1010 0100 0001
无符号右移16位后:0000 0000 0000 0000 0000 1101 1010 0100
高低位异或运算:   0000 1101 1010 0100 0000 0101 1110 0101

也就是高位不变,再hash,降低hash碰撞

数组容量

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

这个方法保证了,无论传入容量参数的是否是2的幂次方,都会找到大于传入参数的的最小二次幂数。

比如传传入一个13:

n=13: 0000 0000 0000 0000 0000 0000 0000 1101
>>>1: 0000 0000 0000 0000 0000 0000 0000 0110
|=  : 0000 0000 0000 0000 0000 0000 0000 1111

n等于15了,之后异或运算也不会发生改变了

int n = cap - 1;

为了避免传入的就是二次幂,进行运算后导致翻倍,所以,先减1,最后再加1

让31位都无符号右移,又通过或运算(有1为1),保障了最后所得到的值是2n-1,最后再加1,得到2n

为什么必须要2n,其他数不行吗?这就是之前提到的为什么要扩容为2倍。关系到数组下标的计算

数组下标的计算

在1.7中是作为方法使用,而1.8直接取消了这个方法,但是下标计算的核心还是没有变

index = (n - 1) & hash

数组容量-1的值与得到的hash值进行与运算(都是1才为1),相当于取余操作,假设n=16,n-1=15

n-1:  0000 0000 0000 0000 0000 0000 0000 1111
hash: 0000 1101 1010 0100 0000 0101 1110 0101
&	: 0000 0000 0000 0000 0000 0000 0000 0101

最后得到的值为5,也就是tab[5]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值