java基础 --- Integer.bitCount( int i) 理解

682 阅读5分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

1.作用

Integer.bitCount( int i) 这个方法是jdk自带的帮我们快速统计 i转成二进制补码后,其中二进制数中包含1的数量,本文主要基于jdk1.8源码分析。

2.源码

/**
 * Returns the number of one-bits in the two's complement binary
 * representation of the specified {@code int} value.  This function is
 * sometimes referred to as the <i>population count</i>.
 *
 * @param i the value whose bits are to be counted
 * @return the number of one-bits in the two's complement binary
 *     representation of the specified {@code int} value.
 * @since 1.5
 */
 //返回指定的{@code int}值的二进制补码表示形式中的1位数。
public static int bitCount(int i) {
    // HD, Figure 5-2
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}

其实仔细看一下,源码也不长,但是为什么是这样写的 它又是是如何实现的,这里可能就需要我们好好思考一下了

3.思考

  1. 首先为了方便我们更好的理解源码,又因为这个几个十六进制数都是八位的,即它对应的二进制数应该用32位来表示,刚好也对应Integer类型数据的值的范围 所以先把源码里的几个十六进制数,做了一张他们和32位二进制数的对照表,方便我们理解原作者这样写的逻辑
十六进制数二进制数
0x5555555501010101010101010101010101010101
0x3333333300110011001100110011001100110011
0x0f0f0f0f00001111000011110000111100001111

在回过来看代码

i = i - ((i >>> 1) & 0x55555555);

乍一看,不知道他想表达啥意思,我们可以把它稍稍转换一下,i - ((i >>> 1) & 0x55555555) 也就等于 ((i >>> 1) & 0x55555555) + (i & 0x55555555)。这里有必要提一下i - ((i >>> 1) & 0x55555555)的执行效率比((i >>> 1) & 0x55555555) + (i & 0x55555555)快,所以源码采用了这种方式。 如下图:

图片.png 它的核心思想就是把i的二进制补码,按两位一切割,分别统计它们奇数位上一的个数,和偶数位上1的个数(这里每个两位长度的二进制数,它们奇数或偶数位上的1的个数,对应的值只能是1或0),然后把奇数位和偶数位上的1的个数相加,得到这行代码的运算结果,即每个两位长度二进制数上1的个数。

我们可以验证一下: 11 = 01+01 = 10 它包含的1的个数是2即二进制数 10 10 = 00+01 = 01 它包含的1的个数是1 01 = 01+00 = 01 同上 00 = 00+00 = 00 它包含0个1

  1. 我们接着往下看 i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); 这里就和我们上面讲的原理类似,这里就是统计每四位的二进制数上包含的1的个数

图片.png

这里在第一步的基础上又进一步,即求出每四位长度二进制的数中包含1的个数,这个格子里二进制数的值相加,就是i的二进制补码包含的1的个数,好接下来 我们继续跟着源码走

  1. (i + (i >>> 4)) & 0x0f0f0f0f 这里需要注意下:(i + (i >>> 4)) & 0x0f0f0f0f 不等于 (i & 0x0f0f0f0f) + ((i >>> 4)&0x0f0f0f0f) 随便举个例子,当i = 31时 它的二进制补码为 11111,大家可以算一下,它的左边等于0 右边等于16 好了 言归正传,咱们接着来看源码的逻辑

图片.png

注意:虽然这步也是像前面一样,合并格子的值,为什么用(i & 0x0f0f0f0f) + ((i >>> 4)&0x0f0f0f0f)不行,是因为8个二进制数中 包含最多的1也就是8 用四位长度二进制数就可以表示了,如果用这种格式(i & 0x0f0f0f0f) + ((i >>> 4)&0x0f0f0f0f) 统计的数据不准确 如 11111 统计的是16。

为什么前面用的是这种形式:因为 两位长度的二进制数 不足以表示四位长度的二进制数包含的1的数目

  1. i = i + (i >>> 8); 实际上这里是把 原始i的补码 后八位的含有1的数量 加上 后八到后十六位二进制数含有一的数量 统一放到前八位中去保存 , 后面的 十六到三十二位 统一放到16到24位中保存。

  2. i = i + (i >>> 16); 这步和第四步基本一样,把前十六位和后十六位含有一的数量相加。

为什么可以这么做? 因为三十二位int类型补码数据,最多包含32个1 即用后六位二进制数 即可保存下来。

  1. return i & 0x3f 0x3f 对应的二进制是 00111111, 所以这一步就是返回处理后的后六位二进制数据,即初始补码中包含1的数量。

不得不说,java源码开发人员真的太厉害了,本文仅用来记录一下,防止以后自己,看到这个不理解为啥这样写的。同时也欢迎各位小伙伴指正和探讨。