HashMap 是如何工作的?图文详解,一起来看看!

关注人瑞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 的次幂作为数组长度。

为什么要使用 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, 然而, 这是不可能的。哪怕是设计在良好的哈希函数, 也会产生哈希冲突。

前人研究了很多哈希冲

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值