之前做过类似的文章,讲的不是太好,现在重新记录一遍,方便自己理解
参考文献:一文搞明白位运算、补码、反码、原码
二进制的正负
我们十进制使用加减号+ -
表示数字的正负,而计算机是二进制的,怎么表示正负呢?
答:二进制中的第一位来表示符号位,0表示正数,1表示负数
正数的位移运算
Java中有三个位移运算
<<
左移:符号位不变,高位丢弃,低位补0>>
右移:符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1>>>
无符号右移:右移符号位也跟着移动,低位丢弃,高位补0
我们直接来看一下Demo:
1 | System.out.println(2 << 1); // 4 |
一眼看到上面Demo的打印结果,你应该是懵逼的,接下来我来解释一下这个结果到底是如何运算出来的。
上面的Demo中有“2”和“-2”,这是两个十进制数,并且是int类型的(java中占四个字节),位运算是基于二进制bit来的,所以我们需要将十进制转换为二进制之后再进行运算:
2 << 1
:十进制“2”转换成二进制为“00000000 00000000 00000000 00000010”,再将二进制左移一位,高位丢弃,低位补0,所以结果为“00000000 00000000 00000000 00000100”,换算成十进制则为“4”2 >> 1
:十进制“2”转换成二进制为“00000000 00000000 00000000 00000010”,再将二进制右移一位,低位丢弃,高位补0,所以结果为“00000000 00000000 00000000 00000001”,换算成十进制则为“1”
对于这两种情况非常好理解,那什么是无符号右移,以及负数是怎么运算的呢?
我们先来看-2 << 1
与-2 >> 1
,这两个负数的左移与右移操作其实和正数类似,都是先将十进制数转换成二进制数,再将二进制数进行移动,所以现在的关键是负数如何用二进制数进行表示。
原码、反码、补码
接下来我们主要介绍十进制数用二进制表示的不同方法,所以为了简洁,我们用一个字节,也就是8个bit来表示二进制数。
原码
十进制 | 原码 |
---|---|
2 | 0000 0100 |
-2 | 1111 1110 |
原码其实是最容易理解的,只不过需要利用二进制中的第一位来表示符号位,0表示正数,1表示负数,
所以可以看到,一个数字用二进制原码表示的话,取值范围是-1111 1111 ~ +0111 1111
,换成十进制就是-127 ~ 127
。
反码
在数学中我们有加减乘除,而对于计算机来说最好只有加法,这样计算机会更加简单高效,
我们知道在数学中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 |
这不对呀??? 5-3=1???,为什么差了1?
我们看一个特殊的运算:
1 | 1-1 |
我们来看一个特殊的运算:
1 | 0+0 |
我们可以看到1000 0000表示-0,0000 0000表示0,虽然-0和0是一样的,但是在用原码和反码表示时是不同的,
我们可以理解为在用一个字节表示数字取值范围时,这些数字中多了一个-0,
所以导致我们在用反码直接运算时符号位可以直接参加运算,但是结果会不对。
补码
为了解决反码的问题就出现了补码。
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
2 | 0000 0010 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 | 1111 1110 |
正数的补码:和原码、反码一样
负数的补码:就是反码 + 1
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
5 | 0000 0101 | 0000 0101 | 0000 0101 |
-3 | 1000 0011 | 1111 1100 | 1111 1101 |
1)、回到之前的5-3
1 | 5-3 |
5-3=2!!正确。
2)、1-1
再来看特殊的:
1 | 1-1 |
1-1=0!!正确
3)、0+0
再来看一个特殊的运算:
1 | 0+0 |
0+0=0!!也正确。
4)、-2-2
再来看一个特殊的运算:
1 | -2-2 |
-2+(-2)=-4!!也完全正确。
所以,我们可以看到补码解决了反码的问题。
所以对于数字,我们可以使用补码的形式来进行二进制表示。
妙啊妙啊秒啊秒,突然想到了,为什么Java中的数字都限制了大小,必须限制最大的那个位,目的就是让计算机知道哪个是符号位。
负数位运算
<<
左移:符号位不变,高位丢弃,低位补01
2
3
4
5
6
7
8
9
1010000000 00000000 00000000 00000010 // -2原码
11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
11111111 11111111 11111111 11111110 // -2补码(反码 + 1)
-2 << 1 : 表示-2的补码左移一位得到补码,再该补码得到=》反码=》原码
11111111 11111111 11111111 11111100 // -2补码左移一位(补码)
11111111 11111111 11111111 11111011 // -2补码左移一位(反码 => 补码-1)
10000000 00000000 00000000 00000100 // -2补码左移一位(原码 => 反码取反,符号位除外)
= -4>>
右移:符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补11
2
3
4
5
6
7
8
9
1010000000 00000000 00000000 00000010 // -2原码
11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
11111111 11111111 11111111 11111110 // -2补码(反码 + 1)
-2 >> 1 : 表示-2的补码右移一位得到补码,再该补码得到=》反码=》原码
11111111 11111111 11111111 11111111 // -2补码右移一位(补码): 注意符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1
11111111 11111111 11111111 11111110 // -2补码右移一位(反码 => 补码-1)
10000000 00000000 00000000 00000001 // -2补码右移一位(原码 => 反码取反,符号位除外)
= -1
无符号右移
>>>
无符号右移:右移符号位也跟着移动,低位丢弃,高位补0
上面在进行左移和右移时,我有一点没讲到,就是在对补码进行移动时,符号位是固定不动的,
而无符号右移是指在进行移动时,符号位也会跟着一起移动。1
2
3
4
5
6
7
8
9
1010000000 00000000 00000000 00000010 // -2原码
11111111 11111111 11111111 11111101 // -2反码(原码取反,符号位除外)
11111111 11111111 11111111 11111110 // -2补码(反码 + 1)
-2 >>> 1 : 表示-2的补码无符号右移一位得到补码,再该补码得到=》反码=》原码
01111111 11111111 11111111 11111111 // -2补码无符号右移一位(补码)
因为现在的符号位为0,表示正数,正数的原、反、补码都相同
所以对应十进制就是 -2 >>> 1 = 2^31 -1 = 2147483647
总结
1)、
二进制中的第一位来表示符号位,0表示正数,1表示负数
2)、
<<
左移:符号位不变,高位丢弃,低位补0>>
右移:符号位不变,低位丢弃,如果为正,高位补0,如果为负,则在高位补1>>>
无符号右移:右移符号位也跟着移动,低位丢弃,高位补0
3)、
正数:原码,反码,补码相同。
负数的原码:-1的原码:10000000 00000000 00000000 00000001
负数的反码:原码,符号位不变,其他位取反
负数的补码:就是反码 + 1
4)、
参与运算的是补码,真实可以转为二进制的是原码
5)、
5-2可以表示为:5+(-2),减可以用加表示,乘可以用多个加表示,除可以用多个加表示,这样加减乘除都可以用加表示了。
反码就是为了实现负数也可以参与加法而产生了,但是会出现运算错误的现象,所以诞生了补码。
原码:真实的二进制表示,第一位表示0表示正,1表示负
反码:为了实现正负数都可以相加所采取的操作
补码:为了解决反码相加之和,数据不一致的情况,使用补码进行位运算就能成功解决
6)、Integer.toBinaryString所打印的是补码
位运算符& | ^ ~
1)、&
二元运算符
and,与,同为1才为1
1 | 10000000 00000000 00000000 00000010 // -2原码 |
2)、|
二元运算符
or,或,一个为1则为1
1 | 10000000 00000000 00000000 00000010 // -2原码 |
3)、^
二元运算符
not equals,异或,两个不同才为1
1 | 10000000 00000000 00000000 00000010 // -2原码 |
4)、~
一元运算符
not,非,所有位取反
1 | ~1 |
下面是给同事陈鹤的讲解
减法可以用加法表示
5 - 3
= 5 + (-3)
计算机只想用加法:乘就是多个加,除就是多个加
计算机的二进制正数表示
十进制 正负 + -
二进制:首尾01 1负 0正
原码
byte = 8bit
0 = 0000 0000
1 + 2
= 0000 0001原码 + 0000 0010原码
= 0000 0011原码
= 3
5 + (-3)
= 0000 0101原码 + 1000 0011原码
= 1000 1000原码
= -8错误了
反码
正数:原码,反码,补码相同。
负数:
反码:原码的符号位不变,所有位取反
5 + (-3)
= 0000 0101反码 + 1111 1100反码
= 0000 0001反码 // 1 0000 0001 因为byte表示8bit,只能取后8位,结果就是 0000 0001
= 1还是错误了
补码
正数:原码,反码,补码相同。
负数:
反码:原码的符号位不变,所有位取反
补码:反码+1
5 + (-3)
= 0000 0101补码 + 1111 1101补码
= 1 0000 0010假补码 // 1 0000 0010 因为byte表示8bit,只能取后8位,结果就是 0000 0010
= 0000 0010真补码
= 2
long 64 [2^64-1,-2^64]
int 32 [2^31-1,-2^31]
byte 8 [2^7-1,-2^7]
逻辑运算符号
&& ||
布尔类型二进制表示
true:1
false:0
位运算符号
& | ^ ~
&:同为1才为1
|:一个为1则为1
^:不相同才为1
1^2
=
00000000 00000000 00000000 00000001
00000000 00000000 00000000 00000010
~:对所有位取反
~1
= 00000000 00000000 00000000 00000001 // 1补码
= 11111111 11111111 11111111 11111110 // ~1补码
= 11111111 11111111 11111111 11111101 // ~1反码
= 10000000 00000000 00000000 00000010 // ~1原码
= -2
注意事项
1)、参与位运算的都是补码,因为补码才可以实现正负数的相加
完活
2020-07-02 17:38:44