字节面试官问我,HashMap 的源码看过吗?我,华为java面试一共几轮

本文详细解析了HashMap的数据结构特点(如table数组、size、modCount和threshold),介绍了hash算法(包括hashCode、散列值计算和冲突处理),并剖析了put()方法的工作原理。重点讲解了负载因子、负载均衡和并发修改策略。
摘要由CSDN通过智能技术生成

1)table 是一个 Node 类型的数组,默认长度为 16,在第一次执行 resize() 方法的时候初始化。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

final HashMap.Node<K,V>[] resize() {

newCap = DEFAULT_INITIAL_CAPACITY;

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

}

Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质上是一个键值对。

static class Node<K,V> implements Map.Entry<K,V> {

final int hash;

final K key;

V value;

HashMap.Node<K,V> next;

Node(int hash, K key, V value, HashMap.Node<K,V> next) {

}

public final K getKey() { return key; }

public final V getValue() { return value; }

public final String toString() { return key + “=” + value; }

public final int hashCode() {

}

public final V setValue(V newValue) {

}

public final boolean equals(Object o) {

}

}

2)size 就是 HashMap 中实际存储的键值对数量,它和 table 的 length 是有区别的。

为了说明这一点,我们来看下面这段代码:

HashMap<String,Integer> map = new HashMap<>();

map.put(“1”, 1);

声明一个 HashMap,然后 put 一个键值对。在 put() 方法处打一个断点后进入,等到该方法临近结束的时候加一个 watch(table.length),然后就可以观察到如下结果。

也就是说,数组的大小为 16,但 HashMap 的大小为 1。

3)modCount 主要用来记录 HashMap 实际操作的次数,以便迭代器在执行 remove() 等操作的时候快速抛出 ConcurrentModificationException,因为 HashMap 和 ArrayList 一样,也是 fail-fast 的。

关于 ConcurrentModificationException 的更多信息,请点击下面的链接查看 03 小节的内容。

4)threshold 用来判断 HashMap 所能容纳的最大键值对数量,它的值等于数组大小 * 负载因子。默认情况下为 12(16 * 0.75),也就是第一次执行 resize() 方法的时候。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final float DEFAULT_LOAD_FACTOR = 0.75f;

final HashMap.Node<K,V>[] resize() {

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}

5)loadFactor 为负载因子,默认的 0.75 是对空间和时间效率上的一个平衡选择,一般不建议修改,像我这种工作了十多年的老菜鸟,就从来没有修改过这个值。

02、HashMap 的 hash 算法

Hash,一般译作“散列”,也有直接音译为“哈希”的,这玩意什么意思呢?就是把任意长度的数据通过一种算法映射到固定长度的域上(散列值)。

再直观一点,就是对一串数据 wang 进行杂糅,输出另外一段固定长度的数据 er——作为数据 wang 的特征。我们通常用一串指纹来映射某一个人,别小瞧手指头那么大点的指纹,在你所处的范围内很难找出第二个和你相同的(人的散列算法也好厉害,有没有?)。

对于任意两个不同的数据块,其散列值相同的可能性极小,也就是说,对于一个给定的数据块,找到和它散列值相同的数据块极为困难。再者,对于一个数据块,哪怕只改动它的一个比特位,其散列值的改动也会非常的大——这正是 Hash 存在的价值!

同学们已经知道了,HashMap 的底层数据结构是一个数组,那不管是增加、删除,还是查找键值对,定位到数组的下标非常关键。

那 HashMap 是通过什么样的方法来定位下标呢?

第一步,hash() 方法:

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

第二步,putVal() 方法中的一行代码:

n = (tab = resize()).length;

i = (n - 1) & hash;

为了更容易理解,我把这两步的方法合并到了一起:

String [] keys = {“沉”,“默”,“王”,“二”};

for (String k : keys) {

int hasCode = k.hashCode();

int right = hasCode >>> 16;

int hash = hasCode ^ right;

int i = (16 - 1) & hash;

System.out.println(hash + " 下标:" + i);

}

1)k.hashCode() 用来计算键的 hashCode 值。对于任意给定的对象,只要它的 hashCode() 返回值是相同,那么 hash() 方法计算得到的 Hash 码就总是相同的。

要能够做到这一点,就要求作为键的对象必须是不可变的,并且 hashCode() 方法要足够的巧妙,能够最大可能返回不重复的 hashCode 值,比如说 String 类。

public int hashCode() {

int h = hash;

if (h == 0 && value.length > 0) {

char val[] = value;

for (int i = 0; i < value.length; i++) {

h = 31 * h + val[i];

}

hash = h;

}

return h;

}

2)>>> 为无符号右移运算符,高位补 0,移多少位补多少个 0。

3)^ 为异或运算符,其运算规则为 1^0 = 1、1^1 = 0、0^1 = 1、0^0 = 0。

4)& 为按位与运算符,运算规则是将两边的数转换为二进制位,然后运算最终值,运算规则即(两个为真才为真)1&1=1、1&0=0、0&1=0、0&0=0。

关于 >>>^& 运算符,涉及到二进制,本篇文章不再深入研究,感兴趣的同学可以自行研究一下。

假如四个字符串分别是"沉",“默”,“王”,“二”,它们通过 hash() 方法计算后值和下标如下所示:

27785 下标:9

40664 下标:8

29579 下标:11

20108 下标:12

应该说,这样的 hash 算法非常巧妙,尤其是第二步。

HashMap 底层数组的长度总是 2 的 n 次方,当 length 总是 2 的 n 次方时,(length - 1) & hash 运算等价于对数组的长度取模,也就是 hash%length,但是 & 比 % 具有更高的效率。

03、HashMap 的 put() 方法

HashMap 的 hash 算法我们是明白了,但似乎有一丝疑虑,就是万一计算后的 hash 值冲突了怎么办?

比如说,“沉X”计算后的 hash 值为 27785,其下标为 9,放在了数组下标为 9 的位置上;过了一会,又来个“沉Y”计算后的 hash 值也为 27785,下标也为 9,也需要放在下标为 9 的位置上,该怎么办?

为了模拟这种情况,我们来新建一个自定义的键类。

public class Key {

private final String value;

public Key(String value) {

this.value = value;

}

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != o.getClass())

return false;

Key key = (Key) o;

return value.equals(key.value);

}

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

总目录展示

该笔记共八个节点(由浅入深),分为三大模块。

高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。该笔记将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这4个方面重点介绍。

一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,将用一个节点来专门讲解如何设计秒杀减库存方案。

高可用。 虽然介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。笔记的最后,将带你思考可以从哪些环节来设计兜底方案。


篇幅有限,无法一个模块一个模块详细的展示(这些要点都收集在了这份《高并发秒杀顶级教程》里),麻烦各位转发一下(可以帮助更多的人看到哟!)

由于内容太多,这里只截取部分的内容。

被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,将用一个节点来专门讲解如何设计秒杀减库存方案。

高可用。 虽然介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。笔记的最后,将带你思考可以从哪些环节来设计兜底方案。


篇幅有限,无法一个模块一个模块详细的展示(这些要点都收集在了这份《高并发秒杀顶级教程》里),麻烦各位转发一下(可以帮助更多的人看到哟!)

[外链图片转存中…(img-YJ3veQDa-1711106903639)]

[外链图片转存中…(img-QhmoZ5rj-1711106903639)]

由于内容太多,这里只截取部分的内容。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值