Java 移位运算详解:左移、右移和无符号右移怎么用

生活资讯31分钟前发布 jdkjadmin
0 00
Java 移位运算详解:左移、右移和无符号右移怎么用

Java 移位运算详解:左移、右移和无符号右移怎么用

Java 的移位运算符包括左移 <<、有符号右移 >> 和无符号右移 >>>。它们可以操作整数的二进制位,常见于位掩码、标志位、协议字段处理和底层数值计算。

理解移位运算,不能只记住“左移乘二、右移除二”。Java 对移位距离有特殊处理;有符号整数还涉及符号扩展;移位结果可能溢出;而 byte、short 等类型参与运算时也会发生类型提升。忽略这些规则,代码往往能编译,却得到与预期不同的结果。

一、三种移位运算有什么区别

运算符作用高位空出的部分
<<左移低位补 0
>>有符号右移(算术右移)复制原数的符号位
>>>无符号右移(逻辑右移)补 0

Java 的 byte、short、char、int 和 long 都是整数类型,但实际移位时,结果类型由左操作数决定。byte、short 和 char 会先提升为 int,所以对它们进行移位,表达式结果是 int。

例如,1 << 3 的结果是 8。以 32 位的 int 表示,左移会丢弃移出最高位的部分,并在右侧补零。最高位若因此变成 1,结果就会被解释为负数;移位本身不会抛出溢出异常。

二、左移:<<

左移将左操作数的二进制位向左移动指定的位数,右侧空出的位补 0。对于不发生溢出的非负数,左移一位相当于乘以 2,左移 n 位相当于乘以 2 的 n 次方。但这只是便于理解的关系,不应当作不会溢出的乘法替代品。

public class ShiftLeftDemo {
    public static void main(String[] args) {
        int a = 3;
        System.out.println(a << 1); // 6
        System.out.println(a << 2); // 12

        int highBit = 1 << 31;
        System.out.println(highBit); // -2147483648

        int wrapped = 1 << 32;
        System.out.println(wrapped); // 1
    }
}

在 32 位 int 中,1 << 31 会把最高位设为 1,因此结果为最小负数 -2147483648。1 << 32 则不是数学意义上的左移 32 位:Java 会先处理移位距离,后文会详细说明,最终它等价于 1 << 0。

当左移用于扩大数值、计算容量或构造掩码时,要先确认移位结果是否仍在目标类型的取值范围内。整数溢出通常不会自动报错,也不会自动扩展为 long。

三、右移:>> 与 >>>

1. 有符号右移 >>

>> 会将二进制位向右移动,高位用原来的符号位填充。正数的符号位为 0,负数的符号位为 1,因此右移后通常仍保持原来的正负号。

public class SignedRightShiftDemo {
    public static void main(String[] args) {
        System.out.println(12 >> 2);  // 3
        System.out.println(-12 >> 2); // -3

        System.out.println(-3 >> 1);  // -2
        System.out.println(-3 / 2);    // -1
    }
}

要特别注意,负数的算术右移不总是等于 Java 整数除法。-3 >> 1 的结果是 -2,而 -3 / 2 的结果是 -1。整数除法向零截断;算术右移的结果相当于对二的幂进行向负无穷取整。若业务要求与除法一致,应直接使用除法,或显式设计负数处理逻辑。

2. 无符号右移 >>>

>>> 也会把位向右移动,但高位始终补 0,不保留符号位。因此,负数经无符号右移后可能变成正数。

public class UnsignedRightShiftDemo {
    public static void main(String[] args) {
        int value = -1;

        System.out.println(value >> 1);  // -1
        System.out.println(value >>> 1); // 2147483647

        long longValue = -1L;
        System.out.println(longValue >>> 1); // 9223372036854775807
    }
}

-1 的二进制位全为 1。算术右移继续用 1 填充,所以仍为 -1;无符号右移在最高位补 0,所得位模式对应最大的正 int。这里的“无符号”描述的是移位填充规则,并不意味着 Java 的 int 或 long 变成了无符号类型:运算结果仍是相同的有符号类型。

四、移位距离的限制:最容易忽略的规则

Java 不会把移位距离按左操作数的位宽原样执行,而是只取距离的低几位:

  • int 左操作数:移位距离只取低 5 位,相当于 distance & 0x1f,范围是 0 到 31。
  • long 左操作数:移位距离只取低 6 位,相当于 distance & 0x3f,范围是 0 到 63。

例如,对 int 来说,移 32 位等同于移 0 位,移 33 位等同于移 1 位;对 long 来说,移 64 位等同于移 0 位。这项规则对负移位距离同样适用:负数不会让移位方向反转,而是按照低位取值。

public class ShiftDistanceDemo {
    public static void main(String[] args) {
        int x = 1;
        System.out.println(x << 0);   // 1
        System.out.println(x << 32);  // 1,距离按 32 & 31 得到 0
        System.out.println(x << 33);  // 2,距离按 33 & 31 得到 1
        System.out.println(x << -1);  // -2147483648,距离按 -1 & 31 得到 31

        long y = 1L;
        System.out.println(y << 64);  // 1,距离按 64 & 63 得到 0
        System.out.println(y << 65);  // 2
    }
}

上面的行为是 Java 语言规则,不是编译器错误。但在业务代码中,移位距离超出预期范围通常意味着参数校验缺失或逻辑错误。如果希望处理的是“任意长度的整数位移”,固定宽度的 int 和 long 并不会自动满足需求,可以考虑 BigInteger.shiftLeft 和 BigInteger.shiftRight;它们与基本类型的距离掩码规则不同,仍应按实际接口约定检查输入。

五、移位时的类型提升与结果类型

移位表达式中,左操作数决定结果类型。左操作数先执行一元数值提升:byte、short 和 char 提升为 int;int 保持为 int;long 保持为 long。右操作数也会进行一元数值提升,但它的类型不会把结果“扩大”为 long。

public class ShiftTypeDemo {
    public static void main(String[] args) {
        byte b = 1;
        int result = b << 2; // b 提升为 int,结果是 int
        System.out.println(result); // 4

        int i = 1;
        long stillInt = i << 40; // 左操作数是 int,结果仍是 int
        System.out.println(stillInt); // 256;距离按 40 & 31 得到 8

        long l = 1L;
        long longResult = l << 40; // 左操作数是 long,结果是 long
        System.out.println(longResult); // 1099511627776
    }
}

intValue << 40 不会因为右边写了 40,就自动变成 64 位运算。需要 long 结果时,应确保左操作数在移位前就是 long,例如使用 1L << n,或先将数值转换为 long。

同理,byte b = (byte) (b << 1) 中的显式强制转换可能截掉移位结果的高位。赋值给较窄类型时要确认这种截断是否符合需求,而不要仅因原变量是 byte 就认为移位仍按 8 位进行。

六、实用场景一:构造位掩码

位掩码常用于检查、设置或清除某个二进制位。假设一个 int 的第 n 位从最低位开始编号,最低位是第 0 位,可以用 1 << n 构造掩码。

public class BitMaskDemo {
    public static void main(String[] args) {
        int value = 0;

        // 设置第 3 位
        value |= 1 << 3;
        System.out.println(value); // 8

        // 检查第 3 位是否为 1
        boolean enabled = (value & (1 << 3)) != 0;
        System.out.println(enabled); // true

        // 清除第 3 位
        value &= ~(1 << 3);
        System.out.println(value); // 0

        // 翻转第 2 位
        value ^= 1 << 2;
        System.out.println(value); // 4
    }
}

如果位编号来自外部输入,应限制 n 的范围。对 int,通常要求 0 到 31;对 long,通常要求 0 到 63。否则,Java 的距离掩码规则会让越界值绕回到其他位。例如 1 << 32 实际得到 1,很可能导致检查或修改了错误的标志位。

七、实用场景二:将多个标志合并到一个整数

互相独立的布尔标志可以各占一位,通过按位或合并,再用按位与逐项检查。这适合标志数量有限、位的含义稳定且调用方清楚编码约定的场景。

public class FlagsDemo {
    private static final int READ  = 1 << 0;
    private static final int WRITE = 1 << 1;
    private static final int ADMIN = 1 << 2;

    public static void main(String[] args) {
        int permissions = READ | WRITE;

        boolean canRead = (permissions & READ) != 0;
        boolean canWrite = (permissions & WRITE) != 0;
        boolean isAdmin = (permissions & ADMIN) != 0;

        System.out.println(canRead);  // true
        System.out.println(canWrite); // true
        System.out.println(isAdmin);  // false
    }
}

位标志不是权限安全机制本身。它只是数据表示方式,不能替代服务端的身份认证、授权检查或可信数据验证。尤其不能仅凭客户端传来的位字段,就认定用户具有相应权限。

八、实用场景三:拆分和组合字段

在处理协议、文件格式或紧凑数据结构时,常需要从一个整数中提取指定范围的位。先右移到目标位,再与掩码按位与,是常见写法。若原数据可能为负数,选择 >> 还是 >>> 必须与字段的符号约定一致。

public class FieldDemo {
    public static void main(String[] args) {
        // 把两个 8 位值放入一个 int:高 8 位存 high,低 8 位存 low
        int high = 0x12;
        int low = 0x34;
        int packed = (high << 8) | low;

        int extractedHigh = (packed >>> 8) & 0xFF;
        int extractedLow = packed & 0xFF;

        System.out.printf("packed = 0x%08X%n", packed);
        System.out.printf("high = 0x%02X%n", extractedHigh);
        System.out.printf("low = 0x%02X%n", extractedLow);
    }
}

示例使用 >>> 和 0xFF,明确按无符号字节提取字段。真实协议处理还需要明确字节序、字段宽度、符号编码和输入长度。移位只处理整数位模式,不会自动处理网络字节序,也不会验证缓冲区是否包含足够的数据。

九、乘除法替代:何时可用,何时不该用

对非负整数,左移一位常可用于乘以 2,右移一位常可用于除以 2。但代码是否能这样替换,取决于溢出、负数、取整方式和业务语义。

  • x << n 只保留固定宽度的位模式;乘法可能溢出,但移位不会通知调用者发生了溢出。
  • 对负数,>> 与整数除法的取整方式可能不同,例如 -3 >> 1 为 -2,而 -3 / 2 为 -1。
  • 对 int,移位距离会限制在 0 到 31;因此任意传入的 n 并不等于数学上的乘以 2n。
  • 如果要检测乘法溢出,应使用适合需求的边界检查,或使用 Math.multiplyExact;若需要任意精度,可使用 BigInteger。

如果优化的前提是“被除数非负、移位距离受控、溢出不可能或已妥善处理”,才适合将移位作为明确的二进制计算写法。现代 Java 编译器和运行时通常会进行常见算术优化,不宜为了猜测性能而牺牲代码可读性;应以实际测量为准。

十、常见错误与排查方法

错误一:把无符号右移理解成无符号类型转换

>>> 只决定右移时高位补 0,不会改变变量类型。若后续仍以 int 解读同一位模式,显示和比较仍遵循有符号 int 的规则。需要按无符号数值解释时,可以使用 Integer.toUnsignedLong 或相应的无符号比较、转换方法。

错误二:以为右边是 long,结果就会变成 long

移位结果看左操作数。例如 1 << 40 的左侧是 int,所以结果仍是 int,移位距离也按 32 位规则处理。构造长整型掩码应写成 1L << n。

错误三:把越界移位距离当成非法参数

基本类型移位不会因为距离超过 31 或 63 就抛出异常,而是按低位取距离。这会掩盖上游错误。对于外部输入、数组位编号或可配置字段,应在移位前主动验证范围。

错误四:忘记左移可能使结果变负

左移只是在固定宽度位模式上操作。1 << 31 是负数;若将它作为掩码,判断逻辑应通过位运算表达,不要假定掩码必定大于零。

错误五:表达式优先级与预期不一致

加法优先级高于移位,位与、位或等运算又有各自的优先级。复杂表达式建议加括号,让数据流一目了然。例如 (value >>> offset) & mask 明确表达先右移、再取掩码字段;涉及混合位运算时,不要依赖读者记忆优先级。

错误六:把移位距离当作方向

负的移位距离不会将左移自动变成右移,也不会将右移自动变成左移。距离只按规则取低 5 位或低 6 位,负值可能因此对应一个看似合理、实际错误的位数。需要根据方向执行不同操作时,应显式选择运算符并验证距离。

十一、移位运算与安全性

移位通常是可预测的基础整数运算,但错误使用仍可能引起安全问题。例如,未校验的位编号可能令权限掩码或访问控制标志检查错位;计算长度、容量或偏移时发生溢出,可能造成边界检查失效;解析输入时忽略字段宽度,也可能丢失或错误解释数据。

编写安全相关代码时,应在移位前限制距离,并在移位后检查结果是否满足范围要求。计算数组长度、缓冲区大小和偏移时,不要只凭左移结果判断空间足够;需要安全的乘法检查时,可使用 Math.multiplyExact,并在边界检查中避免先发生整数溢出再比较。字段解析应使用明确的位宽掩码,并验证原始输入的长度和格式。

移位本身不提供加密、混淆或随机性。简单地对密码、令牌或敏感数据移位,不能构成安全保护;位运算也不自动保证常量时间执行。密码学操作应使用经过审查的标准算法与库,并遵守相应 API 的安全要求。

十二、可运行的综合示例

下面的程序覆盖三种移位、类型提升、距离掩码和位标志。保存为 ShiftSummary.java,使用 JDK 编译运行:

public class ShiftSummary {
    public static void main(String[] args) {
        int positive = 12;
        int negative = -12;

        System.out.println("12 << 1 = " + (positive << 1));
        System.out.println("-12 >> 1 = " + (negative >> 1));
        System.out.println("-12 >>> 1 = " + (negative >>> 1));

        System.out.println("1 << 32 = " + (1 << 32));
        System.out.println("1L << 32 = " + (1L << 32));

        int FLAG_A = 1 << 0;
        int FLAG_B = 1 << 1;
        int flags = FLAG_A | FLAG_B;

        System.out.println("A enabled = " + ((flags & FLAG_A) != 0));
        System.out.println("B enabled = " + ((flags & FLAG_B) != 0));

        byte b = 1;
        int promoted = b << 2;
        System.out.println("byte shift result type is int: " + promoted);
    }
}

编译并运行:

javac ShiftSummary.java
java ShiftSummary

总结

记住 Java 移位运算的四条核心规则,就能避免大多数问题:

  1. << 左移,低位补 0;>> 算术右移,复制符号位;>>> 逻辑右移,高位补 0。
  2. 结果类型由左操作数决定;byte、short 和 char 会先提升为 int。
  3. int 的距离只取低 5 位,long 的距离只取低 6 位;越界和负距离不会自动报错或反转方向。
  4. 移位不检查溢出,也不负责验证输入。用于掩码、字段、长度或安全判断时,应明确位宽、符号约定和参数范围。

对于普通算术,优先使用表达语义清楚的加、乘、除和取模;对于位字段与标志位,移位和按位运算则能清楚表达紧凑的二进制操作。关键是明确每一步操作的类型、位数和边界。

© 版权声明

相关文章