你应该知道的一些“位运算”

相信很多同学在Java基础学习过程中,对于常见常用的各种运算符号都有一定的心得了。特别是我们很容易学习和理解的算术运算符(+,-,*,/,%,++,–),赋值运算符(=,+=,-=,*=,/=,%=),比较运算符(>,<,>=,<=,==,!=),逻辑运算符(&&,||,!),三目运算符( ? :)。但是当看到位运算符的时候,很多没有基础的同学可能有一点懵逼吧😏。
其实,位运算在某些时候还是非常有用的。特别是在底层编码的时候,它可以带来很多性能以及语法上的优化。当然,我们应用层的开发人员可能用得比较少。不过,多了解一些还是很有好处的,所以接下来我们就聊聊那些你应该知道的“位运算”。

什么是位运算

我们知道程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说白了,就是直接对整数在内存中的二进制位进行操作。因此,位运算最直接的好处就是节省内存空间,提高运算效率。在学习位运算符的时候,我们就可以根据这个概念,先确定两个前提:

  1. 位运算符都是以二进制的方式进行运算;
  2. 位运算符都是操作的整数;

位与(&)

规则:位与(&)是一个双目运算符,也就是说&符号的左右各有一个操作数。它是把两个操作数在二进制的形式上按位进行比较,如果都是1,则结果的这一位就为1;否则就为0。
示例:

1
2
3
4
5
6
public static void main(String[] args){
int a = 129;
int b = 128;
System.out.println(a & b);
}
//运行结果为:128

我们来分析解读一下这个程序:变量a的值是129,转换成二进制就是10000001,而变量b的值是128,转换成二进制就是10000000。根据位与(&)运算符的运算规律,只有两个位都是1,结果才是1,可以知道结果就是10000000,即128。
位与(&)符号,如果左右两边操作数是boolean类型的true或false的时候,我们完全可以把true看作是一个1,false看作是一个0。这样,根据规则位与(&)就会起到和逻辑与(&&)一样的作用了。它的左右两端必须同时为true,整个表达式就为true,只要有一个false,这个表达式就为false。
1
2
3
4
5
6
public static void main(String[] args){
System.out.println(true & true);//打印:true
System.out.println(true & false);//打印:false
System.out.println(false & true);//打印:false
System.out.println(false & false);//打印:false
}

应用:
在实际开发中,位与(&)对我们最大的用处是作为逻辑运算符—逻辑与(&&)的补充使用。逻辑与(&&)有一个特点就是短路,而位与(&)就没有短路的效果。所以,当我们需要在与操作的时候要求:不管第1个表达式的结果如何,都必须要运算第2个表达式的时候,我们就可以选用位与(&)符号替代逻辑与(&&)。

位或(|)

规则:位或(|)也是一个双目运算符。它是把两个操作数在二进制的形式上按位进行比较,只要有一个是1,则结果的这一位就为1;否则就为0。
示例:

1
2
3
4
5
6
public static void main(String[] args){
int a = 129;
int b = 128;
System.out.println(a | b);
}
//运行结果为:129

我们来分析解读一下这个程序:变量a的值是129,转换成二进制就是10000001,而变量b的值是128,转换成二进制就是10000000。根据位或(|)运算符的运算规律,只要某个位是1,结果位就是1,可以知道结果就是10000001,即129。
位或(|)符号,如果左右两边操作数是boolean类型的true或false的时候,我们同样可以把true看作是一个1,false看作是一个0。这样,根据规则位或(|)就会起到和逻辑或(||)一样的作用了。它的左右两端只要一个为true,整个表达式就为true;必须两个都是false,整个表达式才为false。
1
2
3
4
5
6
public static void main(String[] args){
System.out.println(true | true);//打印:true
System.out.println(true | false);//打印:true
System.out.println(false | true);//打印:true
System.out.println(false | false);//打印:false
}

应用:
在实际开发中,位或(|)同样是用来对逻辑或(||)做替代的。使用位或(|)操作boolean表达式的时候,效果与逻辑或(||)一样,且没有短路效果。

位非(~)

规则:位非(~)是一个单目运算符,语法上它的右边有一个整数作为操作数。运算时,会把这个操作数的二进制按位进行1变0、0变1的操作。
示例:

1
2
3
4
public static void main(String[] args){
int a = 4;
System.out.println(~a);//打印:-5
}

我们来分析一下:

描述 二进制
4的二进制是 0000 0000 0000 0000 0000 0000 0000 0100
位反后的结果 1111 1111 1111 1111 1111 1111 1111 1011
根据负数二进制转换十进制的规则
先减去1 1111 1111 1111 1111 1111 1111 1111 1010
再取反得到绝对值 0000 0000 0000 0000 0000 0000 0000 0101

所以最终结果为-5。
应用:
位非运算符当然也可以操作boolean表达式,只不过使用它和使用逻辑非(!)没有什么区别,所以在应用层开发中这个运算符使用率并不高。

异或(^)

规则:异或(^)是一个双目运算符。运算时,它是把两个操作数在二进制的形式上按位进行比较,如果都是1或都是0(相同),则结果的这一位就为0;否则两个对应位不同(一个0和一个1),结果位就为1。
示例:

1
2
3
4
5
6
public static void main(String[] args){
int a = 15;
int b = 2;
System.out.println(a ^ b);
}
//运行结果为:13

分析解读一下这个程序:变量a的值是15,转换成二进制为1111,而变量b的值是2,转换成二进制为0010,根据异或的运算规律(相同为0,不同为1),可以得出其结果为1101 即13。
应用:
异或(^)运算有两个非常有趣的结论:

  1. 任何一个数异或(^)它本身,结果是0;
  2. 任何一个数异或(^)0,结果是它本身。

a ^ a 的结果是0;
a ^ a ^ a 的结果是 a;
a ^ a ^ a ^ a 的结果是0;
a ^ a ^ a ^ a ^ a 的结果又是a。
所以,异或(^)运算符又被称之为“翻面”,每异或(^)一次自己就翻一个面。

左移(<<)

规则:将运算符左边的整数(二进制形式)向左移动运算符右边指定的位数(在低位补0)。
示例一:

1
2
3
4
public static void main(String[] args){
System.out.println(5 << 2);
}
//运行结果为:20

分析解读一下这个程序:

描述 二进制
5的二进制是 0000 0000 0000 0000 0000 0000 0000 0101
左移2位 0000 0000 0000 0000 0000 0000 0001 0100

所以结果是:20。

示例二:

1
2
3
4
public static void main(String[] args){
System.out.println(-4 << 2);
}
//运行结果为:-16

分析解读一下这个程序:

描述 二进制
-4的二进制 1111 1111 1111 1111 1111 1111 1111 1100
左移2位 1111 1111 1111 1111 1111 1111 1111 0000
根据负数二进制转换十进制的规则
先减去1 1111 1111 1111 1111 1111 1111 1110 1111
再取反得到绝对值 0000 0000 0000 0000 0000 0000 0001 0000

所以结果是:-16。
应用:
左移(<<)运算就是一种乘法运算,任何一个整数左移多少位,就是把这个整数乘以2的多少次方。这种运算在效率和性能上都比算术运算中的乘法(*)要高很多。

右移(>>)

规则:将运算符左边的整数(二进制形式)向右移动运算符右边指定的位数。使用符号扩展机制,也就是说,如果值为正,则在高位补0,如果值为负,则在高位补1。
示例一:

1
2
3
4
public static void main(String[] args){
System.out.println(13 >> 2);
}
//运行结果为:3

分析解读一下这个程序:

描述 二进制
13的二进制 0000 0000 0000 0000 0000 0000 0000 1101
右移2位 0000 0000 0000 0000 0000 0000 0000 0011

所以结果是:3。

示例二:

1
2
3
4
public static void main(String[] args){
System.out.println(-13 >> 2);
}
//运行结果为:-4

分析解读一下这个程序:

描述 二进制
-13的二进制 1111 1111 1111 1111 1111 1111 1111 0011
右移2位 1111 1111 1111 1111 1111 1111 1111 1100
根据负数二进制转换十进制的规则
先减去1 1111 1111 1111 1111 1111 1111 1111 1011
再取反得到绝对值 0000 0000 0000 0000 0000 0000 0000 0100

所以结果是:-4。
应用:
右移(>>)运算就是一种除法运算,任何一个整数右移多少位,就是把这个整数除以2的多少次方。这种运算在效率和性能上都比算术运算中的除法(/)要高很多。
注意:
同学们会发现右移一个正整数确实会得到除法的效果,但是右移一个负整数得到的结果会比除法的效果要小一个数。
13 >> 2 得到 3
13 / 4 得到 3
-13 >> 2 得到 -4
-13 / 4 得到 -3
这是因为计算机在除不尽的时候统统采用的是向下取整,而我们人则习惯于直接去掉小数部分。不同的计算机语言在设计除法运算的时候有些会直接按计算机的方式来,有些会按人的习惯来。这里很明显,Java语言在设计除法运算符的时候采用了人的自然习惯。

无符号右移(>>>)

规则:将运算符左边的整数(二进制)向右移动运算符右边指定的位数。采用0扩展机制,也就是说,无论值的正负,都在高位补0。
示例:

1
2
3
4
public static void main(String[] args){
System.out.println(-4 >>> 2);
}
//运行结果为:1073741823

分析解读一下这个程序:

描述 二进制
-4的二进制 1111 1111 1111 1111 1111 1111 1111 1100
右移2位高位补0 0011 1111 1111 1111 1111 1111 1111 1111

所以结果是:1073741823。
应用:
无符号右移(>>>)由于高位一定会补0,所以最后的结果一定会是一个正数。但它的计算没有任何数学意义,只有逻辑意义。它主要出现在散列、加密、压缩、影音媒体编码等技术上。

位运算的常见使用技巧

由于位运算符是直接针对二进制数据进行操作,而计算机内部就是直接以二进制的形式表示数据的,所以使用位运算符在效率上往往要比其他运算符高上很多。只是说使用位运算符做比较复杂的运算时,对于我们“普通人”来说比较难于考虑和理解,所以让很多人望而却步。下面,我们就介绍几个比较简单的、作为“普通人”也能用得上的常见技巧。

  1. 使用在逻辑运算中,使用位与(&)和位或(|)替代逻辑与(&&)和逻辑或(||),从而达到不短路的效果。—日常开发中常用
  2. 使用异或(^)完成两个变量的交换。—面试常见
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args){
    int a = 10;
    int b = 7;
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    }
    运行结果:
    a = 7
    b = 10
  3. 计算绝对值;
    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args){
    int x = -5;
    int y = x >> 31;
    int result = (x + y) ^ y;//或(x ^ y)- y
    System.out.println(x + "的绝对值是:" + result);
    }
    运行结果:
    -5的绝对值是:5
  4. 判断int变量是否是奇数或偶数;
    a & 1 == 0;——偶数
    a & 1 == 1;——奇数
  5. 求两个int的平均数;
    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args){
    int x = -5;
    int y = 21;
    int result = (x & y) + ((x ^ y) >> 1);
    System.out.println("x与y的平均数是:" + result);
    }
    运行结果:
    x与y的平均数是:8
  6. m乘以2的n次方 等价于 m << n;
  7. m除以2的n次方 等价于 m >> n;
  8. x = (x == a) ? b : a 等价于 x = a ^ b ^ x;