Java 中支持7种位运算符,一般来讲位运算符只能操作整数类型的变量或值。
&
:按位与,当两位同时为1时才返回1|
:按位或,当两位中有一个为1时即可返回1~
:按位非,单目运算符,将操作树的每个位(包括符号位)全部取反^
:按位异或,当两位相同时返回0,不同时返回1<<
:左移运算符>>
:右移运算符>>>
:无符号右移运算符
1. 原码、反码和补码
1.1 机器数和真值
一个数在计算机中的二进制表示称为这个数的机器数。机器数是带符号的,第1位表示这个数的符号位,0表示正数、1表示负数。例如:十进制的5,使用二进制(8位)的表示为00000101
,而十进制的-5,使用二进制的表示为11111011
。
因为机器数中第1位表示符号位,所以机器数的形式值在某些情况下不等于真正的值。例如,如果仅按照二进制到十进制的转换规则,-5的二进制表示11111011
转为十进制为251。因此,为了更好的区分,将带符号位的机器数对应的真正数值称为机器数的真值。
1.2 原码
源码就是符号位加上真值的绝对值,对于8为的二进制来讲,所能表示的源码范围为11111111 ~ 01111111
,即十进制的-127 ~ 127
。
十进制值1的8位二进制原码: 00000001
十进制值-1的8位二进制原码:10000001
1.3 反码
反码的表示方法:对于正数,反码就是原码;对于负数,反码就是在原码的基础上,除符号位以外,其余各个位取反。
十进制值1的8位二进制原码: 00000001
十进制值1的8位二进制反码: 00000001
十进制值-1的8位二进制原码:10000001
十进制值-1的8位二进制反码:11111110
1.4 补码
补码的表示方法:对于正数,补码就是反码,或者说,补码就是原码;对于负数,补码就是在反码的基础上加1。
十进制值1的8位二进制原码: 00000001
十进制值1的8位二进制反码: 00000001
十进制值1的8位二进制补码: 00000001
十进制值-1的8位二进制原码:10000001
十进制值-1的8位二进制反码:11111110
十进制值-1的8位二进制补码:11111111
1.4.1 补码的意义
从开发者的角度来看,原码是最直接的,在了解进制转换规则后,二进制代码基本是"所见即所得"。补码的出现主要是为了简化计算机中整数运算的硬件实现和逻辑处理,同时提供了一种方便的表示方法,使得符号、零和溢出的处理更加统一和高效,其具体意义如下:
- 加法和减法的统一性:补码可以简化计算机中的加法和减法运算。在使用补码表示时,加法和减法可以通过相同的硬件电路来执行,这样简化了运算单元的设计和实现。
- 零的唯一表示 :在原码中,0有两个表示
1000000
和00000000
,而使用补码可以确保零只有一种表示00000000
。这种唯一性计算机在进行比较和判断是否相等时更加简便。 - 溢出处理: 在补码中,溢出的处理变得更加容易。如果溢出,结果将丢失最高位的进位,而这正好对应着补码的规则,使得溢出后的结果仍然在合法的补码范围内。
补码溢出监测和处理可以阅读这篇文章。
2. 按位与、或以及异或
- &:按位与,当两位同时为1时才返回1
- |:按位或,当两位中有一个为1时即可返回1
- ^:按位异或,当两位相同时返回0,不同时返回1 | 第1个操作数 | 第2个操作数 | 按位与(&) | 按位或(|) | 按位异或(^) | | --- | --- | --- | --- | --- | | 0 | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 1 | 1 | | 1 | 0 | 0 | 1 | 1 | | 1 | 1 | 1 | 1 | 0 |
java
System.out.println(5 & 9); // 输出1
System.out.println(5 | 9); // 输出13
System.out.println(~-5); // 输出4
System.out.println(5 ^ 9); // 输出12
- 对于第1个例子:5的二进制补码为
00000101
,9的二进制补码为00001001
,5 & 9的结果为00000001
,十进制结果为1。 - 对于第2个例子:5的二进制补码为
00000101
,9的二进制补码为00001001
,5 | 9的结果为00001101
,十进制结果为13。 - 对于第3个例子:-5的二进制补码为
11111011
,~-5的结果为00000100
,十进制结果为4。 - 对于第4个例子:5的二进制补码为
00000101
,9的二进制补码为00001001
,5 ^ 9的结果为00001100
,十进制结果为12。
2.1 异或的运算率
- 规律率:
a ^ a = 0
- 恒等率:
a ^ 0 = a
- 交换律:
a ^ b = b ^ a
- 结合律:
(a ^ b) ^ c = a ^ (b ^ c)
- 自反律:
a ^ b ^ a = b
3. 移位操作
移位运算符对应的操作,不会对原有的操作数进行修改,只是得到一个新的运算结果
3.1 左移操作
左移运算符(<<)对应的操作是将操作数的二进制码整体左移指定的位数,左移后右边空出来的位以0填充、左边多余的位数截断。
java
System.out.println(5 << 2); // 输出结果20
System.out.println(-5 << 2); // 输出结果-20
- 对于第1个例子:5的二进制补码为
00000101
,左移2位后的二进制为00010100
,对应的十进制为20。 - 对于第2个例子,-5的二进制补码为
11111011
,左移2位后的二进制为11101100
,对应的反码为11101011
,对应的原码为10010100
,对应的十进制为-20。
3.2 右移操作
右移运算符(>>)对应的操作将操作数的二进制码整体右移指定的位数,右移后左边空出来的位以原来的符号位填充、右边多余的位数截断。
java
System.out.println(5 >> 2); // 输出结果1
System.out.println(-5 >> 2); // 输出结果-2
- 对于第1个例子:5的二进制补码为
00000101
,右移2位后的二进制为00000001
,对应的十进制为1。 - 对于第2个例子:-5的二进制补码为
11111011
,右移2位后的二进制为11111110
,对应的反码为11111101
,对应的原码为10000010
,对应的十进制为-2。
3.3 无符号右移操作
无符号右移运算(>>>)对应的操作的二进制码整体右移指定的位数,右移后左边空出来的位以0填充、右边多余的位数截断。
java
System.out.println(5 >>> 2); // 输出结果1
System.out.println(-5 >> 2); // 输出结果126
- 对于第1个例子:5的二进制补码为
00000101
,右移2位后的二进制为00000001
,对应的十进制为1。 - 对于第2个例子:-5的二进制补码为
11111011
,右移2位后的二进制为01111110
,对应的十进制为126。
4. 位运算符规则
- 对于低于 int 类型(例如:byte、short、char)的操作数总是先自动类型转换为 int 类型后再移位。
- 对于 int 类型的整数移位
a >> b
,当 b > 32时,系统先用 b 对32求余(因为 int 类型只有32位),求余的结果作为真正的移动位数,例如,a >> 33
和a >> 1
的结果是一样的。 - 对于 long 类型的整数移位
a >> b
,当 b > 64 时,系统先用 b 对64求余(因为 long 类型只有64位),求余的结果作为真正的移动位数。
5. 位运算的使用技巧
在 Java 中位运算符被广泛应用于一些特定的场景,通常涉及处理原始数据的底层位表示,常见的使用技巧如下:
5.1 掩码操作
通过位运算,可以方便地进行掩码操作,用来提取或设置特定位的值。下面的例子演示了提取低8位的值。
java
int value = 400;
int mask = 0xFF; // 十进制值255,低8位掩码
System.out.println(value & mask); // 输出144
value 的二进制补码为00000000,00000000,00000001,10010000
(这里的逗号只是为了更好的区分),mask 的二进制补码为00000000,00000000,00000000,11111111
,value & mask 的按位与操作结果为00000000,00000000,00000000,10010000
,对应的十进制为144。
5.2 优化算法性能
位运算可以快速计算一些数学运算。例如,用位运算来代替乘法、除法和取余等操作,可以大幅提高算法的性能,特别是在嵌入式系统和高性能计算机领域中。下面列举了一些简单的使用位运算符优化算法的例子。
判断奇偶数
java
if (n & 1 == 1) {
// 基数操作
} else {
// 偶数操作
}
5.2.1 快速交换值
利用异或的运算律
java
private void swap(int x, int y) {
x = x ^ y;
y = x ^ y; // y = (x ^ y) ^ y = x ^ (y ^ y) = x ^ 0 = x
x = x ^ y; // x = (x ^ y) ^ x = y
}
5.2.2 求整数的绝对值
- 对于正数返回其本身,对于负数先加-1再取反。
- 任何数与-1进行异或运算,相当于执行了取反操作
java
private int abs(int value) {
int temp = value >> 31; // 整数得0,负数得-1
return (value + temp) ^ temp;
}
5.2.3 找出没有重复的数
有一组整型数据,其中,只有一个数出现1次,其余的数均出现2次
java
int find(int[] values) {
int result = values[0];
for (int i = 1; i < values.length; i++) {
result ^= values[i];
}
return result;
}
5.2.4 找出唯一重复的数
把1~1000个数放在长度为1001的数组中,只有唯一一个元素重复,其它的值只出现一次,设计一个算法,在不利用辅助空间的情况下,找出这个重复的值。
java
int find(int[] values) {
int result = values[0];
for (int i = 1; i < values.length; i++) {
result ^= values[i];
}
for (int j = 1; j <= 1000; j++) {
result ^= j;
}
return result;
}
5.3 状态标识位处理
在编程中,我们经常需要维护一些状态标志位,如文件权限、用户权限、开关状态等。这时候可以使用位运算来进行状态标志位的处理,通过对标志位的位运算来实现状态的设置、查询和判断等操作。
java
public class Example {
private static final int FLAG1 = 1;
private static final int FLAG2 = 1 << 1;
private static final int FLAG3 = 1 << 2;
private int flag;
public void doSometing1() {
// 添加FLAG1标识
flag |= FLAG1;
// 其他操作
}
public void doSometing2() {
// 添加FLAG2标识
flag |= FLAG2;
// 其他操作
}
public void execute() {
if ((flag & FLAG1) = FLAG1) {
// 判断是否为FLAG1标识
}
if ((flag & FLAG2) = FLAG2) {
// 判断是否为FLAG2表示
}
}
}
5.4 数据压缩和编码
位运算可以用于数据压缩和编码,比如将一些常见字符的 ASCII 码用更少的位数进行表示,这样可以大幅减少数据的存储和传输成本。