
从二进制看懂 Java 移位运算:<<、>> 与 >>> 的区别
Java 的移位运算符常用于位标志、协议解析和高效计算,但它们处理的是整数的二进制位,不是抽象的“乘除法快捷方式”。尤其要注意:Java 整数是有符号的,右移有两种规则,移位距离会被截取,而且左移可能丢失高位。
本文从固定宽度的二进制表示出发,逐步解释 <<、>> 和 >>> 的区别、参数边界与常见错误,并提供可直接运行的 Java 示例。
先看懂整数的二进制位
Java 的 int 占 32 位,long 占 64 位。两者都使用二进制补码表示有符号整数:最高位是符号位,最高位为 0 时表示非负数,为 1 时表示负数。比如,一个 8 位示意数的二进制 00000101 表示十进制 5;11111101 按补码规则表示十进制 -3。
真实的 Java byte 虽然占 8 位,但执行移位表达式时会先提升为 int。因此,理解移位时主要看左操作数提升后的 int 或 long 位宽:32 位或 64 位。
三种移位运算符分别做什么
左移:<<
左移将所有位向左移动指定位置,右侧空出来的位补 0,超出类型位宽的高位直接丢弃。它不保留符号位,也不会因为溢出而抛出异常。
int x = 5; // ...00000101
int y = x << 2; // ...00010100,也就是 20
在没有发生溢出的特定范围内,x << n 的结果等于 x * 2^n。但这只是理解上的关系,不代表左移具有溢出检查。若结果超出 int 或 long 可表示范围,高位会被丢弃,结果按剩下的位重新解释。
int a = 1 << 30; // 1073741824
int b = 1 << 31; // -2147483648,最高位变成 1
int c = 1 << 32; // 1,移位距离按规则处理为 0
算术右移:>>
算术右移将位向右移动,左侧空出来的位置用原来的符号位填充。非负数补 0,负数补 1,因此结果通常仍保持原来的正负号。
int positive = 20; // ...00010100
int p = positive >> 2; // ...00000101,也就是 5
int negative = -20;
int n = negative >> 2; // -5
对于正数,右移 n 位通常相当于除以 2^n 取整数。但对负数要小心:算术右移的效果相当于向负无穷取整,而 Java 整数除法向 0 截断。例如:
System.out.println(-3 >> 1); // -2
System.out.println(-3 / 2); // -1
因此,不能在所有输入上把 >> 当成整数除法的替代写法。
逻辑右移:>>>
逻辑右移同样将位向右移动,但无论原数正负,左侧空位一律补 0。这适用于按无符号位模式处理数据的场景。
int x = -1; // 32 位全为 1
System.out.println(x >> 1); // -1,左侧补 1
System.out.println(x >>> 1); // 2147483647,左侧补 0
Java 没有 unsigned int 这种基本类型。>>> 不会把变量类型变成无符号类型,它只是改变右移时的补位规则;表达式结果仍然是 int 或 long。如果要按无符号数进行比较或除法,还要使用相应的 API,例如 Integer.compareUnsigned 或 Integer.divideUnsigned。
最关键的边界:移位距离会被截取
Java 并不会因为移位距离过大或为负数而报错。实际使用的移位距离只取其低若干位:
- 左操作数是
int时,只使用移位距离的低 5 位,相当于对 32 取模。 - 左操作数是
long时,只使用移位距离的低 6 位,相当于对 64 取模。
因此,int 左移 32 位和左移 0 位效果相同;long 左移 64 位和左移 0 位效果相同。负距离也会按同一规则处理,并不是“向相反方向移动”。
System.out.println(1 << 0); // 1
System.out.println(1 << 32); // 1,32 的低 5 位为 0
System.out.println(1 << -1); // -2147483648,-1 的低 5 位为 31
System.out.println(1L << 64); // 1,64 的低 6 位为 0
System.out.println(1L << -1); // 负数,实际左移 63 位
这条规则容易让错误悄悄发生。例如,程序员本想用 1 << 32 构造第 33 个标志位,结果却得到 1。构造 int 的位掩码时,可用位置范围是 0 到 31;构造 long 的位掩码时,可用位置范围是 0 到 63。对外部输入的移位距离,应先按业务规则校验,而不是依赖语言自动截取。
操作数类型:为什么 byte 移位结果是 int
移位表达式会对左右操作数分别进行一元数值提升。左操作数的提升后类型决定结果类型:通常是 int 或 long。所以对 byte、short、char 移位,结果是 int;对 long 移位,结果是 long。
byte b = 1;
// byte result = b << 1; // 编译错误:表达式结果是 int
int result = b << 1;
如果确实需要放回较窄的类型,必须显式转换。但强制转换会丢弃超出目标类型宽度的位,使用前要确认这是预期行为。
还有一个常见问题:byte 为负数时会先符号扩展成 int。例如 (byte) 0xFF 的值是 -1,提升为 int 后是 32 个 1。处理字节中的无符号数值时,通常要先屏蔽高位:
byte b = (byte) 0xFF;
int unsignedValue = b & 0xFF;
System.out.println(unsignedValue); // 255
适用场景一:构造和检查位标志
位标志适合表示一组数量有限、彼此独立的开关状态。每个选项占一个二进制位,可以用按位或组合,用按位与检查。
public class FlagsExample {
static final int READ = 1 << 0;
static final int WRITE = 1 << 1;
static final int EXECUTE = 1 << 2;
public static void main(String[] args) {
int permissions = READ | WRITE;
boolean canRead = (permissions & READ) != 0;
boolean canWrite = (permissions & WRITE) != 0;
boolean canExecute = (permissions & EXECUTE) != 0;
System.out.println(canRead); // true
System.out.println(canWrite); // true
System.out.println(canExecute); // false
permissions |= EXECUTE; // 设置 EXECUTE 位
permissions &= ~WRITE; // 清除 WRITE 位
permissions ^= READ; // 翻转 READ 位
}
}
定义标志时要保证移位位置不越界,也不要让不同标志占用同一位。若需要 32 个标志,使用 int 时可以覆盖位 0 到位 31,但位 31 对应的掩码是负数;这并不妨碍位测试,只要把它当作位模式使用即可。超过 32 个标志时,可以考虑 long、BitSet 或其他更适合的数据结构。
适用场景二:解析和组合二进制字段
网络协议、文件格式或设备数据常把多个字段放在一个整数中。读取字段时,可以先右移,再用掩码保留目标位;写入字段时,则先清除对应区域,再放入新值。
public class FieldExample {
public static void main(String[] args) {
int packed = 0x12345678;
int highByte = (packed >>> 24) & 0xFF;
int secondByte = (packed >>> 16) & 0xFF;
System.out.printf("%02X%n", highByte); // 12
System.out.printf("%02X%n", secondByte); // 34
int value = 0x12345678;
int shift = 8;
int mask = 0xFF << shift;
int replacement = 0xAB;
int updated = (value & ~mask) | ((replacement & 0xFF) << shift);
System.out.printf("%08X%n", updated); // 1234AB78
}
}
解析无符号字节时,>>> 可以避免符号扩展带来的影响;与 0xFF 做按位与,则能确保只取出 8 位。实际协议还可能规定字节序、字段长度和符号编码,移位运算不会替你处理这些约定,必须按协议定义实现。
哪些情况下不能把移位当成乘除法
常见说法是“左移等于乘 2,右移等于除 2”,但它有明确前提,不能不加判断地用于业务计算。
- 左移可能溢出:Java 的整数运算不会自动检查溢出。溢出后结果可能改变符号,甚至变成 0。需要溢出检测时,应使用范围检查或
Math.multiplyExact等方法,而不是用移位代替。 - 负数右移和除法不同:
-3 >> 1得到 -2,而-3 / 2得到 -1。需要 Java 除法的截断规则时,应直接使用除法。 >>>不是有符号除法:对负数逻辑右移会补 0,得到的数值可能变成很大的正数。- 移位距离可能被静默截取:距离超过位宽或为负数时不会抛异常,可能导致结果与预期完全不同。
若是在经过证明的范围内,将非负整数除以 2 的幂,右移可以表达清楚的位操作意图;若是在进行一般数值运算,尤其涉及负数、溢出或边界值,使用乘除运算通常更直观,也更容易审查。
常见错误与排查方式
错误一:把 1 << 32 当成第 32 位
1 是 int,移位距离只看低 5 位,所以 1 << 32 实际等同于 1 << 0。若需要构造 long 的掩码,应从 1L 开始,例如 1L << 32。
错误二:以为 >>> 会改变变量的类型
>>> 只改变补位方式,表达式的类型仍由左操作数决定。一个负的 int 经过逻辑右移后,依然是 int;需要无符号比较或转换时,应选用对应的库方法。
错误三:忽略小整数的提升和符号扩展
byte、short 移位前会提升为 int,负值会符号扩展。读取二进制字节时,如果希望得到 0 到 255 的值,应使用 b & 0xFF,不要直接把 byte 转成 int 就假设结果是无符号值。
错误四:忽略运算符优先级
移位运算的优先级低于加减法。因此,1 << 2 + 1 解析为 1 << (2 + 1),不是 (1 << 2) + 1。复杂表达式建议加括号,把移位距离、掩码和组合步骤写清楚。
完整可运行示例
下面的程序将常见规则集中在一起。保存为 ShiftDemo.java,然后使用 javac ShiftDemo.java 编译,并运行 java ShiftDemo。
public class ShiftDemo {
public static void main(String[] args) {
int five = 5;
System.out.println("5 << 2 = " + (five << 2));
int minusTwenty = -20;
System.out.println("-20 >> 2 = " + (minusTwenty >> 2));
System.out.println("-20 >>> 2 = " + (minusTwenty >>> 2));
System.out.println("-3 >> 1 = " + (-3 >> 1));
System.out.println("-3 / 2 = " + (-3 / 2));
System.out.println("1 << 32 = " + (1 << 32));
System.out.println("1L << 32 = " + (1L << 32));
System.out.println("1 << -1 = " + (1 << -1));
byte b = (byte) 0xFF;
System.out.println("byte as int = " + b);
System.out.println("unsigned byte = " + (b & 0xFF));
}
}
运行时可以重点观察四件事:负数使用 >> 会保留符号位;使用 >>> 会在左侧补 0;int 与 long 的有效移位距离规则不同;负的移位距离不会报错,而是按低位规则转换为实际距离。
移位运算与安全性
移位运算本身不是安全机制。它可以用于位标志、字段提取和数据编码,但不能替代输入校验、权限校验、加密或安全随机数生成。移位也不会自动检查整数溢出,处理不可信输入时,尤其要校验字段范围和移位距离,避免截断规则让错误值绕过预期逻辑。
使用移位处理协议数据时,还应明确类型宽度、字节序、字段掩码和符号解释,并用边界测试覆盖 0、最大距离、负数以及最高位被置位等情况。若代码涉及敏感数据或安全协议,不要自行设计位级加密方案;位移和异或操作本身并不意味着具有密码学安全性。
总结
<<:向左移位,右侧补 0,高位丢弃;溢出不会自动报错。>>:算术右移,左侧用符号位填充,适合保留有符号解释的位移。>>>:逻辑右移,左侧补 0,常用于按无符号位模式处理整数。int的移位距离只取低 5 位,long只取低 6 位;过大或负距离都不会抛出异常。byte、short、char移位前会提升为int;无符号字节通常需用& 0xFF提取。
真正使用移位运算时,先确认左操作数的位宽,再确认符号位是否需要保留、移位距离是否有效,最后检查溢出和掩码范围。把这几件事想清楚,移位表达式就不再只是看起来神秘的符号组合。





