深入源码分析,我目前的想法是:最好是能 debug,对于特殊的情况,能简单/快速的模拟出来。然后再配上一些面试题食用,效果应该不错。
目前想到的关于 HashMap 深入的点:
- HashMap 本身实现
-
- hash 算法
- 如何 put 值:链表+红黑树
- 扩容实现
- 遍历/查找/删除 等实现
- 扩展
-
- 并发
HashMap 碰撞后会发生什么?
我们知道 HashMap#put() 值的时候,当计算出来两个 hash 值一致,就会形成链表。当链表长度大于 8 且 HashMap 容量大于 64 的时候会转成红黑树。你问我咋知道的,代码就这么写的🙃。
模拟 hashCode 值相同
- 首先要知道 HashMap 怎么计算 hash 值的
//如果是 null 返回 0 否则取这个值的hash值,和这个hash值右移16位在按位异或
//异或:两个为0:0;两个为1:0;其余1
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
计算正在存放位置:(容量-1)& hash
那么其实只要我们计算的 hash 值一样,那么下标就一定一样。
那么如何获取这些值,随便写写测试? 那你头大了,都写不出来
因为 ASCII 码值是数字对应字符,我们可以从 ASCII 数字中映射到字符上面去
public static void test(){
Map<Integer, List<String>> map = new HashMap();
for (int i = 33; i < 100; i++) {
char ch= (char) i;
String str = String.valueOf(ch);
int index = 15 & hash(str);
List<String> list = map.get(index);
if (list == null) {
list = new ArrayList<>();
}
list.add(str);
map.put(index,list);
}
map.forEach((k,v)->{
System.out.println(k+" "+v);
});
}
static int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
0 [0, @, P, `]
1 [!, 1, A, Q, a]
2 [", 2, B, R, b]
3 [#, 3, C, S, c]
4 [$, 4, D, T]
5 [%, 5, E, U]
6 [&, 6, F, V]
7 [', 7, G, W]
8 [(, 8, H, X]
9 [), 9, I, Y]
10 [*, :, J, Z]
11 [+, ;, K, []
12 [,, <, L, ]
13 [-, =, M, ]]
14 [., >, N, ^]
15 [/, ?, O, _]
比如我们就拿 key 为 1 的吧。 [!, 1, A, Q, a] 这些作为 hashMap 的可以,然后值不同,可以延申出无数个。那么就可以很轻松的进入链表 + 红黑树的 debug 中。而且这中间还会涉及到扩容。
那么这里基本模拟出来的值,几乎就把 HashMap 中的重要操作能走完:链表化/红黑树化/扩容等
面试题
为啥要写点面试题呢?
答:自己看的时候啥都会,自己做的时候啥也不是。而且题目能让你更有针对性的思考。不然看了,效果不大
HashMap 的 hash 算法
刚上面说了,HashMap 计算 hash 的方法:为什么异或右移16位计算?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
下面看一个案例:
0000 1010 1000 1000 1010 0011 0111 0100 `原数`
0000 0000 0000 0000 0000 1010 1000 1000 `右移16`
0000 1010 1000 1000 1010 1001 1111 1100 `异或结果`
这样能保持高位不变。右移 16 位,能起到混淆的作用【因为高区的16位很容易被直接丢失】。而且 【异或】能保证 0/1 分布均匀,都是 50% 的机会【异或:00:0;11:0;01/10:1】。如果采用 & 计算会向 0 靠拢,采用 | 会向 1 靠拢。
HashMap 的加载因子
默认 0.75;为啥?为了一个平衡吧
可以调整吗?可以的,在初始化的时候可以指定 初始化容量和加载因子
小了:扩容频繁。大了:扩容条件苛刻,hash 碰撞的概率变高,链表更长
HashMap 初始容量
默认 16。
为什么初始化容量是 2 的次方比较好?
2的次方-1 那么会形成 0111 这种数据。在 & 的时候,每个位置都有机会。
初始化不是 2 的次方怎么办?
类似下面的,那么数组中不是 0 就是 16 那么会全部冲突
1101 0011 0010 1110 0110 0100 0010 1011 `原数`
0000 0000 0000 0000 0000 0000 0001 0000 `16的二进制`
0000 0000 0000 0000 0000 0000 0000 0000 `结果`
下面的代码会保证初始化的容量是 2 的次方,如果手动传入不是,那么会帮你初始化大于这个值的最小 2 次方的数。比如 17,会初始化为 32。
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;
}
涉及并发
应该来说,涉及这个面试题的,肯定会问并发。我只能说并发下可能会造成死锁,建议使用
ConcurrentHashMap
总结
- 里面涉及的一些容量/加载因子/初始化等都是经过大量计算的出来的比较平衡的值。并不是我说的这么简单。
- 如果作为延申讲讲:那么我肯定是不够格的,因为学的太少了。
- 现在越来越发现,学的越多,知道的越少。
不要怕,不要急,不要悔,不要死