Java源码之tableSizeFor和highestOneBit

621 阅读4分钟

前言

之前在看HashMap源码的时候,看到tableSizeFor这个方法,觉得这个只是在初始化的时候进行容量的确定方法,也没有细看(当然主要是因为都是位运算,所以也懒得看)。后来在一次项目中看到有人使用了Integer.highestOneBit方法,看到它的实现,觉得跟tableSizeFor十分相似,所以便决定研究下,所以在此进行阐述写一下自己的心得。

tableSizeFor

    /**
     * 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;
    }

highestOneBit

    /**
     * Returns an {@code int} value with at most a single one-bit, in the
     * position of the highest-order ("leftmost") one-bit in the specified
     * {@code int} value.  Returns zero if the specified value has no
     * one-bits in its two's complement binary representation, that is, if it
     * is equal to zero.
     *
     * @param i the value whose highest one bit is to be computed
     * @return an {@code int} value with a single one-bit, in the position
     *     of the highest-order one-bit in the specified value, or zero if
     *     the specified value is itself equal to zero.
     * @since 1.5
     */
    public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }

从这里可以看出,这两段的代码皆有相似之处,那么为什么要这么做呢?

演示过程

观察可得,核心且相同的一段代码为:

        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);

这里的场景当中,我们其实不会用到负数,所以这里的>>和>>>可以看做是等价(这两者的区别应该不用再说了吧),OK,我们举例i=100,由于是位运算,所以我这边将100用二进制进行表示:

0000,0000,0000,0000,0000,0000,0110,0100

那么处理过程如下:

可以看到在这几次运算之后,我们可以巧妙地返回一个2^*-1的数字。
那么接下来我们来分析一下具体的内容。

highestOneBit

从这个方法上面的注释我们可以知道,这个方法是用来返回一个不大于指定值且最大的一个2的幂次的数。经过上面的转换之后,又进行了一次操作:

也就是说,上面的操作,其实返回的是大于指定值且为最小的一个2的幂次的数-1,那么只需要进行右移然后进行作差,即可得到一个不大于指定值且最大的一个2的幂次的数。

tableSizeFor

这个是HashMap当中用来确定容量的一个方法。当我们对HashMap指定容量的时候,HashMap触发扩容阈值并非是我们指定的容量,而是取tableSizeFor(initialCapacity)返回的值。这个方法返回的是大于指定值且最小的2的幂次的数。

    /**
     * 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); <----------在这边指定触发的阈值
    }

我们可以知道,相同的那段代码返回的是一个大于指定值且最小的2的幂次-1,为了符合这个结果,只需要在最后+1即可。(当然,那些三目运算符我就不做过多解释,^_^)。
看到这里,各位读者肯定会有一个疑问,这个方法为什么在一开始要进行-1的操作呢?好像可以自己修改一些代码,输入100,发现无论-1还是不-1,得到的结果都是一样的。
这里就要考虑到一些临界值,那就是输入的数据是2的幂次。当数据是2的幂次的时候,tableSizeFor返回的结果就不一样了,例如输入值为4,那么如果-1,则返回4;不-1,则返回8。这个可以通过上述同样的演示流程可得。

反思

①为什么这里要进行1、2、4、8、16这样的运算呢?
对于局部来说,其实就是为了把高位移到低位(对于4位来说,前两位是高位,后两位是低位)这样之后再进行"|"操作,那么就可以将局部得到全1。
②为什么这里只是到16就结束了呢?
因为我们这里针对的数值都是int类型,在Java当中int类型占到4个字节,也就是32位。这也是我在演示中使用32位的原因。为什么不进行32位右移呢,这是因为32位右移之后就变成全0了,"|"操作就没有什么意义,也不会影响结果,只是多余的操作。