位运算符可以直接对二进制位进行操作,比使用乘除法的效率更高,Java 中常用的移位运算符包括 <<、>>、>>>。
1 <<
<< 表示左移,左移时不管正负,低位补 0。
为了使运算过程看得更清楚,下文的示例只取 8 位,但在实际的计算机中,如果操作数是 short(占 2 字节)、byte(占 1 字节)、char 类型(占 2 字节),则在进行位运算前会自动将其转为 int 类型,即会自动扩大位数过小的数据类型,以便在移位过程中不会丢失信息,运算的结果也为 int 类型。另外注意:操作数是以补码的形式存储在计算机中的,使用补码的原因是它可以让计算机统一处理加法和减法,运算时不需要区分正数和负数,加法器既可以进行加法运算,也可以进行减法运算,简化了运算和硬件设计。
正数:result = 20 << 2
20 的二进制补码:0001 0100
向左移动两位后:0101 0000
结果:result = 80
负数:result = -20 << 2
-20 的二进制原码:1001 0100
-20 的二进制反码:1110 1011
-20 的二进制补码:1110 1100
左移两位后的补码:1011 0000
反码:1010 1111
原码:1101 0000
结果:result = -80
左移时,操作数的所有位(包括符号位)都会向左移动指定的位数,移动位数的不同,有可能影响结果的正负:
1 | byte number1 = (byte) (20 << 3); // -96 |
2 >>
符号 >> 表示右移,若该数为正,则高位补 0,若为负数,则高位补 1。
正数:result = 20 >> 2
20 的二进制补码:0001 0100
向右移动两位后:0000 0101
结果:result = 5
负数:result = -20 >> 2
-20 的二进制原码:1001 0100
-20 的二进制反码:1110 1011
-20 的二进制补码:1110 1100
右移两位后的补码:1111 1011
反码:1111 1010
原码:1000 0101
结果:result = -5
右移时,操作数的所有位(包括符号位)都会向右移动指定的位数,但由于高位补位的数字与原先的符号位相同,因此不会影响结果的正负。
3 >>>
符号 >>> 表示无符号右移,也叫逻辑右移,无符号右移时不管正负,高位补 0。
无符号右移是在不考虑符号位的情况下,操作数的所有位(包括符号位)向右移动指定的位数。为了确保结果的正确性,操作数必须被扩展为足够大的位数,因此,无符号右移只对 32 位和 64 位有意义。
正数:result = 20 >>> 2
结果与 result = 20 >> 2 相同。
负数:result = -20 >>> 2
-20 的二进制原码:10000000 00000000 00000000 00010100
-20 的二进制反码:11111111 11111111 11111111 11101011
-20 的二进制补码:11111111 11111111 11111111 11101100
无符号右移两位后:00111111 11111111 11111111 11111011
结果:result = 1073741819
4 拓展
<<=、>>=、&=、^=、|=、~ 分别表示什么意思?
这些操作符分别是左移后赋值、右移后赋值、按位与后赋值、按位异或后赋值(对应位上的值相同时,结果为 0,不同时为 1)、按位或后赋值,跟 += 还有 -= 的意思差不多,只不过这些都是要转换成二进制后再计算,最后一个运算符代表取反。
4.1 取反
注意取反运算符是对所有的位进行取反操作,包括符号位。这与负数的反码不同,负数的反码是符号位保持不变,其余位取反。举例:
正数:result = ~15
15 的二进制补码:0000 1111
取反后的补码为:1111 0000
反码为:1110 1111
原码为:1001 0000
结果:result = -16
4.2 按位与
以下面这段代码为例进行说明(可先尝试理解):
1 | // 验证前导码(0xDEADBEEF) |
这段代码的意思是:通过位运算将前 4 个字节的数据合并为一个 32 位的整数。
bytes[0] & 0xFF:将第一个字节的符号位清零,并保留低 8 位。原因:Java 中的 Byte 类型是 8 位有符号整数(-128 ~ 127),0xFF 的二进制表示为 1111 1111,但它在 Java 中会被视为一个 32 位的整数(int),值为 255。当单字节与 0xFF 进行按位与运算时,单字节也会被提升为 32 位的整数(int),在提升过程中,单字节的符号位会被复制到高位。比如 bytes[0] 为 1000 0001 时:
bytes[0](提升后):11111111 11111111 11111111 10000001
0xFF(32 位):00000000 00000000 00000000 11111111
按位与结果:00000000 00000000 00000000 10000001
也就是将单字节转换成了 8 位的无符号整数(0 ~ 255),避免负数问题。这里有一点需要重申,有符号整数在内存中始终以补码形式存储,但仅在算术运算中才涉及补码的转换(加、减、乘、除、取模、自增、自减)。位运算直接比较二进制位,不涉及数值计算,也不涉及数值的语义(符号、大小、进位),因此不涉及补码的转换,上文中的 1000 0001 可以理解为就是补码形式。
Java 在设计时为了简化语言,没有提供无符号类型,因此 byte、short、int、long、float、double 都是有符号的(包括它们的包装类型)。若需要无符号数,可通过位运算或 toUnsignedInt() 方法处理。
bytes[0] & 0xF0(高 4 位保留):提取字节的高 4 位(前 4 个二进制位),清零低 4 位。
bytes[0] & 0x0F(低 4 位保留):提取字节的低 4 位(后 4 个二进制位),清零高 4 位。
跟 0xF0 / 0x0F 按位与不能将有符号整数转换为无符号整数,因为它们在 Java 中不会被视为 32 位的整数(int),而是会被视为一个字节(byte)。举例:
bytes[0]:1101 0110
0xF0:1111 0000
按位与结果:1101 0000
bytes[0]:1101 0110
0x0F:0000 1111
按位与结果:0000 0110
这些掩码操作在嵌入式系统、网络协议、数据压缩和图像处理中尤为常见。