第八周_S- HashMap Debug

38 阅读4分钟

深入源码分析,我目前的想法是:最好是能 debug,对于特殊的情况,能简单/快速的模拟出来。然后再配上一些面试题食用,效果应该不错。

目前想到的关于 HashMap 深入的点:

  • HashMap 本身实现
    • hash 算法
    • 如何 put 值:链表+红黑树
    • 扩容实现
    • 遍历/查找/删除 等实现
  • 扩展
    • 并发

HashMap 碰撞后会发生什么?

我们知道 HashMap#put() 值的时候,当计算出来两个 hash 值一致,就会形成链表。当链表长度大于 8 且 HashMap 容量大于 64 的时候会转成红黑树。你问我咋知道的,代码就这么写的🙃。

模拟 hashCode 值相同

  1. 首先要知道 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

总结

  1. 里面涉及的一些容量/加载因子/初始化等都是经过大量计算的出来的比较平衡的值。并不是我说的这么简单。
  2. 如果作为延申讲讲:那么我肯定是不够格的,因为学的太少了。
  3. 现在越来越发现,学的越多,知道的越少。

不要怕,不要急,不要悔,不要死