
Java移位运算符详解:<<、>> 和 >>> 有什么区别
Java 的移位运算符看起来简单:把一个整数向左或向右移动若干位。但实际使用时,符号扩展、移位距离的处理、类型提升和溢出常常让结果出乎意料。理解这些规则,才能正确地处理位字段、标志位、哈希计算和底层数据。
Java 提供三种移位运算符:
<<:左移。>>:带符号右移,也称算术右移。>>>:无符号右移,也称逻辑右移。
它们只作用于整数类型。理解它们的关键,是明确左操作数的类型、右操作数的有效移位距离,以及右移时如何填充空出来的高位。
一、从二进制看三种移位
Java 的整数采用固定宽度的二进制补码表示。一个 int 有 32 位,一个 long 有 64 位。以 8 位形式演示运算原理(Java 实际没有 8 位移位类型):
00001100 // 十进制 12
1. 左移:<<
<< 将各位向左移动,右侧空出的位用 0 填充,左侧移出去的位被丢弃:
00001100 << 2
00110000 // 十进制 48
在不发生溢出的情况下,左移 n 位的效果相当于乘以 2n。但这只是帮助理解的关系,不等于可以无条件将乘法替换为左移:固定宽度的整数溢出、负数和位宽限制都可能影响结果。
2. 带符号右移:>>
>> 将各位向右移动,左侧空出的位由原数的符号位填充。正数的符号位为 0,负数的符号位为 1。这样做可以保留数值的正负号。
00001100 >> 2
00000011 // 十进制 3
如果原数是负数,右移时左侧补 1。例如:
// 8 位示意;Java 的 int 实际是 32 位
11110000 >> 2
11111100
对非负整数而言,>> 的效果通常可以理解为向下取整地除以 2 的幂。对负数要特别谨慎:Java 的整数除法向零截断,而算术右移会进行符号扩展,两者结果不总相同。例如 -3 / 2 是 -1,而 -3 >> 1 是 -2。
3. 无符号右移:>>>
>>> 也向右移动,但左侧空出的位一律补 0,不保留符号位。它把相同的位模式当作不带符号的位串进行右移;运算结果的 Java 类型仍然是有符号的 int 或 long。
例如,int 的最高位为 1 时,该数作为 Java 有符号整数是负数。使用 >>> 后高位补 0,结果通常成为非负数;若使用 >>,高位则继续补 1。
Java 没有内置的无符号整数原始类型,但提供了 Integer.toUnsignedString、Integer.compareUnsigned 等方法来处理部分无符号整数需求。>>> 并不会改变变量类型,也不会自动把结果转换成某种无符号类型。
二、最重要的边界:移位距离会被屏蔽
许多语言对过大的移位距离有不同规则;Java 的规则是确定的:移位距离只取其低位,效果相当于对位宽取余。
- 左操作数经类型提升后为
int:只使用移位距离的低 5 位,即实际距离为s & 0x1f,范围是 0 到 31。 - 左操作数经类型提升后为
long:只使用移位距离的低 6 位,即实际距离为s & 0x3f,范围是 0 到 63。
因此,int 移位 32 位,实际等于移位 0 位;移位 33 位,实际等于移位 1 位。long 移位 64 位,实际同样等于移位 0 位。负数距离也不会触发异常,而是按低位规则处理。
public class ShiftDistanceDemo {
public static void main(String[] args) {
int value = 1;
System.out.println(value << 0); // 1
System.out.println(value << 1); // 2
System.out.println(value << 31); // -2147483648
System.out.println(value << 32); // 1:距离按 32 取模,等于 0
System.out.println(value << 33); // 2:距离按 32 取模,等于 1
System.out.println(value << -1); // -2147483648:-1 & 31 等于 31
}
}
这个规则容易造成严重的逻辑错误。例如,开发者可能以为 1 << 32 是一个包含第 32 位的掩码,实际结果却是 1。如果业务要求参数只能在某个范围内,应该显式校验,而不是依赖移位运算符帮忙限制距离。
三、类型提升:byte、short 和 char 会变成 int
Java 没有针对 byte、short 或 char 的独立移位结果类型。移位表达式会先对左操作数进行一元数值提升:
byte、short、char提升为int。int保持为int。long保持为long。
表达式的结果类型由提升后的左操作数决定,不由右操作数决定。右操作数可以是 int、long 等整数类型;它先进行一元数值提升,再按左操作数的宽度屏蔽移位距离。
byte b = 3;
int result = b << 2; // b 先提升为 int,结果类型是 int
long value = 1L;
long result2 = value << 40; // 左操作数是 long,结果类型是 long
下面的赋值会报错,因为移位结果是 int,不能直接赋给 byte:
byte b = 1;
// b = b << 1; // 编译错误:结果类型是 int
b = (byte) (b << 1); // 显式窄化转换
复合赋值是一个常见例外:Java 会在复合赋值中隐式进行相应的转换,因此下面的写法可以编译:
byte b = 1;
b <<= 1; // 等价于 b = (byte) (b << 1)
但“可以编译”不代表结果一定符合预期。窄化回 byte 时,高位可能被丢弃,数值也可能变成负数。需要保留较大范围时,应使用 int 或 long,而不是依靠复合赋值。
四、完整示例:观察符号扩展和零填充
以下程序可以直接保存为 ShiftDemo.java,使用 javac ShiftDemo.java 编译,再使用 java ShiftDemo 运行。它用二进制字符串展示 int 的 32 位结果。
public class ShiftDemo {
private static String bits(int value) {
String s = Integer.toBinaryString(value);
return "0".repeat(32 - s.length()) + s;
}
public static void main(String[] args) {
int positive = 12;
int negative = -12;
System.out.println("12 = " + bits(positive));
System.out.println("12 << 2 = " + bits(positive << 2));
System.out.println("12 >> 2 = " + bits(positive >> 2));
System.out.println("12 >>> 2= " + bits(positive >>> 2));
System.out.println();
System.out.println("-12 = " + bits(negative));
System.out.println("-12 >> 2 = " + bits(negative >> 2));
System.out.println("-12 >>> 2= " + bits(negative >>> 2));
System.out.println();
System.out.println("12 << 2 的十进制结果 = " + (positive << 2));
System.out.println("-12 >> 2 的十进制结果 = " + (negative >> 2));
System.out.println("-12 >>> 2 的十进制结果 = " + (negative >>> 2));
}
}
注意:Integer.toBinaryString 对负数返回的是其 32 位补码表示,不会在前面添加负号。因此,用 32 位字符串比较 >> 和 >>>,能直观看到高位填充方式的差别。
示例中的 String.repeat 从 Java 11 开始提供。如果使用更早版本,可自行编写补零循环,移位运算的规则不受影响。
五、常见适用场景
1. 位掩码和标志位
用不同的二进制位表示多个开关,是移位的常见用途。通常使用 1 << n 构造第 n 位掩码,再用按位与、按位或判断和设置标志。
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 canAdmin = (permissions & ADMIN) != 0;
permissions |= ADMIN; // 设置 ADMIN 位
permissions &= ~WRITE; // 清除 WRITE 位
System.out.println(canRead); // true
System.out.println(canAdmin); // false
System.out.println(permissions);
}
}
使用这类写法时,要确认位编号合法,并选用足够宽的类型。对 int,可用于单个位掩码的位编号是 0 到 31;对 long,是 0 到 63。尤其是编号为 31 的 int 掩码,1 << 31 的结果是负数,因为最高位是符号位。它仍然可以作为位掩码使用,但不能把它当作非负数大小来比较。
2. 从整数中提取或组合字段
如果多个小字段按约定打包在一个整数中,可以通过右移和掩码取出字段,也可以通过左移后按位或组合字段。以下示例把三个字节打包成一个 int:
int a = 0x12;
int b = 0x34;
int c = 0x56;
int packed = (a & 0xff) << 16
| (b & 0xff) << 8
| (c & 0xff);
int extractedA = (packed >>> 16) & 0xff;
int extractedB = (packed >>> 8) & 0xff;
int extractedC = packed & 0xff;
这里先用 & 0xff 保留低 8 位,再通过移位放到目标位置。提取时使用 >>>,并再次与 0xff,明确只保留目标字段。若输入来自有符号的 byte,掩码尤其重要:直接扩展到 int 时,负数会进行符号扩展。
3. 除以二的幂或构造快速索引
对非负整数,将其右移 k 位,等效于整除 2k。这有时用于分桶或提取高位索引。但不要仅凭“移位更快”就替换普通乘除法:现代 Java 虚拟机和处理器会优化常见运算,实际性能需要针对真实代码进行测量;而负数、溢出和边界处理可能使替换改变程序含义。
左移也可用于构造以 2 的幂为单位的值或索引,但必须检查移位距离和溢出。溢出不会抛出异常,结果只保留固定宽度内的位。
六、常见错误和容易忽略的规则
错误一:把大于等于位宽的距离当成零结果
Java 不会因为 int 移位 32 位就得到 0,也不会因为移位距离过大就抛出异常。距离会屏蔽低 5 位或低 6 位。若程序接收外部提供的位编号,必须在移位前验证范围。
错误二:以为负数左移或乘法溢出会报错
移位和普通整数运算一样采用固定宽度结果。被移出有效位宽的位会丢弃,不会自动扩展到 long,也不会抛出算术溢出异常。想以 long 运算,应在移位前确保左操作数是 long:
int n = 31;
int a = 1 << n; // int 运算,结果为负数
long b = 1L << n; // long 运算,结果为正数
只把结果赋给 long 并不能挽回已经发生的 int 溢出:
long wrong = 1 << 31; // 先执行 int 移位,再扩展到 long
long right = 1L << 31; // 从一开始就在 long 中移位
错误三:混淆右移运算符
处理普通有符号数值时,通常考虑 >>;需要把位串右移并让高位补零时,通常考虑 >>>。例如解码一个可能含最高位的打包整数,使用无符号右移再掩码往往更容易表达意图。不要只看运算结果是否为正就判断运算符正确与否,还应明确要保留的是符号语义还是位模式。
错误四:忽略运算优先级
移位运算的优先级低于加法和减法,高于关系运算。因此:
int x = value >> 1 + 2;
会按 value >> (1 + 2) 解析,也就是右移 3 位,而不是先右移 1 位再加 2。复杂表达式应使用括号明确意图:
int a = value >> (1 + 2);
int b = (value >> 1) + 2;
错误五:误以为 >>> 能让数值变成无符号类型
>>> 只规定右移时高位补 0。结果仍然是 int 或 long,后续比较、打印和运算仍受 Java 有符号类型规则影响。需要无符号解释时,应使用对应的标准库方法,或明确通过掩码转换到更宽的类型。
七、安全使用:校验输入,也不要把移位当作加密
移位可以用于解析协议字段、处理权限标志和构造索引,但不能代替边界校验。假设代码用 1 << index 检查某个标志,如果 index 来自用户输入,超范围的值会按照 Java 的距离屏蔽规则折返到另一个位,并不会自动失败。应先检查,例如要求 0 <= index && index < 32,再执行 int 移位。
位运算也不能自动消除溢出风险。若移位结果用于数组长度、缓冲区大小、权限判断或资源分配,应先验证输入范围,并在合适时使用更宽类型或显式溢出检查。仅仅先转成 long 也不意味着永远安全,因为 long 同样是固定宽度类型。
不要把 <<、>> 或 >>> 当作加密、保密或安全散列方案。位移本身是可逆或信息丢失的简单位操作,不提供密码学安全性。即便某些密码学算法内部使用位运算,也不能据此自行拼装加密算法;应使用经过审查的标准算法和 Java 密码学 API。
如果移位出现在密码学或安全敏感代码中,也不能仅凭代码中没有分支就断定执行时间恒定。JIT 编译器、处理器和周边数据访问都可能影响时序特征;是否具备抗侧信道能力,需要针对具体实现和环境分析。
八、快速判断口诀
- 左移
<<:低位补 0,高位丢弃;可能溢出。 - 带符号右移
>>:高位补符号位,适合保留有符号数的符号扩展语义。 - 无符号右移
>>>:高位补 0,适合按位模式进行逻辑右移。 int的实际移位距离为s & 31;long为s & 63。byte、short、char左操作数会提升为int;结果类型由提升后的左操作数决定。- 超范围距离不会自动报错。业务范围需要自己校验,运算结果也不会自动检测溢出。
写移位表达式时,先问自己三个问题:左操作数到底是 int 还是 long?右移后高位应该保留符号还是补零?移位距离和结果范围是否经过验证?把这三点想清楚,大多数 Java 移位相关的问题就能避免。




