JDK1.8 HashMap

Posted mougg

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK1.8 HashMap相关的知识,希望对你有一定的参考价值。

背景

HashMap 是集合框架中最重要的类之一:

  • LinkedHashMap 直接继承 HashMap
  • ConcurrentHashMap 的实现就是 HashMap + 分段锁
  • HashSet 底层是 HashMap
  • TreeMap 的红黑树在 HashMap 也有实现
    JDK1.8 java.util.HashMap#table 注释说 "length is always a power of two",为什么?

哈希冲突(Hash Collision)

哈希表解决冲突的方式有两种:

  1. 开放地址法(open addressing),一个萝卜一个坑,没坑了就扩容。
  2. 链地址法(chaining)
    Java 中的实现是链地址法,底层是 数组、链表、红黑树。这个数组其实就是算法常用的 hash table,定义如下:
transient Node<K,V>[] table;

这个 Node 就是链表,在长度超过8的时候会转换为 TreeNode。
注意下,Node 会在第一次计算元素 hash 值后,把 hash 记录下来,扩容时可以复用。

是 mask,不是 mod

在看 java.util.HashMap#put 源码之前,我以为会用 mod 计算元素在 table 中的索引:

i = hash % n
// hash:通过 hash() 计算出来的哈希值
// n:table.length

然而实际情况是 i = (n-1) & hash,网上很多说法这是用二进制实现的高性能取余。究竟是不是,试一下就知道:

int[] ns = new int[]{16, 15};
for (int n : ns) {
    System.out.println("----" + n + "----");
    System.out.println(100 % n);
    System.out.println(100 & n - 1);
}

技术图片
结论很明显,这不是一般的取余,而是当 n 为 power of 2 恰好表现为取余。
联想一下子网掩码的工作原理,答案就出来了:这是把 n - 1 作为一个 mask 来计算索引。
为什么恰好是 n - 1 呢,可以看一个案例:
技术图片
(a) 是 resize 之前的情况,key1 的索引是 0101,key2 的坐标是 0101,很好,这俩属于同一个 bucket;
(b) 是 resize 之后的情况,由于 "length is always a power of two"n - 1 扩容后只需要简单左移1位,这会儿 mask 一下你会发现,扩容后 key1 的新坐标是 0101,key2 的新坐标是 1101,它们只是新增了一个最高位。
上文我们提到,Node 会把 hash 记录下来,现在扩容,只需看看hash值中对应 mask 最高位:0则索引不变,1则 新索引 = 原索引 + oldCap

java.util.HashMap#hash 也很讲究

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

Java 的 hashCode 是一个int类型,占用4字节,即32位,分为高16位,低16位。
计算索引的公式是 (n - 1) & hash,当 table 还没扩容时,n - 1 很小,& 只能对低位有效果。
为了保证 hashcode 的高低位都能参与到索引计算中,hash() 将 hashCode 高16位 和 低16位 做异或运算,同时控制了开销。

结论

  1. (n - 1) & hash 不是为了高性能取余,而是对 hash 做 mask 计算索引
  2. resize 不需要 rehash,所以性能上来了

Reference

[1]Java8系列之重新认识HashMap
[2]散列表-哈希冲突

以上是关于JDK1.8 HashMap的主要内容,如果未能解决你的问题,请参考以下文章

Java中HashMap底层实现原理(JDK1.8)源码分析

源码解析JDK1.8-HashMap链表成环的问题解决方案

JDK1.8源码解析-HashMap

JDK1.8源码之HashMap(上)

JDK1.8源码之HashMap(上)

jdk1.8 HashMap底层数据结构:深入解析为什么jdk1.8 HashMap的容量一定要是2的n次幂