前言
之前在看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了,"|"操作就没有什么意义,也不会影响结果,只是多余的操作。