在平时看各种框架的源码过程中,经常会看到一些位移运算,所以作为一个有经验的Java开发者是一定要掌握位移运算的。
1. 正数位移运算
java中有三个位移运算:
<<:左移
>>:右移
>>>:无符号右移
1 | System.out.println(2 << 1); //4 |
下面就来详细解释以下结果是如何运算出来的。
上面Demo中有 2和 -2 ,这两个十进制数,并且是int
类型(Java中占4个字节),位元算是基于二进制bit来的,所以我们需要将十进制转化为二进制之后再进行运算:
2 << 1
:十进制2转化为二进制为00000000 00000000 00000000 00000010
,再将二进制左移 1 位,高位丢弃,低位补0,所以结果为00000000 00000000 00000000 00000100
,换算成十进制为4(2n-1 其中n为非0为下标 )2 >> 1
:十进制2转化为二进制为00000000 00000000 00000000 00000010
,再将二进制右移 1 位,高位补0,低位丢弃,所以结果为00000000 00000000 00000000 00000001
,换算成十进制为1
对于这两种情况都非常好理解,那么什么是无符号右移,以及负数是怎么运算的?我们先来看-2 << 1
和 -2 >> 1
,这两个负数的左移与右移操作其实和正数类似,都是先将十进制数转换成二进制数,再将二进制数进行移动,所以现在的关键是负数如何用二进制数进行表示。
2. 原码、反码、补码
接下来我们主要介绍十进制数用二进制表示的不同方法,所以为了简洁,我们用一个字节,也就是8个bit来表示二进制数。
2.1 原码
十进制 | 原码 |
---|---|
2 | 0000 0010 |
-2 | 1000 0010 |
-111 1111
~ +111 1111
,
换成十进制就是 -127 ~ 127
2.2 反码
在数学中我们有加减乘除,而对于计算机来说最好只有加法,这样计算机会更加简单高效,我们知道在数学中5-3=2
,可以使用加法表示:5+(-3)=2
,而乘法是加法的累积,除法是减法的累积,所以在计算机中只要有加法就够。
一个数字用原码表示是容易理解的,但是需要单独的一个bit来表示符号位。并且在进行加法时,计算机需要先识别某个二进制原码是正数还是负数,识别出来之后再进行相应的运算。这样效率不高,能不能让计算机在进行运算时不用去管符号位,也就是说让符号位也参与运算,这就要用到反码。
十进制 | 原码 | 反码 |
---|---|---|
2 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 |
正数的反码和原码一样,负数的反码就是在原码的基础上符号位保持不变,其他位取反。
那么我们来看下,用反码直接运算会怎么样,以5-3
为例。5 - 3
等于5 + (-3)
十进制 | 原码 | 反码 |
---|---|---|
5 | 0000 0101 | 0000 0101 |
-3 | 1000 0011 | 1111 1100 |
1 | 5-3 |
结果算出来是1,不合理啊,差了1。
接着看一个特殊的运算:
1 | 1-1 |
再看一个特殊的运算:
1 | 0+0 |
我们可以看到1000 0000表示-0,0000 0000表示0,虽然-0和0是一样的,但是在用原码和反码表示时是不同的,我们可以理解为在用一个字节表示数字取值范围时,这些数字中多了一个-0,所以导致我们在用反码直接运算时符号位可以直接参加运算,但是结果会不对。
2.3 补码
为了解决反码的问题就出现来补码
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
2 | 0000 0010 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 | 1111 1110 |
正数的补码和原码、反码一样,负数的补码就是反码+1(满2进1)。
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
5 | 0000 0101 | 0000 0101 | 0000 0101 |
-3 | 1000 0011 | 1111 1100 | 1111 1101 |
1 | 5-3 |
5 - 3= 2 ! 正确
再来看特殊的
1 | 1-1 |
1 - 1 = 0 ! 正确
1 | 0+0 |
0 + 0 = 0 ! 正确
所以,我们可以看到补码解决来反码的问题,对于数字,我们可以使用补码的形式进行二进制表示。
3. 负数位移运算
那么接着来看 -2 << 1
与 -2 >> 1
其中十进制-2
原码、反码、补码分别为:
- 原码:
1000000 00000000 00000000 00000010
- 反码:
1111111 11111111 11111111 11111101
- 补码:
1111111 11111111 11111111 11111110
那么 -2 << 1
表示 -2的补码左移一位后结果为:1111111 11111111 11111111 11111100
该补码对应的反码为
1 | 1111111 11111111 11111111 11111100 |
所以 -2 << 1 = -4
, 同理 -2 >> 1
也是一样的计算方法。
4. 无符号右移
上面在进行左移和右移时,我有一点没讲到,就是在对补码进行移动时,符号位是固定不动的,而无符号右移是指在进行移动时,符号位也会跟着一起移动。
比如 -2 >>> 1
:
- -2原码:
1000000 00000000 00000000 00000010
- -2反码:
1111111 11111111 11111111 11111101
- -2补码:
1111111 11111111 11111111 11111110
右移后的补码对应的反码、原码为:0111111 11111111 11111111 11111111
(因为现在的符号位是0表示正数,正数的补、原、反码都是一样的)
所以对应的十进制为2147483647
,也就是 -2 >>> 1 = 2147483647
5. 总结
相信看完上面写的小伙伴们,都以发现:
2 << 1 = 4 = 2 * 21
2 << 2 = 8 = 2 * 22
2 << n = 2 * 2n
m << n = m * 2n
右移则相反,所以大家以后在源码中再看到位运算时,可以参考上面的公式。