n |= n >>> 1——JDK10的HashMap原理 tableSizeFor(initialCapacity)方法

本文深入探讨了HashMap在Java中如何确定其容量和阈值的计算原理。通过源码解析,详细解释了tableSizeFor方法如何确保容量为2的幂次方,以及在扩容时threshold值的确定方式。

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

前言

今天我大儿子在我们房子罚款群里问了一个问题,问题如下:
在这里插入图片描述这个问题的答案是16,hashmap具体的知识,自行百度,知道了hashmap的大概知识之后再看回来我下面说的。

为什么是16呢,因为在hashmap在扩容的时候会对threshold进行判断,size大于等于threshold就扩容。这个threshold就是2的次方数,2的次方数就是2的几次方的意思。threshold取得值,是你设定大小的下一个2的次方数。10的下一个2的次方数是16,所以答案是16。

这个答案是通过阅读源码得到的。是对的,不信你往下看。

源码在哪里?我们找一下。
这是map的put方法所调用的putval:我们只看最后的部分就好。

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

最后的关键部分:

if (++size > threshold)
            resize();

resize就是扩容方法。
那么threshold从哪里来?狗子方法中(打错了,应该是构造方法,哈哈哈,不过这样挺有意思,哈哈哈哈哈哈)最后一行进行了赋值:

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

构造方法中最后一行就是:
this.threshold = tableSizeFor(initialCapacity);

那么这个this.threshold = tableSizeFor(initialCapacity);是怎么回事呢?我们看他的源码:

/**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

这就是我们要找的部分了。

看到了这些代码,我一下就回想起了大学时,教我们逻辑电路那个老师了,什么短路与,运算或,与或非,异或,左移右移,这堆烂码七糟的东西。

我忘了这老师叫啥了,名字好像是仄仄平或者平仄平。

他的形象一直能想起来,中年,短粗胖,喝酒。哇啦哇啦的。

后来问了同学,知道了,他叫仄平仄。。。和我记得完全不一样。

那这段代码是什么意思呢,看到的时候我很懵,后来仔细看,照着算一下,也就明白了。

关键代码的问题解析

|

首先,什么是|。就是二进制运算时,有1就是1。
1|0=1,1|1=1,0|0=0。

>>>

右移几位。直接往右移就完了,0补位,很简单。
111>>>1=011。
111>>>2=001。

开始计算

这段代码到底咋回事?

		int n = cap - 1;
		n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;

他的意义就是找到比输入大的,并且与输入相邻的2的次方数。
比如输入3,输出4。输入5、6、7,输出8。

我们只要跟着算一下就会明白了。下面开始计算。以输入10为例。你也可以随便选择一个数,按照下面步骤进行。

比如输入10,10-1=9,9化成二进制就是:1001。
然后开始这步:

n |= n >>> 1;

此时n=1001,n先右移1位,变成0100。
然后0100再与之前的n做或运算,也就是0100|1001=1101。
此时n=1101。

然后是:

n |= n >>> 2;

1101>>>2=0011
0011|1101=1111
此时n=1111。

然后是:

 n |= n >>> 4;

聪明的人已经知道了,以后的结果都是1111,不信给你算。
1111>>>4=0000
0000|1111=1111
此时n=1111。

然后是

 n |= n >>> 8;

1111>>>8=0000 0000
0000 0000|1111也就是0000 0000|0000 1111 = 0000 1111
0000 1111也就是1111。
此时n=1111。

移16就不用算了,你得多笨。

这时候发现了啊,一直右移,做或运算,最后就变成了全是1的。然后再看tableSizeFor的代码最后一行,最后要得到n+1。

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

这段代码先是判断n是否小于0,这个防御判断就不用管了。大于0的时候,判断n和MAXIMUM_CAPACITY的大小,取值MAXIMUM_CAPACITY或者n+1。

这里可以这么理解:
n >= MAXIMUM_CAPACITY 可以写成n + 1 > MAXIMUM_CAPACITY
也就是:
(n + 1 > MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
然后就理解成,n+1和MAXIMUM_CAPACITY谁大就取谁。

我们输入的10,最后得出1111,1111+1之后是几?是10000,16,也就是2的4次方。

几个问题:

为什么取的是n+1呢?计算的结果都是1的,我们加了1之后就会进一位并且变成1000…000。变成2的次方数。

我们是怎么保证输出一定大于等于输入的呢?
1)我们输入的数,转为二进制之后,最大的情况也不过是,11111…111这样的,如果是这样,那么输出=输入。
2)如果我们的输入不是1…11111…111这种情况,也就是说,此时不是最大值,此时 必有的位为0,经过计算之后,位0的位变为1,必比变换之前大。

那有人说了,为什么这里我输入的数字,是位数很多的呢?位数多到,上述的最后一步>>>16都不能包括我所有的位数。那不是上述操作移完了之后还会有0存在吗,也就是得出的数不是2的次方数了。

不好意思,输入类型是int型。int型最大值是2的31次方。

而上述计算中。>>>1相当于除以2的1次方。
然后>>>2是除以2的2次方。
然后>>>4——2的4次方。
然后>>>8——2的8次方。加起来是15次方。
最后>>>16——2的16次方,加起来就是一共运算了2的31次方的数。所以你输入的int型,都可以移动的到。

你的担心不存在啊。

结论

多算几个数我们会发现规律:
设输入x,x的二进制数共有k位。
经计算之后,得出数y,y共有k+1位。
并且:y为大于x的,2的次方数的最小值。

我乍看有所疑惑的这段代码,原来就是这么回事。

寻找最近的2次幂((代码请基于jdk8’联网解析每一步) (请详细解析相关概念和描述,联网解析,中文解析)) 在定义elements变量时说,其长度总是2的次幂,但用户传入的参数并不一定符合规则,所以就需要根据用户的输入,找到比它大的最近的2次幂。比如用户输入13,就把它调整为16,输入31,就调整为32,等等。考虑下,我们有什么方法可以实现呢? 来看下ArrayDeque是怎么做的吧: private void allocateElements(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. if (numElements &gt;= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity &gt;&gt;&gt; 1); initialCapacity |= (initialCapacity &gt;&gt;&gt; 2); initialCapacity |= (initialCapacity &gt;&gt;&gt; 4); initialCapacity |= (initialCapacity &gt;&gt;&gt; 8); initialCapacity |= (initialCapacity &gt;&gt;&gt; 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity &gt;&gt;&gt;= 1;// Good luck allocating 2 ^ 30 elements } elements = new Object[initialCapacity]; } 看到这段迷之代码了吗?在HashMap中也有一段类似的实现。但要读懂它,我们需要先掌握以下几个概念: 在java中,int的长度是32位,有符号int可以表示的值范围是 (-2)31 到 231-1,其中最高位是符号位,0表示正数,1表示负数。 &gt;&gt;&gt;:无符号右移,忽略符号位,空位都以0补齐。 |:位或运算,按位进行或操作,逢11。 我们知道,计算机存储任何数据都是采用二进制形式,所以一个int值为80的数在内存中可能是这样的: 0000 0000 0000 0000 0000 0000 0101 0000 比80大的最近的2次幂是128,其值是这样的: 0000 0000 0000 0000 0000 0000 1000 0000 我们多找几组数据就可以发现规律: 每个2的次幂用二进制表示时,只有一位为 1,其余位均为 0(不包含符合位) 要找到比一个数大的2的次幂(在正数范围内),只需要将其最高位左移一位(从左往右第一个 1 出现的位置),其余位置 0 即可。 但从实践上讲,没有可行的方法能够进行以上操作,即使通过&操作符可以将某一位置 0 或置 1,也无法确认最高位出现的位置,也就是基于最高位进行操作不可行。 但还有一个很整齐的数字可以被我们利用,那就是 2n-1,我们看下128-1=127的表示形式: 0000 0000 0000 0000 0000 0000 0111 1111 把它和80对比一下: 0000 0000 0000 0000 0000 0000 0101 0000 //80 0000 0000 0000 0000 0000 0000 0111 1111 //127 可以发现,我们只要把80从最高位起每一位全置为1,就可以得到离它最近且比它大的 2n-1,最后再执行一次+1操作即可。具体操作步骤为(为了演示,这里使用了很大的数字): 原值: 0011 0000 0000 0000 0000 0000 0000 0010 无符号右移1位 0001 1000 0000 0000 0000 0000 0000 0001 与原值|操作: 0011 1000 0000 0000 0000 0000 0000 0011 可以看到最高2位都是1了,也仅能保证前两位为1,这时就可以直接移动两位 无符号右移2位 0000 1110 0000 0000 0000 0000 000
最新发布
04-01
<think&gt;好的,我现在需要帮助用户理解JDK8中ArrayDeque的allocateElements方法是如何使用位运算来计算最近的2次幂的。首先,我得回忆一下这个方法的实现原理。 用户提到的allocateElements方法,应该是用来确定ArrayDeque初始容量的,确保容量是2的幂次。这样设计的原因可能与哈希表类似,方便使用位运算进行索引计算,提高效率。比如HashMap的容量也是2的幂次,这样可以方便地通过(n-1)&hash来计算索引。 接下来,我需要回忆具体的代码实现。根据JDK源码,allocateElements方法可能包含一些位运算,比如位或操作和右移操作。例如,当给定一个初始容量numElements时,方法需要找到不小于这个数的最小的2的幂次。这个算法通常涉及将最高位的1之后的所有位都填充为1,然后加1,这样就能得到最近的2次幂。 比如,假设输入是17,二进制是10001。通过位运算将其转换为11111,再加1得到100000,即32。这样确实得到了比17大的最近的2次幂。 接下来,我需要详细分解这个过程的每一步。首先,将数减1,这样在处理已经是2次幂的数时不会出错。例如,如果输入是16(10000),减1后是15(01111),然后进行位或操作,得到15,再加1还是16。这样就保持原数不变,因为它已经是2的幂次。 然后,逐次右移并执行位或操作,确保所有低位都被填充为1。比如,对于初始数n,先右移1位,进行位或,这样最高位的1右边的一位也被置为1。接着右移2位、4位、8位、16位(对于32位整数来说),直到所有可能的低位都被覆盖。这样处理后,n的值会变成全1的二进制数,加1后自然就是下一个2的幂次。 需要注意的是,这个算法在JDK中的实现可能有一些边界情况的处理,比如当初始容量已经是2的幂次时,是否需要调整。此外,还要考虑最大容量的限制,比如不能超过2^30这样的限制,避免溢出。 然后,关于用户提到的位运算符号,比如|(按位或)和&gt;&gt;&gt;(无符号右移),需要解释它们在算法中的作用。例如,无符号右移确保在移位时高位补0,而不是补符号位,这对于处理正整数是必要的。 还需要引用相关的资料,比如用户提供的引用[^2]和[^4],其中提到HashMap的实现和位运算的应用。可能可以关联到类似的数据结构设计思路,比如为什么需要容量为2的幂次,以及如何高效计算。 最后,生成相关问题,帮助用户进一步理解位运算在其他场景中的应用,或者ArrayDeque的其他实现细节,比如扩容机制、性能优化等。 总结来说,我需要分步骤解释allocateElements方法中的位运算过程,结合具体例子,并引用相关资料来支持解释,确保用户能够理解其原理和实现步骤。</think&gt;### ArrayDeque allocateElements方法位运算原理解析 JDK8中`ArrayDeque.allocateElements`方法通过位运算高效计算大于等于给定数的最小2次幂,这是基于**二次幂对齐**原则的经典实现。以下分步骤解析其原理: #### 一、核心算法步骤(以numElements=17为例) 1. **初始减1**:`initialCapacity = numElements - 1` ```text 17-1=16 → 二进制 10000 ``` *作用:处理输入值本身是2次幂的情况* 2. **无符号右移+按位或操作**: ```java initialCapacity |= (initialCapacity &gt;&gt;&gt; 1); // 右移1initialCapacity |= (initialCapacity &gt;&gt;&gt; 2); // 右移2位 initialCapacity |= (initialCapacity &gt;&gt;&gt; 4); // 右移4位 initialCapacity |= (initialCapacity &gt;&gt;&gt; 8); // 右移8位 initialCapacity |= (initialCapacity &gt;&gt;&gt; 16); // 右移16位 ``` - 逐步将所有低位填充为1: ```text Step1: 10000 | 01000 = 11000 Step2: 11000 | 00110 = 11110 Step3: 11110 | 00001 = 11111 ...后续步骤数值不再变化 ``` 3. **最终加1对齐**: ```text 11111 + 1 = 100000 → 十进制32 ``` #### 二、关键位运算解析 1. **`&gt;&gt;&gt;`无符号右移** - 特点:高位补0,不保留符号位 - 示例:`8 &gt;&gt;&gt; 1 = 4`(二进制`1000 → 0100`) 2. **`|`按位或** - 作用:合并高位1到低位,确保数值二进制形式中最高位后的所有位变为1 #### 三、JDK8源码实现(简化版) ```java private void allocateElements(int numElements) { int initialCapacity = numElements; initialCapacity |= (initialCapacity &gt;&gt;&gt; 1); initialCapacity |= (initialCapacity &gt;&gt;&gt; 2); initialCapacity |= (initialCapacity &gt;&gt;&gt; 4); initialCapacity |= (initialCapacity &gt;&gt;&gt; 8); initialCapacity |= (initialCapacity &gt;&gt;&gt; 16); initialCapacity++; if (initialCapacity < 0) // 超过int最大值时回退 initialCapacity &gt;&gt;&gt;= 1; // 最终设置数组容量为initialCapacity } ``` #### 四、设计意义 1. **性能优势**:位运算时间复杂度为$O(1)$,远快于循环判断 2. **内存对齐**:保证数组容量为2次幂,便于通过`(length-1) & index`快速计算索引 3. **避免哈希冲突**:类似HashMap中数组长度取2次幂的设计思想[^1] #### 五、边界条件处理 - **输入为0时**:返回最小容量1($2^0$) - **超过int最大值**:回退到$2^{30}$(`initialCapacity &gt;&gt;&gt;= 1`)
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值