一、引例
今天在阅读 Integer 类的源码的时候,发现了两个比较有意思的方法, 频繁的使用位运算进行左移和右移操作,
分别是 numberOfLeadingZeros 方法和 numberOfTrailingZeros 方法。
二、numberOfLeadingZeros
这个方法从 JDK 1.5 以后开始出现,主要功能是返回无符号整型 i 的最高非零位前面的 0 的个数。
例如:
无符号整数 24 的二进制表示为 0000 0000 0000 0000 0000 0000 0001 1000,第五位的 1 为最高非零位,那么调用这个方法返回的就是该位以前 0 的个数,为 27。
我们先来看一下这个方法的源码:
public static int numberOfLeadingZeros(int i) {
// HD, Figure 5-6
if (i == 0)
return 32;
int n = 1;
if (i >>> 16 == 0) { n += 16; i <<= 16; }
if (i >>> 24 == 0) { n += 8; i <<= 8; }
if (i >>> 28 == 0) { n += 4; i <<= 4; }
if (i >>> 30 == 0) { n += 2; i <<= 2; }
n -= i >>> 31;
return n;
}
一开始对频繁的移位操作以及移动的位数百思不解,查阅了相关资料后恍然大悟,这是二分法的经典应用,这个方法的思路是在寻找最高非零位的时候采用折半查找,因为 int 类型是 32 位的,所以先右移 16 位,判断左半部分是否为 0,如果为 0 的话,说明 1 在右半部分,然后将 i 左移 16 位为下面的操作做铺垫,因为此时已经确定高 16 位为 0 了,所以左移 16 位只是将最高非零位移到高 16 位上。如果第一个条件成立了,那么右移 24 位就相当于将原先的低 16 位右移 8 位来确定最高非零位是在低 8 位还是高 8 位上,后面的操作类似,不再一一推导了。
n 变量用来存放 0 的个数,在每次的操作中,如果条件成立,那么就用 n 变量加上 0 的个数。
最后的 n -= i >>> 31 是对最后两位数的判断,因为在上一行代码中,只要条件成立就将 n 的值加 2,并没有再往下判断剩下的两位 0 的个数,因为 n 的初始值为 1,所以默认剩下的两位中最后 1 位是 1,因此最后还要判断是到底是哪一位为 1,如果 i >>> 31 为 0,那么说明最后一位为 1,就不需要减了,如果不为 0,说明倒数第二位为 1,就需要减去 1。
例如:
无符号整数 24 的二进制表示为 0000 0000 0000 0000 0000 0000 0001 1000,调用此方法的执行流程如下:
- 右移 16 位的值为 0,说明在最高非零位在低 16 位上,n = 17,i 左移 16 位后为 0000 0000 0001 1000 0000 0000 0000 0000
- 右移 24 位的值为 0,说明在最高非零位在低 16 位上的低 8 位上,n = 25,i 左移 8 位后为 0001 1000 0000 0000 0000 0000 0000 0000
- 右移 28 位的值为 1,不为 0,说明在最高非零位在低 16 位的低 8 位的高 4 位上
- 右移 30 位的值为 0,说明在最高非零位在低 16 位的低 8 位的高 4 位的低 2 位上,n = 27,i 左移 2 位后为 0110 0000 0000 0000 0000 0000 0000 0000
- 最后,i 右移 31 位为 0,不需要减 1,最终答案为 27
三、numberOfTrailingZeros
这个方法也是从 JDK 1.5 以后开始出现,主要功能是返回无符号整型 i 的最低非零位后面的 0 的个数。
例如:
无符号整数 24 的二进制表示为 0000 0000 0000 0000 0000 0000 0001 1000,第四位的 1 为最低非零位,那么调用这个方法返回的就是该位以后 0 的个数,为 3。
我们先来看一下这个方法的源码:
public static int numberOfTrailingZeros(int i) {
// HD, Figure 5-14
int y;
if (i == 0) return 32;
int n = 31;
y = i <<16; if (y != 0) { n = n -16; i = y; }
y = i << 8; if (y != 0) { n = n - 8; i = y; }
y = i << 4; if (y != 0) { n = n - 4; i = y; }
y = i << 2; if (y != 0) { n = n - 2; i = y; }
return n - ((i << 1) >>> 31);
}
这两个方法在编码风格上有一点小的出入,但是思想都是一样的,都是采用二分法,推导过程与 numberOfLeadingZeros 类似,就不详细推导了,读者有兴趣可自行推导。