"大翔在飞翔科技架构评审会上画了一个权限表:'读=4,写=2,执行=1——这就是位运算。一个字节8个位,可以同时控制8种权限,比建8张表高效一万倍。'白歌补充道:'而且
n << 3比n * 8快一个数量级,虽然现代JVM会自动优化,但理解底层原理才能写出真正高性能的代码。'黄俪翻开笔记本:'原来无符号右移>>>是做HashMap哈希扰动的不二法门。'"
位运算符
概述
位运算符直接操作整数类型的二进制位(bit)。Java SE 8 提供7种位运算符,涵盖按位逻辑运算和位移运算。位运算在权限控制、高效乘除、哈希算法、数据压缩、网络协议解析等底层场景中不可或缺。
位运算符仅适用于整数类型:byte、short、int、long、char。操作前,byte/short/char 会被自动提升为 int。
操作符行为表
按位逻辑运算符
| 操作符 | 含义 | 运算规则 | 示例(int a=5(0101), b=3(0011)) | 结果 |
|---|---|---|---|---|
& | 按位与 | 两位都为1则得1,否则得0 | a & b → 0101 & 0011 | 1(0001) |
| | 按位或 | 两位任意一个为1则得1 | a | b → 0101 | 0011 | 7(0111) |
^ | 按位异或 | 两位不同得1,相同得0 | a ^ b → 0101 ^ 0011 | 6(0110) |
~ | 按位取反 | 1变0,0变1(含符号位) | ~a → ~0101 | -6(补码表示) |
位移运算符
| 操作符 | 含义 | 运算规则 | 示例(int a=8(1000)) | 结果 |
|---|---|---|---|---|
<< | 左移 | 所有位向左移n位,右侧补0 | a << 2 | 32 |
>> | 带符号右移 | 所有位向右移n位,左侧补符号位 | -8 >> 2 | -2 |
>>> | 无符号右移 | 所有位向右移n位,左侧补0 | -8 >>> 2 | 1073741822 |
深度原理分析
一、补码(Two's Complement)——位运算的数学基础
Java中所有整数类型都使用补码表示负数。理解补码是驾驭位运算的前提。
补码的求法(以8位为例):
- 写出绝对值的原码
- 按位取反(得到反码 / one's complement)
- 加1(得到补码 / two's complement)
求 -5 的8位补码表示:
+5 原码: 0000 0101
按位取反: 1111 1010 (反码)
加1: 1111 1011 (补码 = -5)
验证: 5 + (-5) = 0
0000 0101 (+5)
+ 1111 1011 (-5)
------------
1 0000 0000 → 溢出丢弃进位 → 0000 0000 = 0 ✓
为什么要用补码? 补码使得加法和减法可以用同一套电路完成——A - B 就是 A + (-B的补码),硬件只需加法器。
二、按位逻辑运算的逐位详解
以8位为例,设 a = 0b0110_1101 (109),b = 0b1011_0010 (178):
按位与 &(同1则1):
0110 1101 (109)
& 1011 0010 (178)
-------------
0010 0000 (32) ← 用于"掩码":提取特定位
按位或 |(有1则1):
0110 1101 (109)
| 1011 0010 (178)
-------------
1111 1111 (255) ← 用于"置位":将特定位设为1
按位异或 ^(不同则1):
0110 1101 (109)
^ 1011 0010 (178)
-------------
1101 1111 (223) ← 用于"翻转":切换特定位
按位取反 ~(1→0, 0→1):
~ 0110 1101 (109)
-------------
1001 0010 (-110) ← 包括符号位一起取反
三、位移运算的低层机制
关键区别——>> vs >>>:
int n = -8; // 二进制(int 32位): 11111111 11111111 11111111 11111000
// >> 带符号右移:高位补1(符号位),结果仍是负数
System.out.println(n >> 2); // -2 (11111111...11111110)
// >>> 无符号右移:高位补0,结果变成很大的正数
System.out.println(n >>> 2); // 1073741822 (00111111...11111110)
位移的模运算规则:当位移量超过类型位数时,Java会取模。对于 int(32位),n << 35 实际执行 n << (35 % 32) = n << 3。对于 long(64位),取模64。
四、位运算的高效乘除
在硬件层面,位移比乘法/除法快得多(尽管现代JVM的JIT编译器会自动将 n * 8 优化为 n << 3,但理解这一原理对性能调优至关重要):
| 数学运算 | 位运算等价 | 前提条件 |
|---|---|---|
n × 2^k | n << k | 无溢出风险 |
n ÷ 2^k(正数) | n >> k | n为非负数 |
n % 2^k | n & (2^k - 1) | 2^k 次幂取模 |
n × 10 | (n << 3) + (n << 1) | 通用优化 |
int n = 100;
System.out.println(n << 3); // 100 * 8 = 800
System.out.println(n >> 2); // 100 / 4 = 25
System.out.println(n & 15); // 100 % 16 = 4 (因为 15 = 0b1111)
完整示例一:飞翔科技权限控制系统
场景
大翔为飞翔科技内部系统设计了基于位运算的权限控制——一个 int 可以同时管理8种权限,比数据库关联表方案高效数万倍。
/**
* 飞翔科技权限控制系统 —— 位运算实战
*
* 权限模型(每个权限占一个bit):
* bit 0 (值=1): 查看员工信息
* bit 1 (值=2): 编辑员工信息
* bit 2 (值=4): 删除员工信息
* bit 3 (值=8): 查看薪资
* bit 4 (值=16): 编辑薪资
* bit 5 (值=32): 系统管理
* bit 6 (值=64): 审计日志
* bit 7 (值=128): 超级管理员
*/
public class PermissionSystem {
// 权限常量(2的幂次)
static final int PERM_VIEW_EMPLOYEE = 1; // 0000 0001
static final int PERM_EDIT_EMPLOYEE = 2; // 0000 0010
static final int PERM_DELETE_EMPLOYEE = 4; // 0000 0100
static final int PERM_VIEW_SALARY = 8; // 0000 1000
static final int PERM_EDIT_SALARY = 16; // 0001 0000
static final int PERM_SYSTEM_ADMIN = 32; // 0010 0000
static final int PERM_AUDIT_LOG = 64; // 0100 0000
static final int PERM_SUPER_ADMIN = 128; // 1000 0000
public static void main(String[] args) {
System.out.println("===== 飞翔科技权限系统 feixiang.net =====\n");
// --- 白歌(架构师):拥有 查看+编辑+查看薪资+编辑薪资 权限 ---
int baiGePermissions = PERM_VIEW_EMPLOYEE | PERM_EDIT_EMPLOYEE
| PERM_VIEW_SALARY | PERM_EDIT_SALARY;
System.out.println("白歌的权限编码: " + baiGePermissions);
System.out.println("二进制: " + toBinary8(baiGePermissions));
// --- 权限检查:使用 & (按位与)---
System.out.println("\n--- 权限查询 ---");
System.out.println("白歌能否查看员工? "
+ ((baiGePermissions & PERM_VIEW_EMPLOYEE) != 0));
System.out.println("白歌能否删除员工? "
+ ((baiGePermissions & PERM_DELETE_EMPLOYEE) != 0));
System.out.println("白歌是否超级管理员? "
+ ((baiGePermissions & PERM_SUPER_ADMIN) != 0));
// --- 授予权限:使用 | (按位或)---
System.out.println("\n--- 授予新权限 ---");
System.out.println("授予白歌 '审计日志' 权限...");
baiGePermissions |= PERM_AUDIT_LOG; // 等价于 baiGePermissions = baiGePermissions | PERM_AUDIT_LOG
System.out.println("新权限编码: " + baiGePermissions);
System.out.println("二进制: " + toBinary8(baiGePermissions));
System.out.println("白歌能否查看审计日志? "
+ ((baiGePermissions & PERM_AUDIT_LOG) != 0));
// --- 撤销权限:使用 & 和 ~ ---
System.out.println("\n--- 撤销权限 ---");
System.out.println("撤销白歌 '编辑薪资' 权限...");
baiGePermissions &= ~PERM_EDIT_SALARY; // 将对应位清零
System.out.println("新权限编码: " + baiGePermissions);
System.out.println("二进制: " + toBinary8(baiGePermissions));
System.out.println("白歌能否编辑薪资? "
+ ((baiGePermissions & PERM_EDIT_SALARY) != 0));
// --- 切换权限:使用 ^ (按位异或)---
System.out.println("\n--- 切换权限(有→无,无→有)---");
System.out.println("切换白歌 '系统管理' 权限(首次:添加)...");
baiGePermissions ^= PERM_SYSTEM_ADMIN;
System.out.println("编码: " + baiGePermissions + ", 二进制: " + toBinary8(baiGePermissions));
System.out.println("白歌是否系统管理员? "
+ ((baiGePermissions & PERM_SYSTEM_ADMIN) != 0));
System.out.println("再次切换 '系统管理' 权限(二次:移除)...");
baiGePermissions ^= PERM_SYSTEM_ADMIN;
System.out.println("编码: " + baiGePermissions + ", 二进制: " + toBinary8(baiGePermissions));
System.out.println("白歌是否系统管理员? "
+ ((baiGePermissions & PERM_SYSTEM_ADMIN) != 0));
// --- 大翔:超级管理员(拥有所有权限)---
System.out.println("\n--- 大翔:超级管理员 ---");
int daXiangPermissions = PERM_VIEW_EMPLOYEE | PERM_EDIT_EMPLOYEE
| PERM_DELETE_EMPLOYEE | PERM_VIEW_SALARY
| PERM_EDIT_SALARY | PERM_SYSTEM_ADMIN
| PERM_AUDIT_LOG | PERM_SUPER_ADMIN;
System.out.println("大翔的权限编码: " + daXiangPermissions);
System.out.println("二进制: " + toBinary8(daXiangPermissions));
// 检查是否拥有所有权限
if (daXiangPermissions == 255) { // 8位全1 = 0b1111_1111 = 255
System.out.println(" 全部8项权限已开通 √");
}
}
/** 将int转为8位二进制字符串,便于观察 */
static String toBinary8(int n) {
String s = Integer.toBinaryString(n);
// 补零到8位
while (s.length() < 8) s = "0" + s;
// 如果超过8位,只取低8位
if (s.length() > 8) s = s.substring(s.length() - 8);
return s;
}
}
运行输出:
===== 飞翔科技权限系统 feixiang.net =====
白歌的权限编码: 27
二进制: 00011011
--- 权限查询 ---
白歌能否查看员工? true
白歌能否删除员工? false
白歌是否超级管理员? false
--- 授予新权限 ---
授予白歌 '审计日志' 权限...
新权限编码: 91
二进制: 01011011
白歌能否查看审计日志? true
--- 撤销权限 ---
撤销白歌 '编辑薪资' 权限...
新权限编码: 75
二进制: 01001011
白歌能否编辑薪资? false
--- 切换权限(有→无,无→有)---
切换白歌 '系统管理' 权限(首次:添加)...
编码: 107, 二进制: 01101011
白歌是否系统管理员? true
再次切换 '系统管理' 权限(二次:移除)...
编码: 75, 二进制: 01001011
白歌是否系统管理员? false
--- 大翔:超级管理员 ---
大翔的权限编码: 255
二进制: 11111111
全部8项权限已开通 √
权限控制核心模式总结
完整示例二:位运算高效计算
场景
黄俪在优化飞翔商城的批量价格计算时,用位运算替代乘除法,大幅提升计算效率。
/**
* 飞翔商城 —— 位运算高效计算
* 演示 <<、>>、>>>、& 的数学等价用法
*/
public class EfficientCalculation {
public static void main(String[] args) {
int productPrice = 189; // 商品单价(元)
int quantity = 64; // 购买数量(故意取2的幂便于演示)
System.out.println("===== 飞翔商城高效计算 feixiang.net =====\n");
// --- 左移 << : 等价于乘以 2^k ---
System.out.println("--- 左移(乘法优化)---");
System.out.println(quantity + " × 2 = " + (quantity << 1)); // 64 × 2 = 128
System.out.println(quantity + " × 4 = " + (quantity << 2)); // 64 × 4 = 256
System.out.println(quantity + " × 8 = " + (quantity << 3)); // 64 × 8 = 512
System.out.println(quantity + " × 16 = " + (quantity << 4)); // 64 × 16 = 1024
// --- 右移 >> : 等价于除以 2^k(正整数)---
System.out.println("\n--- 带符号右移(除法优化)---");
int totalAmount = 1024;
System.out.println(totalAmount + " ÷ 2 = " + (totalAmount >> 1)); // 512
System.out.println(totalAmount + " ÷ 4 = " + (totalAmount >> 2)); // 256
System.out.println(totalAmount + " ÷ 8 = " + (totalAmount >> 3)); // 128
System.out.println(totalAmount + " ÷ 16 = " + (totalAmount >> 4)); // 64
// --- 取模优化:n & (2^k - 1) 等价于 n % 2^k ---
System.out.println("\n--- 按位与取模(n % 2^k 优化)---");
int n = 189;
System.out.println(n + " % 8 = " + (n % 8) + "(常规写法)");
System.out.println(n + " & 7 = " + (n & 7) + "(位运算写法,7=0b111)");
System.out.println(n + " % 16 = " + (n % 16));
System.out.println(n + " & 15 = " + (n & 15) + "(15=0b1111)");
// --- 奇偶判断:n & 1 ---
System.out.println("\n--- 奇偶判定(n & 1)---");
System.out.println("189 & 1 = " + (189 & 1) + " → 奇数");
System.out.println("188 & 1 = " + (188 & 1) + " → 偶数");
System.out.println("(比 n % 2 快,JVM自动优化后两者无差别,但理解原理重要)");
// --- 无符号右移 >>> : 处理大数 ---
System.out.println("\n--- 无符号右移 >>> ---");
int negative = -189;
System.out.println("-189 的二进制: " + Integer.toBinaryString(negative));
System.out.println("-189 >> 2 = " + (negative >> 2)
+ " 二进制: " + Integer.toBinaryString(negative >> 2));
System.out.println("-189 >>> 2 = " + (negative >>> 2)
+ " 二进制: " + Integer.toBinaryString(negative >>> 2));
System.out.println("(>>> 高位补0,结果变成很大的正数)");
// --- 交换两个整数(不使用临时变量)---
System.out.println("\n--- 异或交换两数(无临时变量)---");
int a = 188, b = 88;
System.out.println("交换前: a=" + a + ", b=" + b);
// 三步异或完成交换
a = a ^ b; // a = 188^88
b = a ^ b; // b = (188^88)^88 = 188
a = a ^ b; // a = (188^88)^188 = 88
System.out.println("交换后: a=" + a + ", b=" + b);
System.out.println("(利用性质: a^b^a = b, a^b^b = a)");
}
}
运行输出:
===== 飞翔商城高效计算 feixiang.net =====
--- 左移(乘法优化)---
64 × 2 = 128
64 × 4 = 256
64 × 8 = 512
64 × 16 = 1024
--- 带符号右移(除法优化)---
1024 ÷ 2 = 512
1024 ÷ 4 = 256
1024 ÷ 8 = 128
1024 ÷ 16 = 64
--- 按位与取模(n % 2^k 优化)---
189 % 8 = 5(常规写法)
189 & 7 = 5(位运算写法,7=0b111)
189 % 16 = 13
189 & 15 = 13(15=0b1111)
--- 奇偶判定(n & 1)---
189 & 1 = 1 → 奇数
188 & 1 = 0 → 偶数
(比 n % 2 快,JVM自动优化后两者无差别,但理解原理重要)
--- 无符号右移 >>> ---
-189 的二进制: 11111111111111111111111101000011
-189 >> 2 = -48 二进制: 11111111111111111111111111010000
-189 >>> 2 = 1073741776 二进制: 11111111111111111111111010000
(>>> 高位补0,结果变成很大的正数)
--- 异或交换两数(无临时变量)---
交换前: a=188, b=88
交换后: a=88, b=188
(利用性质: a^b^a = b, a^b^b = a)
易错场景
反例一:>> 用于负数得到意外结果
// ❌ 错误认知:以为 >> 就是简单除以 2^n
int n = -8;
System.out.println(n >> 2); // -2(正确,-8/4=-2)
int m = -5;
System.out.println(m >> 1); // -3(不是 -2!因为向负无穷取整)
// -5 >> 1 = -3, 但 -5/2 = -2(向零取整)
原因:>> 对于负数不是简单的除法——它是向负无穷方向取整(floor division),而Java整数除法是向零取整。因此 -5 >> 1 = -3(floor of -2.5),而 -5 / 2 = -2(truncation toward zero)。
纠正:
// ✅ 理解差异,选择合适的操作
int m = -5;
int floorDiv = m >> 1; // -3(向负无穷取整)
int truncDiv = m / 2; // -2(向零取整)
// 根据业务需求选择
反例二:位运算优先级与比较运算符混淆
// ❌ 错误:位运算优先级低于比较运算符
int flag = 0b0011;
if (flag & 0b0001 == 0) { // 实际是 flag & (0b0001 == 0) → flag & false → 编译错误或意外结果
System.out.println("bit0 为0");
}
纠正:
// ✅ 正确:用括号明确优先级
int flag = 0b0011;
if ((flag & 0b0001) == 0) { // 先按位与,再比较
System.out.println("bit0 为0");
}
反例三:~ 取反的结果出乎意料
// ❌ 错误预期:~5 = -5?错!
System.out.println(~5); // 输出 -6,而非 -5
原因:~ 是按位取反(含符号位),不是求相反数。~n = -n - 1(补码性质)。求相反数应该用 -n。
面试考点
Q1:>> 和 >>> 有什么区别?Java为什么需要无符号右移?
>>是带符号右移(arithmetic shift),高位补符号位(正数补0,负数补1),保持数值符号不变。>>>是无符号右移(logical shift),高位始终补0,结果总是非负数。Java设计
>>>的原因:(1)处理无符号数据(如网络字节流、哈希值);(2)实现位图算法;(3)HashMap的hash()方法中使用>>>让高位参与扰动,减少哈希碰撞。
Q2:如何使用位运算判断一个数是否是2的幂?
(n > 0) && ((n & (n - 1)) == 0)。原理:2的幂的二进制只有一个1,减1后该位变成0、后面全变1,按位与后必为0。例:8 (1000)&7 (0111)=0。此技巧在HashMap的tableSizeFor()中大量使用。
Q3:~5 为什么等于 -6 而不是 -5?
Java使用补码表示负数。5的32位二进制是
000...0101。按位取反(含符号位)得111...1010。补码解释:这是一个负数,其绝对值 =~(111...1010) + 1 = 000...0101 + 1 = 000...0110 = 6,所以值为-6。通式:~n = -n - 1。
Q4:位运算的常用应用场景有哪些?
① 权限控制:一个int管理32种权限(如Linux文件权限 rwx = 4|2|1)。② 高效乘除:
n << k等价n × 2^k。③ 哈希算法:JDK HashMap的hash()方法使用>>>扰动高位。④ 奇偶判断:n & 1比n % 2理论上更高效。⑤ 数据压缩:多个小值打包到一个int中。⑥ 加密/校验:CRC校验、简单异或加密。⑦ 状态标志:用一个整数的不同bit表示不同状态。