从短路运算的 NPE 陷阱,到位运算的权限校验,从字节码到优先级,这篇带你全面通关
我们之前聊过:
- 数据类型:定义“值长什么样”(int、String、char)
- 变量类型:定义“值存在哪、活多久”(局部、实例、静态)
- 修饰符:定义“谁能用、有什么特殊功能”(public、static、volatile)
今天我们来补上 Java 语法体系的最后一块核心拼图:运算符(Operators)。
如果说变量是“数据容器”,那么运算符就是“数据加工厂”。理解了运算符,你就理解了 Java 程序如何“动”起来。
一、Java 运算符全景分类(12 大类)
| 算术运算符 | + – * / % ++ — | 高 |
| 关系/比较运算符 | == != > < >= <= | 中 |
| 逻辑运算符 | && ` | |
| 位运算符 | & ` | ^` `~` `<<` `>>` `>>>` |
| 赋值运算符 | = += -= *= /= %= &= ` | = ^=` `<<=` `>>=` `>>>=` |
| 条件运算符(三元) | ? : | 低 |
| instanceof | instanceof | 中 |
| 其他 | 括号 ()、数组下标 []、成员访问 . | 最高 |
🔥 核心结论:括号 () 优先级最高,赋值 = 优先级最低。掌握这个,就能避免 90% 的优先级混乱。
二、9 大高频陷阱与硬核解析(面试必看)
陷阱 1:i = i++ 到底等于几?(经典送命题)
int i = 0;
i = i++;
System.out.println(i); // 输出:0
底层字节码解析(JVM 视角):
编译器在处理 i++ 时,会先将当前值 0 压入操作数栈(临时存储),然后给 i 加 1(i 变成 1),最后将栈中的临时值 0 弹出并赋值给 i。
所以 i 经历了:0 → 存入栈 → 自增成 1 → 栈中 0 覆盖回 i → 最终 0。
| 初始 | 空 | 0 |
| 加载 i | [0] | 0 |
| i++(自增) | [0] | 1 |
| =(赋值) | 弹栈 | 0(被覆盖) |
💡 ++i 的区别:int i=0; int j=++i; → 先自增(i=1),再压栈赋值(j=1)。输出 i=1, j=1。
陷阱 2:短路运算 && 和 || 的“救人一命”
短路与(&&):如果左边为 false,右边根本不执行。
短路或(||):如果左边为 true,右边根本不执行。
String str = null;
if (str != null && str.length() > 0) { // ✅ 安全,左边 false,右边不执行,不会 NPE
// …
}
if (str != null & str.length() > 0) { // ❌ 危险!非短路 &,两边都执行,直接 NPE
// …
}
记忆口诀:&& 是“一假即假,遇假就停”;|| 是“一真即真,遇真就停”。
陷阱 3:位运算右移 >> vs >>>(符号位处理)
- >>:有符号右移,左边补符号位(正数补 0,负数补 1)。
- >>>:无符号右移,左边统一补 0(负数的二进制会变成很大的正数)。
int a = –8; // 二进制:11111111 11111111 11111111 11111000
System.out.println(a >> 1); // -4(保留符号位,除以 2 向下取整)
System.out.println(a >>> 1); // 2147483644(左边补 0,变成超大正数)
日常开发中,>>> 常见于哈希算法、位图(BitMap)或底层网络报文解析。
陷阱 4:复合赋值运算符 += 的“隐形强转”
byte b = 127;
b += 1; // ✅ 编译通过!b 变成 -128(发生溢出,但语法正确)
b = b + 1; // ❌ 编译报错:Type mismatch: cannot convert from int to byte
原理:b += 1 等价于 b = (byte)(b + 1),编译器自动插入了强制类型转换。而 b = b + 1 中,b + 1 会先类型提升为 int,不能直接赋值给 byte。
🚨 警示:虽然 += 编译通过,但溢出风险依然存在。127 + 1 = -128,这在业务中可能是灾难性的。
陷阱 5:运算符优先级中的“隐形炸弹”——移位低于加减
这是 90% 的开发者都会答错的点:
int result = 2 << 3 + 1;
System.out.println(result); // 输出:32
错误推理:(2 << 3) + 1 = 16 + 1 = 17 ❌
正确推理:+ 优先级高于 <<,所以 2 << (3 + 1) = 2 << 4 = 32 ✅
📋 优先级速记口诀(由高到低):
括号 > 单目(++ — ! ~)> 算术(* / % > + -)> 移位(<< >> >>>)> 比较(< > <= >= instanceof)> 相等(== !=)> 位逻辑(& > ^ > |)> 逻辑(&& > ||)> 三元(? :)> 赋值(=)
陷阱 6:== 与 equals() 的经典混淆(联动之前的 Integer 缓存池)
- 基本类型(int、char 等):== 比较的是数值本身。
- 引用类型(String、Integer 等):== 比较的是内存地址(引用),equals() 比较的是内容。
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(缓存池命中,同一对象)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超出 -128~127,不同对象)
System.out.println(c.equals(d)); // true(内容相同)
铁律:包装类比较数值,永远用 equals(),不要用 ==。
陷阱 7:instanceof 与数组/接口的“奇怪行为”
String[] arr = new String[10];
System.out.println(arr instanceof Object); // true(数组是对象)
System.out.println(arr instanceof Object[]); // true
System.out.println(arr instanceof Cloneable); // true(数组实现了 Cloneable)
System.out.println(arr instanceof Serializable); // true(数组实现了 Serializable)
instanceof 右边可以是类、接口、数组类型。数组在 JVM 中也是对象,且有对应的类([Ljava.lang.String;)。
陷阱 8:逻辑非 ! 与按位取反 ~ 的区别
- !:逻辑非,只用于 boolean,!true → false。
- ~:按位取反,用于整数,将二进制每一位 0 变 1、1 变 0(包括符号位)。
boolean flag = true;
System.out.println(!flag); // false
int num = 5; // 二进制:0000…0101
System.out.println(~num); // -6(二进制:1111…1010,即 -6)
陷阱 9:除零异常——整数 vs 浮点数的差异
int a = 1 / 0; // ❌ 运行时抛出 ArithmeticException
double b = 1.0 / 0; // ✅ 输出 Infinity(IEEE 754 规范)
double c = 0.0 / 0; // ✅ 输出 NaN(Not a Number)
浮点数运算遵循 IEEE 754 标准,遇到除零不会抛异常,而是返回特殊值(Infinity、-Infinity、NaN)。判断 NaN 要用 Double.isNaN(),不要用 ==。
三、位运算的实战场景(让代码更优雅)
场景 1:权限系统(位掩码)
// 权限定义:读=1,写=2,执行=4
public static final int READ = 1; // 001
public static final int WRITE = 2; // 010
public static final int EXEC = 4; // 100
int permission = READ | WRITE; // 011(拥有读和写权限)
boolean canRead = (permission & READ) != 0; // true
boolean canExec = (permission & EXEC) != 0; // false
// 追加权限
permission |= EXEC; // 011 | 100 = 111(拥有全部权限)
// 移除权限
permission &= ~WRITE; // 111 & ~010 = 101(移除写权限)
场景 2:高效乘除 2 的幂(底层优化)
int a = 10;
a << 1; // 相当于 a * 2 = 20(比乘法快)
a >> 1; // 相当于 a / 2 = 5(比除法快)
// 但注意:右移负数时向下取整,除法向零取整
int b = –5;
System.out.println(b >> 1); // -3(向下取整)
System.out.println(b / 2); // -2(向零取整)
四、JVM 字节码视角(进阶)
运算符在编译后,会变成 JVM 字节码指令。以 int a = 2 + 3; 为例:
// Java 源码
int a = 2 + 3;
// 字节码(javap 反编译)
iconst_2 // 将常量 2 压入操作数栈
iconst_3 // 将常量 3 压入操作数栈
iadd // 弹出栈顶两个 int,相加后将结果压回栈顶
istore_1 // 将栈顶值弹出,存储到局部变量表的槽位 1(即变量 a)
| + – * / %(int) | iadd、isub、imul、idiv、irem |
| ++ –(int) | iinc(直接在局部变量表自增,无需压栈) |
| ==(int) | if_icmpeq、if_icmpne |
| &&(boolean) | ifeq(条件跳转,实现短路) |
| << >> >>> | ishl、ishr、iushr |
关键:iinc 指令是直接在局部变量表上操作的,不经过操作数栈,所以 i++ 的效率其实非常高。但 i = i++ 多了一步 istore 覆盖,导致了上面的陷阱。
五、实战避坑总结
| 判空后再调用方法 | 用 && 短路运算,不要用 & |
| 包装类/对象比较 | 永远用 equals(),== 只用来比较基本类型或判断是否为 null |
| 浮点数判断是否为 0 | 用 Math.abs(a) < 1e-6,不要用 a == 0 |
| 权限控制/状态位 | 用位运算符 &、` |
| 复杂表达式 | 加括号! 不要依赖优先级记忆,让代码一眼就能看懂 |
| 除以 2 的幂次优化 | 用 >> 或 << 代替 * /(但要注意负数取整的差异) |
六、思考题(检验是否真的懂了)
下面这段代码,每一行的输出分别是什么?为什么?
int i = 0;
System.out.println(i++); // 1
System.out.println(++i); // 2
System.out.println(i = i++); // 3
int j = 5;
System.out.println(2 << ++j); // 4
boolean flag = false;
System.out.println(flag = true); // 5(注意!这里不是比较)
答案(选中下方空白区域查看):
💬 互动话题:你在实际开发中,有没有因为 ++ 的前置后置、或者短路与非短路运算符搞混,导致过线上 Bug?欢迎评论区分享你的血泪史!🚀
如果觉得有收获,别忘了点赞、收藏、转发,让更多 Javaer 彻底搞懂运算符的底层逻辑!我们下篇见!👋
发布日期:2026-08-16
网硕互联帮助中心






评论前必须登录!
注册