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

Posted 太书红叶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了n |= n >>> 1——JDK10的HashMap原理 tableSizeFor(initialCapacity)方法相关的知识,希望对你有一定的参考价值。

前言

今天我大儿子在我们房子罚款群里问了一个问题,问题如下:
这个问题的答案是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的次方数的最小值。

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

以上是关于n |= n >>> 1——JDK10的HashMap原理 tableSizeFor(initialCapacity)方法的主要内容,如果未能解决你的问题,请参考以下文章

计算阶乘

JDK1.7中的HashMap

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

win10 配置jdk环境变量(重装系统后免安装)

HashMap(jdk1.8)刨根问底java快速提升

Windows10下的JDK环境配置。