Java中位运算的详细介绍和应用

280 阅读14分钟

关于位运算,涉及到的知识点比较多,本文会尽可能全面的去介绍,但个人水平有限,如有错漏,烦请指正。

文章从Typora编写,一些上标下标的显示在掘金上可能显示的不好。

我认为在学习位运算前,得先了解计算机中数据的表现方式。

数据的表示

计算机中数据是以二进制流通的,那么我们就要知道将数转成二进制的方法:

十进制转二进制

短除法:将94转换成二进制:

94 / 2 余 0 得47

47 / 2 余 1 得23

23 / 2 余 1 得11

11 / 2 余 1 得5

5 / 2 余 1 得2

2 / 2 余 01

最后一个粗体1也算,倒序得出:1011110


拓展:十进制转八进制

同样用短除法:将94转换成八进制:

94 / 8 余 6 得11

11 / 8 余 31

同样最后一个粗体1也算,倒序得出:136

拓展:十进制转十六进制

还是短除法:将94转成16进制:

94 / 16 余145

14的十六进制表示为E,虽然没有足够的数可以倒序,但还是可以得出:5E

结论:十进制转R进制,R为任何整数,通过短除法用十进制数除以R,最后倒序得出的就是R进制数。


拓展:二进制转八进制和十六进制

用上面十进制94转换后的数据来试试

二进制转八进制:1011110

8的二进制位数是000,即每三个数分一段,将1011110从左往右分段,右边最后一段不够的补0:

001 011 110

将每段转成8进制拼接起来:

1 3 6 -> 136,所以得出的八进制是136

二进制转十六进制:1011110

16的二进制位数是0000,即每四个数分一段,将1011110从左往右分段,右边最后一段不够的补0:

0101 1110

将每段转成16进制拼接起来:

5 14 -> 5E

结论:二进制转八进制、十六进制或者是其他幂指数为2的n次方的进制,将其分段并转成对应进制数,最后拼接而成的就是该进制。

R进制转十进制

R进制转十进制不能用上面的方法,有一套公式:

二进制:abc.de = a*2^2 + b*2^1 + c*2^0 + d*2^-1 + e*2^-2

七进制:abc.de = a*7^2 + b*7^1 + c*7^0 + d*7^-1 + e*7^-2


每一项用R^k^ 表示,R为进制数,二进制为2,七进制为7;k为数值位置,个位数为0,向右一位加一,向左一位减一。每一项加起来的和为10进制结果

举个例子:二进制1011110转十进制:

1*2^6 + 0*2^5 + 1*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 0*2^0 = 64 + 0 + 16 + 8 + 4 + 2 + 0 = 94


现在大家应该对进制转换都比较了解了,接下来就看看关于原码、反码和补码的知识。

原码、反码和补码

原码:最高位是符号位,0表示正号,1表示负号。数值0的原码有两种表现形式:[+0]原 = 0000 0000,[-0] 原 = 1000 0000。

反码:最高位是符号位,0表示正号,1表示负号。正数的反码和原码相同,负数的反码是符号位不变,其他按位取反。数值0的反码有两种表现形式:[+0]反 = 0000 0000,[-0]反 = 1111 1111。

补码:最高位是符号位,0表示正号,1表示负号。正数的补码与其原码和反码相同,负数的补码等于其反码的末位加1。数值0有唯一的编码:[+0]补 = 0000 0000,[-0]补 = 0000 0000。

上面的内容是很重要的知识点,但是暂时不需要强行看懂,后面慢慢来。

既然有了原码这样的二进制表示,我们一定会有一个疑问:

为什么会有反码和补码

原码是计算机中对数字的二进制的表现方式,但原码不能直接进行运算,可能会出错,例如:

1 + (-1) = 0000 0001原 + 1000 0001原 = 1000 0010原 = -2(10进制)

所以为了解决原码存在的这个问题,出现了反码:

1 + (-1) = 0000 0001反 + 1111 1110反 = 1111 1111反 = 1000 0000原 = -0(10进制)

虽然解决了运算问题,但是又发现了一个新的问题**-0**,在数学运算中0的正负是没有任何意义的,而且0000 0000原和1000 0000原都表示0,造成了编码位置的浪费,为了解决0的符号以及两个编码的问题,出现了补码:

1 + (-1) = 0000 0001补 + 1111 1111补 = 0000 0000补 = 0000 0000原 = 0(10进制)

这样用补码解决了-0的问题,同时还可以用1000 0000补表示-128,在范围上就比原码和补码多了一位。

  • 根据运算法,减去一个正数等于加上它的负数,所以计算机可以只有加法没有减法,这样计算机的运算设计更加简单了
  • 负数补码推原码等于补码符号位不变,再进行一次补码

有了以上的基础,我们可以更好了理解位运算了。

Java中的运算符

在Java中有7种运算符:

符号含义描述
&两个比特位都为1时,结果才为1,否则为0
|两个比特位有一个1就为1,两个都是0才为0
~按位取反,1变0,0变1
异或相同为0,不同为1。任何整数和自己异或的结果为0,和0异或的结果不变
<<左移将所有二进制位左移若干位,高位舍弃,低位补0,与正负无关
>>右移将所有二进制位右移若干位,低位舍弃,高位补值与整数正负有关,正数补0,负数补1
>>>无符号右移将所有的二进制位右移若干位,低位舍弃,高位补0,与正负无关

先假设有两个数5和9

5的二进制是:0101

9的二进制是:1001

然后要知道一点,计算机中的二进制是补码的形式,我们在Java中执行一下代码就可以看出:

System.out.println(Integer.toBinaryString(-1));

并且因为Java中int是32位,所以结果为11111111 11111111 11111111 11111111补

与运算

与运算是两个比特位都为1时,结果才为1,否则为0,我们对5和9的两个二进制数进行运算:5 & 9

00000000 00000000 00000000 00000101
00000000 00000000 00000000 00001001
----------------------------------- &
00000000 00000000 00000000 00000001

提示:正数的原码和补码一样

所以得出:5 & 9 = 1

或运算

或运算是两个比特位有一个1就为1,两个都是0才为0:5 | 9

00000000 00000000 00000000 00000101
00000000 00000000 00000000 00001001
----------------------------------- |
00000000 00000000 00000000 00001101

同样:正数的原码和补码一样

得出:5 | 9 = 13

非运算

非运算是按位取反:~5

00000000 00000000 00000000 00000101 
----------------------------------- ~
11111111 11111111 11111111 11111010 补
10000000 00000000 00000000 00000110 原

5的补码是:00000000 00000000 00000000 00000101补

取反后成了负数:11111111 11111111 11111111 11111010补

补码的补码为原码,所以对上面再进行一次补码(符号位不变,其他各位取反,末位加1):10000000 00000000 00000000 00000110原

得出:~5 = -6

异或运算

异或运算是两个比特位相同为0,不同为1:5 ^ 9

00000000 00000000 00000000 00000101
00000000 00000000 00000000 00001001
----------------------------------- ^
00000000 00000000 00000000 00001100 补

结果为:00000000 00000000 00000000 00001100补

因为正数的补码和原码一样,所以得出:5 ^ 9 = 12

左移

用正数5来做示例:5 << 2

5的补码:

00000000 00000000 00000000 00000101

二进制向左移2位,高位舍弃,低位补0:

00000000 00000000 00000000 00010100

得出:5 << 2 = 20,相当于乘以2的n次方,n为移动位数

右移

用正数9来做示例:9 >> 2,将二进制右移2位,低位舍弃,高位补值与整数正负有关,正数补0,负数补1:

9的补码:

00000000 00000000 00000000 00001001

二进制向右移,低位舍弃,9为正数,所以高位补0

00000000 00000000 00000000 00000010

得出:9 >> 2 = 2,相当于除以2的n次方,n为移动位数


改用-9看看:-9 >> 2,将二进制右移2位,低位舍弃,高位补值与整数正负有关,正数补0,负数补1:

10000000 00000000 00000000 00001001 (-9的原码)

11111111 11111111 11111111 11110111 (-9的补码)

二进制向右移,低位舍弃,-9为负数,所以高位补1,所以:

11111111 11111111 11111111 11111101

得出的补码是负数,转成原码要再做一次补码:

10000000 00000000 00000000 00000011

得出:-9 >> 2 = -3

无符号右移

与右移运算类似,区别在于移位后不足的补0,补值与正负数无关。

运算符的数学意义

基础操作

首先先了解下位运算的一些基础操作:

a | 0 == a
a & -1 == a
a & 0 == 0

a ^ a == 0
a ^ 0 == a
a ^ -1 = |a| - 1

a |~ a == -1
a &~ a == 0
a & a == a
a | a == a

a | (a&b) == a
a & (a|b) == a

判断一个数的奇偶性

因为1的二进制是:00000000 00000000 00000000 00000001

&运算的规则是都为1时,结果为1,否则是0,所以任何数的前31位和1进行&运算都是0,如:

00000000 00000000 00000000 00000001
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx?
-----------------------------------
00000000 00000000 00000000 0000000?

只有最后一位可能有不同结果,而偶数的最后一位不可能是1,所以任意整数N是偶数的条件为:

N & 1 == 0

交换两个整数的时候不需要中间变量

int a = 1; b = 2;
a = a ^ b;
b = b ^ a;
a = a ^ b;
System.out.println(a);	// 2
System.out.println(b);	// 1

其实用加法运算也可以,但是要注意我们今天学的是位运算:

int a = 1, b = 2;
a = a + b;
b = a - b;
a = a - b;
System.out.println(a);	// 2
System.out.println(b);	// 1

变换符号,正数变负数,负数变整数

对待操作数取反再加1即可:

int a = 1;
a = ~a + 1;
System.out.println(a); // 结果为:-1

求绝对值

方法一:如果是正数直接返回,负数则通过上面变换符号的方式得到绝对值

判断是正数还是负数,可以通过右移运算,回顾一下右移运算的规则:右移若干位,低位舍弃,高位补值,如果是正数补0,负数则补1,我们用-5来举例:-5 >> 31

-510000000 00000000 00000000 0000010111111111 11111111 11111111 11111011>> 31,即右移31位后,将本来的首位即符号位移到了最右边
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx1
前面的值,如果被操作数(-5)是正数,则会补0,负数补1

因为-5是负数,所以补1得到的值是:11111111 11111111 11111111 11111111 补,转成原码:
10000000 00000000 00000000 00000001,即值为-1

假设原值为5,则右移31位后得到的数是:00000000 00000000 00000000 00000000,值为0

所以通过判断右移31位得到的值是0还是-1,就可以知道被操作数是正数还是负数。
至于说为什么不用 if n > 0 来判断,我认为是位运算的效率更高。

看代码:

public static int abs(int n) {
    int i = n >> 31; // 得到符号位,0为正数,-1为负数
    return i == 0 ? n : (~n + 1); // 正数直接返回,负数则返回绝对值
}

方法二

先上代码:

public static int abs(int n) {
    return (n ^ (n >> 31)) - (n >> 31);
}

从上面的基础操作可以知道两个公式:

a ^ 0 = a
a ^ -1 = |a| - 1

所以:

当n为正数时:
(n ^ (n >> 31)) - (n >> 31)
= n ^ 0 - 0
= n
当n为负数时:
(n ^ (n >> 31)) - (n >> 31)
= n ^ -1 + 1
= |n| - 1 + 1
= |n|

判断一个数是不是2的幂

如果n是2的幂,那么n的二进制一定是首位为1,其他各位全部是0,因为:

1000(2进制) = 8(十进制) = 2的3次幂

1100(2进制) = 12(十进制),不是2的幂

1110(2进制) = 14(十进制),不是2的幂

....

先列举一些是2的幂的数:2,4,8,16,他们的二进制是:

0000 0010
0000 0100
0000 1000
0001 0000

然后是他们的减1之后的数:1,3,7,15,他们的二进制是:

0000 0001
0000 0011
0000 0111
0000 1111

当2&1、4&3、8&7、16&15的时候,结果全都是0,详细说一下4&3:

0000 0100
0000 0011
--------- &
0000 0000

其他的同理,所以可以得出是2的幂的数,会满足条件:

N & (N - 1) == 0

正数n乘以2

n << 1,如 9 << 1 = 9 * 2 = 18

同理,n乘以2的x次幂:n << x,如 9 << 2 = 9 * 2 * 2 = 36

正数n除以2

n >> 1,如 9 >> 1 = 9 / 2 = 4

同理,n除以2的x次幂:n >> x,如 9 >> 2 = 9 / 2 / 2 = 2

参考资料

Java 位运算超全面总结(以及Koltin)

java 位运算与实战