关注人瑞IT内推圈
回复“大厂”领取整理好的面试资料
1、HashMap 在 JAVA 中的怎么工作的?
基于 Hash 的原理。
2、什么是哈希?
最简单形式的 hash,是一种在对任何变量 / 对象的属性应用任何公式 / 算法后, 为其分配唯一代码的方法。
一个真正的 hash 方法必须遵循下面的原则:
“
哈希函数每次在相同或相等的对象上应用哈希函数时, 应每次返回相同的哈希码。换句话说, 两个相等的对象必须一致地生成相同的哈希码。
Java 中所有的对象都有 Hash 方法,Java 中的所有对象都继承 Object
类中定义的 hashCode()
函数的默认实现。此函数通常通过将对象的内部地址转换为整数来生成哈希码,从而为所有不同的对象生成不同的哈希码。
3、HashMap 中的 Node 类
Map 的定义是:将键映射到值的对象。
因此,HashMap
中必须有一些机制来存储这个键值对。答案是肯定的。HashMap
有一个内部类 Node
,如下所示:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 记录hash值, 以便重hash时不需要再重新计算
final K key;
V value;
Node<K,V> next;
...// 其余的代码
}
当然,Node
类具有存储为属性的键和值的映射。
key 已被标记为 final,另外还有两个字段:next 和 hash。
在下面中, 我们将会理解这些属性的必须性。
4 、键值对在 HashMap 中是如何存储的
键值对在 HashMap
中是以 Node
内部类的数组存放的, 如下所示:
transient Node<K,V>[] table;
哈希码计算出来之后, 会转换成该数组的下标, 在该下标中存储对应哈希码的键值对, 在此先不详细讲解 hash 碰撞的情况。
该数组的长度始终是 2 的次幂, 通过以下的函数实现该过程
static final int tableSizeFor(int cap) {
int n = cap - 1;// 如果不做该操作, 则如传入的 cap 是 2 的整数幂, 则返回值是预想的 2 倍
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;
}
其原理是将传入参数 (cap) 的低二进制全部变为 1, 最后加 1 即可获得对应的大于 cap 的 2 的次幂作为数组长度。
在此有涉及到 HashMap
的 hash 函数及数组下标的计算, 键 (key) 所计算出来的哈希码有可能是大于数组的容量的, 那怎么办?
可以通过简单的求余运算来获得, 但此方法效率太低。HashMap
中通过以下的方法保证 hash 的值计算后都小于数组的容量。
(n - 1) & hash
这也正好解释了为什么需要 2 的次幂作为数组的容量。由于 n 是 2 的次幂, 因此, n - 1 类似于一个低位掩码。
通过与操作, 高位的 hash 值全部归零,保证低位才有效, 从而保证获得的值都小于 n。同时, 在下一次 resize() 操作时, 重新计算每个 Node 的数组下标将会因此变得很简单, 具体的后文讲解。
以默认的初始值 16 为例:
01010011 00100101 01010100 00100101
& 00000000 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000000 00000101 //高位全部归零,只保留末四位
// 保证了计算出的值小于数组的长度 n
但是, 使用了该功能之后, 由于只取了低位, 因此 hash 碰撞会也会相应的变得很严重。这时候就需要使用 「扰动函数」
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该函数通过将哈希码的高 16 位的右移后与原哈希码进行异或而得到, 以以上的例子为例
异或
此方法保证了高 16 位不变, 低 16 位根据异或后的结果改变。计算后的数组下标将会从原先的 5 变为 0。
使用了 「扰动函数」 之后, hash 碰撞的概率将会下降。有人专门做过类似的测试, 虽然使用该 「扰动函数」 并没有获得最大概率的避免 hash 碰撞, 但考虑其计算性能和碰撞的概率, JDK 中使用了该方法, 且只 hash 一次。
5、哈希碰撞及其处理
在理想的情况下, 哈希函数将每一个 key 都映射到一个唯一的 bucket, 然而, 这是不可能的。哪怕是设计在良好的哈希函数, 也会产生哈希冲突。
前人研究了很多哈希冲