从二进制看懂 Java 移位运算:`<<`、`>>` 与 `>>>` 的区别

资讯22分钟前发布 jdkjadmin
0 00
从二进制看懂 Java 移位运算:`<<`、`>>` 与 `>>>` 的区别

从二进制看懂 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 提取。

真正使用移位运算时,先确认左操作数的位宽,再确认符号位是否需要保留、移位距离是否有效,最后检查溢出和掩码范围。把这几件事想清楚,移位表达式就不再只是看起来神秘的符号组合。

© 版权声明

相关文章