HashMap关键就这几个点,你Get到了?


先贴几个基本符号热个身

  1. & 与 两个位都为1时,结果才为1
  2. | 或 两个位都为0时,结果才为0
  3. ^ 异或 两个位相同为0,不同为1
  4. ~ 取反 所有位置0变1,1变0
  5. << 左移 各二进位全部左移若干位,高位丢弃,低位补0
    6.>> 带符号右移 各二进位全部右移若干位,低位丢弃,高位补为符号位
    7.>>> 无符号右移 各二进位全部右移若干位,低位丢弃,高位补0

1. 数据结构 数组和链表 插入 查询 和删除

1.1 数据结构数组加链表
数组特性: 查找比较快插入 删除比较慢,因为在他后边的所有数据下标都会变
链表:查找比较慢,需要从链表头部开始遍历链表获取,插入和删除比较快,只要改变上一个和自身的next 指引就可以了
数组和链表结合,这样的数据结构使得 插入 删除 查询 各项效率都变得比较快
jdk 1.8 变成了数组 链表/红黑树

2. Hash 与 容量长度-1时下标会更加均匀

hash & (len -1) = 数组下标
数组加链表的结构,数组通过key生成的 hash 然后和 数组长度-1进行& 运算后生成的一个下标值,因为下标是从0 开始的,所以 数组的下标= hash 值 & (数组长度-1) 这样就会获取一个下标用于存储数据,但是容量为啥是2的倍数呢, 用两个数出来分析下
16,我们知道一个偶数 假设数组长度是 16 那么用于计算下标的是16-1=15 ,15的二进制是1111,二进制最右一位是1, 这样无论 hash 值是一个奇数还是偶数,都会有同样的概率生成下标为奇数和偶数的下标数。 比如数 101 ,103 101 & 15 =0 103 &15= 1(1100111 & 1111=1) 生成的下标是0和 1
17,如果数组长度是一个奇数 17,那么用于计算长度的下标是16 二进制是10000,二进制最后一位是0,这样无论hash值是 奇数还是偶数 & 16 生成的 下标是只能是一个偶数,那么一半的存储空间就浪费掉了, 比如数 101 ,103 101 & 16 =0 103 &16= 0 ,生成的下标都是0 ,发生了hash 碰撞
所以说容量的长度是2的倍数时生成的下标最分散。原因是& 操作 两个位都为1时,结果才为1,所以只有一个数组长度是2的倍数,偶数,用于计算的下标长度才可能是一个奇数,保障了二进制最后一位是1.

3. 容量16

首先需要明白一点就是 容量必须是2的幂指数,那么 2,4,8,16,32 等等都可以是初始容量,但是数组的下标计算是 hash & len-1 这样会得到一个数组下标,如果初始容量太小,容易发生
hash 碰撞,且频繁触发扩容,损耗性能,所以选择16 作为一个初始容量值,这是一个经验值,不是一个必然值,初始容量也不是非16不可,这个初始容量我们在初始化链表时也是可以设置的。

4. 负载因子 0.75

负载因子是指,数组的下标占用率,比如说初始数组长度是16 ,如果已经有12个下标已经被用于存储数据了,那么在存储第13个数据的时候,就会发生扩容,负载因子也是可以设置的,负载因子越小数组内发生碰撞的概率就会越小,因为有足够多的盒子来存放数据但是占用的内存就会越大,负载因子越大,说明数组中数据的占用率越高,当然发生hash 碰撞的概率就会越大,带来的就是性能的下降,但是内存占用比较小。 打个比喻,有10个人,负载因子大就好比,一辆车一次把10 个人都拉走,这样运费低省成本,但是车子跑的慢, 负载因子小就好比我 一辆车就座5个人,我用两辆车拉,这样虽然运费高,但是我速度快。 这个就是一个时间和空间的平衡

5. hash 的计算与扰动 函数

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

^ : 异或 ,两个位相同为0,不同为1
这个是hash 函数的计算公式,如果key 是string 可以看下

public int hashCode() {
    int h = hash;
    final int len = length();
    if (h == 0 && len > 0) {
        for (int i = 0; i < len; i++) {
            h = 31 * h + charAt(i);
        }
        hash = h;
    }
    return h;
}

通过String 函数获取一个hash值,这个31 完全是一个经验值,他使得生成的hash 值更加均匀平滑。主要原因是他生成的值既在int 类型范围内又相对的均匀分散不容易发生碰撞。
当然还有其他说法
1. 是一个奇质数。
2.另外在二进制中,2个5次方是32,那么也就是 31 * i == (i << 5) - i。这主要是说乘积运算可以使用位移提升性能,同时目前的JVM虚拟机也会自动支持此类的优化。
3.通过大量数据证明,经验值

5.1 扰动函数 :

扰动的目的是使得数据的高低位变得更加的均匀,我们在计算出一个hash 值后拿到一个int 值,
int类型在内存中占用了4个字节,也就是32位。int类型是有符号的,因此,32位并不会全部用来存储数据,使用最高位来存储符号,最高位是0,表示数据是正数,最高位是1,表示数据是负数,使用其他的31位来存储数据。
(h = key.hashCode()) ^ (h >>> 16)
通过 异或 (^) 将生成的hash 值 ^ 上 hash 右移16 位 后的数 ,就生成了一个 经过扰动后的hash 值,经过扰动后生成的数高低位混合,数据位分布更加均匀,这样就会避免数据都集中在高位,低位是0 导致生成下标都是偶数,导致哈希碰撞几率增大。

6. 自定义容量如何找2的倍数

容量是我们可以自己定义的,但是实际的容量必须是2的倍数,所以每次传入一个自定的容量,hashmap 就会根据我们传入的值检验一遍这个值,并通过程序获取这个值最近的一个2的倍数值作为实际的容量

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的倍数建减一的数
2 10 减一 1 1
4 100 减一 11 3
8 1000 减一 111 7
16 10000 减一 1111 15
这主要是为了把二进制的各个位置都填上1,当二进制的各个位置都是1以后,就是一个标准的2的倍数减1了,最后把结果加1再返回即可。
比如 我们定义一个自定义hashmap 长度是11二进制是 1011 最后右移完是 1111 = 15 长度是 15 +1 =16这样就找到了最近的2的倍数的容量长度 16,那么实际使用的是16 。

7. 1.8 1.7 的扩容方案

1.7 扩容 方案:容量方案和1.8 是一样的,都是原容量增加一倍。但是扩容时的扩容思路是完全不一样的,1.8 的方法也更加巧妙
1.7进行扩容时会重新计算一遍hash 值与上 新的数组(长度-1),然后根据下标将数据放到新的数组中。
1.8 对流程进行巧妙的改变,不需要重新计算hash ,假设原始容量是16 ,扩容新增容量16,原哈希值与扩容新增出来的长度16,进行&运算,如果值等于0,则下标位置不变。如果不为0,那么新的位置则是原来位置上加16
首先找两个数key = aa,e4we,一个需要扩容后改变下标一个不需要

7.1 使用算法计算hash

int hash = key.hashCode() ^ (key.hashCode() >>> 16);
容量16下的下标
aa= 110000100000 & (16-1) = 下标 0
e4we =1011101011101101010011 & (16-1) = 下标 3
容量32 下的下标
aa= 110000100000 & (32-1) = 下标 0
e4we =1011101011101101010011 & (32-1) = 下标 19
使用1.8扩容方案后只需要 原哈希值与扩容新增出来的长度16,进行&运算
110000100000 & 16(10000) =0 为0数据下标位置不变
1011101011101101010011 & 16 (10000)=10000 不为0,新的下标是 新增容量 16 + 3(原下标)= 19 (扩容后的下标),所以在1.8 中只需要将原 hash 值& 上扩容的容量 看下是否值为0,为0 在新数组中下标不变,不为0 在新数组中的位置是老的下标 加上 新增容量长度 等于 扩容后的下标,这里设计的非常巧妙,需要仔细体会下。

8. 扩容条件 链表长度大于8 且数组长度 大于64

在1.8 hashMap 数据结构变成了 数组 + 链表或者 红黑树,主要是在发生hash碰撞时使用红黑树提升查询 插入 和 删除 性能。
数组长度大于64以后,如果某个下标上发生了hash 碰撞,且链表长度大于8就会链表转 红黑树,红黑树本质上是一种二叉算法,有利于提升查找 插入 和 删除性能 ,当链表转化为红黑数的过程会把原来的链表顺序记录,在后面 如果红黑树元素少于6个会退化为一个链表。 这里6和 8的差距主要是为了极端情况下 防止 链表和 红黑树的来回转换。

9. 树形结构

9.1 二叉搜索树

1.理性下是平衡的二叉数
2.可能退化为一个链表

9.2. 2-3树和 234 树

红黑树的前身,主要思路是在一个节点下有2个或者3个或者4个子接点

9.3 3.红黑树

1.每个节点是红的或者黑的
2.根节点是黑的
3.每个叶子节点是黑色的
4.如果一个节点是红的,则他的两个儿子都是黑的
5.对每个结点,从该节点到其子孙节点的所有路径上的包含相同数目的黑节点

10 线程不安全可能出现的问题

HashMap是线程不安全的,并且其扩容的代码会将原来的链表进行反序,例如原先的是 3-> 2-> 1,现在还在同一位置则是 1->2->3,那么在多线程并发操作的情况下一个正序,一个反序,就会有可能出现。

10.1 什么时候会出现环链?

多线程操作,同时去扩容,当一个线程已经操作完毕了,将原来的顺序反过来了;另一个线程再开始执行扩容代码,此时就会出现环链。

https://zhuanlan.zhihu.com/p/91960960
https://blog.csdn.net/jarniyy/article/details/124537921

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值