阅读源码前的必须知道的事之位运算

353 阅读8分钟

7anIejYMiK3Qbvu

当下的努力和收获可能不成正比,但最后一定会成正比

不知道你们是否会有这种时候,在阅读源码的时候,遇到&、^、|这些符号,然后可能会去查一下,也有的可能就直接一笔带过,觉得并不影响。但有时候,知道这种运算的方式以及运算之后的结果更有利于去看懂源码,知道这样做的意图,才会发现原来是这样。那么话不多说,介绍一下这三种运算。

1.与运算(&)

说到运算之前呢,我们先来说说二进制,以及二进制的转换。那什么是二进制,有请百度百科。

JrHwWhejE2NOFmI

其实,简单来说,就是0和1的组合。比如:十进制的3,在二进制表现为:0011。那么十进制和二进制到底是如何转化的呢?我来告诉你

二进制转十进制

给定一个数,0101 1101这个二进制数从右到左的下标依次为0-7,那么我们就知道了 0101 1101 = 1 x 20 + 0 x 21 + 1 x 22 + 1 x 23 + ... =

1 x 1 + 0 x 2 + 1 x 4 + 1 x 8 + 1 x 16 + 0 x 32 + 1 x64 + 0 x128 = 1 + 0 + 4 + 8 + 16 + 0 + 64 + 0 = 93。这里我们标粗的表示从右到左的每一个数,然后各自乘以2的0-7次方的和,最终得到我们最终的结果。

十进制转二进制

同样,我们对十进制的93进行二进制的转换,如下:

93 / 2 = 46 -----------余 1

46 / 2 = 23 -----------余 0

23 / 2 = 11 -----------余 1

11 / 2 = 5 -----------余 1

5 / 2 = 2 -----------余 1

2 / 2 = 1 -----------余 0

1 / 2 = 0 -----------余 1

然后再从下往上组合,得到结果为1011101,这时候我们会发现,怎么只有7位,原来的可是8位,其实这就是我们得结果,只不过高位没有补0,省略掉了而已,我们往高位补0后得到0101 1101,是不是就一样了呢。

弄清楚二进制之后,那可以进行接下来的与运算了。

规则:相同运算位同时为1,结果才为1,其他都为0;0 & 0 = 0,0 & 1 = 0,1 & 0 = 0,1 & 1 = 1

        3		0011
        &
        5		0101
        R		0001	

可得,3 & 5 的结果为 1

2.或运算

规则:相同运算位只要一个为1,结果就为1,0 | 0 = 0,1 | 0 = 1,0 |1 = 1, 1|1 = 1

        3		0011
        |		
        5		0101
        R		0111

可得,3 | 5的结果为7

3.异或运算

规则:相同运算位不同,则结果为1,否则为0, 0 ^ 0 = 0,1 ^ 0 = 1,0 ^ 1 = 1,1 ^ 1 = 0

        3		0011
        ^		
        5		0101
        R		0110

可得,3 ^ 5的结果为6

说完这些,我们再来看看接下来更重要的东西,左移(<<)右移(>>)无符号右移(>>>),因为这些在源码中的应用更广泛,后面我们将结合Hashmap源码中的来具体解析。

4.右移

右移又分为逻辑右移算术右移逻辑右移顾名思义就是在你的脑袋瓜里按照思维逻辑整体把二进制数往右边移动,左边补上0即可,比如11000101逻辑右移1位则结果为01100010。如下图:

9znGj1blDNCotEc

红色的为右移而丢弃的1位,蓝色为补位。

算术右移呢,如果最高位为1,则前面补1,否则的话前面就补0。比如1000 1000 算术右移两位,则表示为1110 0010,而如果是逻辑右移呢,则表示为0010 0010。

综上呢,可以看出:

无符号数逻辑右移n位,在算术上表达为:该数除以2的n次方

有符号数算术右移n位,在算术上表达为:该数除以2的n次方

于是你按着结论算了下1000 1000算术右移两位后的结果:-8 除以-98怎么也不等于4啊,心想:我套你🐒,你搁这乱扯呢,你看我干不干你就完了。

其实并没有错,只是计算的方式错了,因为在计算机中,负数是以其正值的补码的形式存在的。那么这里又涉及到了原码反码补码这三个概念了。

**原码:**正数的原码,就是他的二进制数;负数的原码就是其绝对值的二进制数,然后最高位置为1

**反码:**正数的反码就是其原码;负数的反码就是对其除了符号位之外的位按位取反

**补码:**正数的补码就是其原码;负数的补码就是反码 +1

这么一说你可能还有点抽象,那么在比如一个int类型的数8,4字节32位所以在计算机中表示为:

00000000 00000000 00000000 00001000

那么来看看它的原码,按上面所说表示为:

00000000 00000000 00000000 00001000

那么-8的原码呢,按照规则最高位置为1:

10000000 00000000 00000000 00001000

-8的反码:

11111111 11111111 11111111 11111000

-8的补码(也就是我们计算机中存的负数的二进制表达形式):

11111111 11111111 11111111 11111001

右移两位:

11111111 11111111 11111111 11111110

取反:

10000000 00000000 00000000 00000001

加1得到右移两位后的原码:

10000000 00000000 00000000 00000010

现在我们再来计算-8算术右移两位后的结果为-2,那么-8除以-2等于2^2^;从而得出我们的结论并没有错✌️。

5.左移

左移就比较简单了,高位全部丢弃,低位全部补0。左移在算术上表达为:左移n位,等于乘以2的n次方,在数据不溢出前提下。

比如:8 << 2 = 1000 << 2 = 0010 0000 = 32 = 8 * 2^2^

6.应用

那么说了这么多,这些东西还没派上用场呢,话不多说,我们可以看一下hashmap中的一个tableSizeFor方法:

		/**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
		/**
     * 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;
    }

看过源码或者博客的应该都知道,这个方法是为了返回一个离自己最近的的2的n次方的数。比如输入的13,返回的就是2^4^ = 16,输入的20返回的就是2^5^ = 32。

我们先不管他这个方法的设计。我们先走一遍流程,看看是不是我们说的这样。

第一步:输入13

第二步:13 -1 = 12

第三步:12和12无符号右移1位进行或运算

	1100
  | 
    0110
  R 1110				n结果为14

第四步:14和14无符号右移2位进行或运算

	1110
  | 
    0011
  R 1111				n结果为15

其实到这后面都不要算了,因为或运算中1或上任何数都是1,

所以执行最后一步:

15大于0并小于MAXIMUM_CAPACITY,所以return 15 +1 = 16。

那么为什么这么移位和运算呢?我们以我们输入的数的二进制从左到右出现的第一个1设置为最高位,通过这样移位运算,我们可以使得高位以下所有的位全部变成1,经过+1操作之后就会变成我们所需的离自己最近的的2的n次方的数。那么有的也许还会问了,为什么要执行右移1次,2次,4次,一直到16次这么多,我们可以看一下hashmap中声明的MAXIMUM_CAPACITY,再把1 + 2 + 4 + 8 + 16算出来等于31。我们知道int类型的范围在-2^31^ ~ 2^31^ - 1。因此int的最大2次幂为30,而代码中移动了31位,再进行最终判断的时候n大于MAXIMUM_CAPACITY,我们这时候就会返回MAXIMUM_CAPACITY

所以,可以看出,在阅读一些源码的过程中,对于一些基本知识的掌握还是有必要的。能帮我们去理解为什么会是这样的,有些时候知其然我们还应该更多的去知其所以然。写这篇文章也是由于自己是这么过来的,不知道你们是否会有这种感受,看源码的时候硬着头皮看,看完一知半解,又或者由于哪里一直不理解耿耿于怀,就很纠结那种心理。反正成长的过程永远都是痛苦的,等到过了这个坎之后的轻松和快乐也是加倍的。如果你看到这里,那么谢谢你。我是互联网中一朵小小的浪花,希望有一天变成能奔涌起来,我叫皮皮,下期再见!

VFdGxRMrouHzgN1