logo头像

BUG本天成,妙手偶得之

JAVA中有趣的位运算

&, |, ^, ~ 这些符号什么意思?有什么妙用?一起来感受它们的神奇吧~

当我们看一些源码的时候,经常会看到诸如 &、|、^、~ 的符号,这些就是位运算符。

位运算是直接对一个整形的二进制位进行操作,效率上比起加减乘除高不少,因此常运用在对性能很敏感的场景。

& 与运算

在二进制格式下,将两个数的每一位(1或0)分别做与运算(1&1=1,其它=0),得到一个新的二进制数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Bit {
public static void main(String[] args) {
/*
* 十进制 二进制
* 5 0 1 0 1 从最低位(右)开始比较,不足的为0
* 与 与
* 14 1 1 1 0
* = =
* 4 0 1 0 0
*/
System.out.println(5 & 14);
}
}
// 输出: 4

判断整数n是奇数还是偶数:

  • n & 1 = 0 偶数
  • n & 1 = 1 奇数

原理:二进制格式下,右边第一位是0则是偶数,反之为奇数,因此只需要和1进行与运算即可。

| 或运算

在二进制格式下,将两个数的每一位(1或0)分别做或运算(0|0=0,其它=1),得到一个新的二进制数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Bit {
public static void main(String[] args) {
/*
* 十进制 二进制
* 2 0 1 0 从最低位(右)开始比较,不足的为0
* 或 或
* 4 1 0 0
* = =
* 6 1 1 0
*/
System.out.println(2 | 4);
}
}
// 输出: 6

在Linux系统中,文件权限管理用1、2、4分别表示执行x、写w、读r的权限。

可以看做一个三位的二进制数,每一位分别表示一种权限的开启与否(1开启,0关闭),通过或运算组合就得到了不同的权限组合。

所以最高权限就是7,即二进制的“111”,拥有读、写、执行全部权限。而777权限则是所属用户、组用户、其他用户都拥有最高权限。

基于这个思路,我们只需要一个int或者long型的数字就可以存储几十个布尔类型的属性值,在某些场景下很有用。

^ 异或运算

异或:相同为false,不同true

在二进制格式下,将两个数的每一位(1或0)分别做异或运算(0^0=0,1^1=0, 其它=1),得到一个新的二进制数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Bit {

public static void main(String[] args) {
/*
* 十进制 二进制
* 2 0 1 0 从最低位(右)开始比较,不足的为0
* 异或 异或
* 6 1 1 0
* = =
* 4 1 0 0
*/
System.out.println(2 ^ 6);
}
}
// 输出: 4

异或有个有趣的特性,它的逆运算是它本身,即A^B=C,C^B=A。基于这个特点,可以做一个简单的加密,把B作为秘钥,原文A用秘钥B加密后进行传输或存储等,使用时再用秘钥B进行解密。

通过异或操作还能实现两个数的交换,不需要中间值。(简单测了下性能并没有很棒棒)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Bit {

public static void main(String[] args) {
int x = 2;// 010
int y = 4;// 100
x = x ^ y;// 110
y = y ^ x;// 010
x = x ^ y;// 100
System.out.println("x = " + x);
System.out.println("y = " + y);
}
}

/* 输出:

x = 4
y = 2

*/

~ 非运算

在二进制格式下,将两个数的每一位(1或0)分别做非运算(~0=1,~1=0),得到一个新的二进制数。

1
2
3
4
5
6
7
8
public class Bit {

public static void main(String[] args) {
System.out.println(~1);
}

}
// 输出: -2

1进行非运算后值成了负数,不只是1,只要是正数,取非后都是负数,因为对于有符号的整数,最高位(最左边)是用来表示正负的,最高位为0是正数,1是负数。

正数1非运算后从“00000000000000000000000000000001”变成了“11111111111111111111111111111110”。

二进制表示负数的情况,要转成十进制需要两个步骤:

  1. 逐位取反 -> 00000000000000000000000000000001(2进制)
  2. 加1 -> 00000000000000000000000000000010(2进制) -> 2(10进制)
  3. 加上负号 -> -2(10进制)

总结

通过位运算可以巧妙且高效地达到某些目的,但如果不是很有必要,并不建议使用,毕竟可读性不高,别人看起来太痛苦(想想在阅读源码时看到一堆位运算的心情)。

这次简单介绍了与、或、非、异或,下次再讲讲移位操作的实践。