HashMap源码解析(一)

本文详细介绍了HashMap在JDK1.8中的源码实现,包括核心成员变量、构造函数(无参、指定容量和加载因子、指定Map)、hash函数的设计以及扩容策略。重点讲解了如何通过hash函数减少哈希冲突,确保高效性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

大家好, 我是徐徐!
今天给大家带来的是HashMap的源码分享, 话不多说, 下面我们直切正题.

源码环境

JDK1.8

核心成员变量

HashMap的核心成员变量如图1所示:
图1 - HashMap的核心成员变量table
哈希表, 在真正的被使用时才会初始化, 其长度始终是2的整数次幂.
entrySet
缓存entrySet()方法的调用结果.
size
HashMap中包含的元素(key-value对)数量.
modCount
用于记录HashMap结构被修改(key-value数量的修改, 以及HashMap内部结构的修改, 如rehash操作)的次数.
threshold
扩容阈值, 它的取值可能如下:

  • 容量 * 加载因子: 绝大多数情况下是这个值;
  • 容量本身: 如果table还没有初始化, 那么此时该值就是table将要初始化的容量的大小;
  • 0: 使用默认构造函数构造时.

loadFactor
加载因子衡量的是哈希表在容量自动增加之前允许达到的装满元素的程度.
简单的说就是, 当你向HashMap中put元素时, 当其元素的个数达到其最大容量 * 加载因子时, HashMap将进行自动扩容

构造函数

HashMap一共提供了4个构造函数, 如图2所示, 接下来我们逐个分析
图2 - HashMap的构造函数

无参构造函数

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

无参构造函数的代码非常简单, 就做了一件事, 为加载因子赋了一个默认值0.75F.

指定容量和加载因子的构造函数

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

这段代码, 做的事情也相对简单, 首先是校验参数initialCapacityloadFactor, 校验通过之后, 为成员变量赋值, 然后根据initialCapacity计算threshold.
下面讲解一下tableSizeFor()这个方法
这个方法要实现的功能如下:

输入一个整数, 返回距离其最近的2的整数次幂的整数, 如:
输入7, 返回8
输入8, 返回8

static final int tableSizeFor(int cap) {
	// n = cap - 1 是因为如果cap本来就是2的次幂, 那么向上取2的次幂后, 
	// 得到的是当前值的2倍, 而实际上可以直接使用cap本身作为table的初始化容量.
    // 比如 cap = 8 如果不减一, 经过下面的操作后会得到16.
    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;
}

下面用一个示例演示一下上述这段代码是如何实现上述功能的.

假设n的二进制表示为(*表示0或1):
00000000 00001*** ******** ********

n右移一位后的值为:
00000000 000001** ******** ********

n |= n >>> 1 的值为:
00000000 00001*** ******** ********
|
00000000 000001** ******** ********
-----------------------------------
00000000 000011** ******** ******** // 此时n的高位有2个1

n再右移2位后的值为:
00000000 00000011 ******** ********

n |= n >>> 2 的值为:
00000000 000011** ******** ********
|
00000000 00000011 ******** ********
-----------------------------------
00000000 00001111 ******** ******** // 此时n的高位有4个1

... 以此类推, 最后n的值为:
00000000 00001111 11111111 11111111

此时 n + 1 就为2的整次幂了
00000000 00010000 00000000 00000000

指定容量的构造函数

public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

这段代码没有什么好说的, 直接调用上述指定容量和加载因子的构造函数, 并且使用默认的加载因子0.75F

指定Map的构造函数

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 将指定m中的元素, 加入到当前HashMap中
    putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            // 根据m的元素个数和当前的加载因子计算当前HashMap的容量
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                // table还没有, 同时当前容量大于扩容阈值, 这里为扩容阈值赋值为table的容量
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        // 遍历m中的元素, 加入到当前HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

上述代码的核心逻辑就是遍历指定的Map, 然后将其中的元素加入到当前的HashMap中来.

核心方法

hash函数

hash()函数的声明如下, 以下我们称hash()函数的返回值为hash值

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

这个方法代码看着简单, 但却很有深意. 深意主要体现在(h = key.hashCode()) ^ (h >>> 16), 为什么要这么干呢, 直接用key的hashCode不行吗?
简单的来说, 这么做的原因是为了减少hash冲突.
那么这么做怎么就能够减少hash冲突呢?
我们知道, table的容量是2的整次幂, 在加入key-value时, 计算元素所在的hash桶的方式为: key的哈希值 & (table容量 - 1), 下面我们提供一个示例来演示这个计算过程:

# 假设table的容量如下
00000000 00010000 00000000 00000000
# 则容量减一为 
00000000 00001111 11111111 11111111

# 假设某key的hash值如下(*表示0或者1)
******** ******** ******** ********

# 则其与table的容量-1进行按位与的结果为
******** ******** ******** ********
&
00000000 00001111 11111111 11111111
-----------------------------------
00000000 0000**** ******** ********

可以看到, 最后的计算结果高位全是0, 也就是说, 根据这种计算方式, hash值的高位被屏蔽掉了. 为了让高位也能参与运算, 所以才有了(h = key.hashCode()) ^ (h >>> 16), 我们同样提供一个示例来演示这个计算过程:

# 假设 h = key.hashCode() 的值如下(h表示原始数据的高位, l表示原始数据的低位)
hhhhhhhh hhhhhhhh llllllll llllllll

# 则 h >>> 16 为
00000000 00000000 hhhhhhhh hhhhhhhh

# 最终 (h = key.hashCode()) ^ (h >>> 16)为
hhhhhhhh hhhhhhhh llllllll llllllll
^
00000000 00000000 hhhhhhhh hhhhhhhh
-----------------------------------
hhhhhhhh hhhhhhhh ******** ********

通过上述示例可以看到, 最后得到的结果的低16位, 是原始数据的低16位与其高16位进行异或的结果.


好了, 今天的分享就到此结束了~
如这个篇文章对你有帮助, 记得点赞关注哟~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值