从短路运算的 NPE 陷阱,到位运算的权限校验,从字节码到优先级,这篇带你全面通关
我们之前聊过:
- 数据类型:定义“值长什么样”(
int、String、char) - 变量类型:定义“值存在哪、活多久”(局部、实例、静态)
- 修饰符:定义“谁能用、有什么特殊功能”(
public、static、volatile)
今天我们来补上 Java 语法体系的最后一块核心拼图:运算符(Operators)。
如果说变量是“数据容器”,那么运算符就是“数据加工厂”。理解了运算符,你就理解了 Java 程序如何“动”起来。
一、Java 运算符全景分类(12 大类)
| 分类 | 运算符 | 优先级(大致) |
|---|---|---|
| 算术运算符 | +-*/%++-- | 高 |
| 关系/比较运算符 | ==!=><>=<= | 中 |
| 逻辑运算符 | &&` | |
| 位运算符 | &` | ^` `~` `<<` `>>` `>>>` |
| 赋值运算符 | =+=-=*=/=%=&=` | =^=` `<<=` `>>=` `>>>=` |
| 条件运算符(三元) | ? : | 低 |
| instanceof | instanceof | 中 |
| 其他 | 括号()、数组下标[]、成员访问. | 最高 |
🔥核心结论:括号
()优先级最高,赋值=优先级最低。掌握这个,就能避免 90% 的优先级混乱。
二、9 大高频陷阱与硬核解析(面试必看)
陷阱 1:i = i++到底等于几?(经典送命题)
inti=0;i=i++;System.out.println(i);// 输出:0底层字节码解析(JVM 视角):
编译器在处理i++时,会先将当前值0压入操作数栈(临时存储),然后给i加1(i变成1),最后将栈中的临时值0弹出并赋值给i。
所以i经历了:0→ 存入栈 → 自增成1→ 栈中0覆盖回i→ 最终0。
| 操作 | 操作数栈 | 局部变量i |
|---|---|---|
| 初始 | 空 | 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,右边根本不执行。
Stringstr=null;if(str!=null&&str.length()>0){// ✅ 安全,左边 false,右边不执行,不会 NPE// ...}if(str!=null&str.length()>0){// ❌ 危险!非短路 &,两边都执行,直接 NPE// ...}记忆口诀:
&&是“一假即假,遇假就停”;||是“一真即真,遇真就停”。
陷阱 3:位运算右移>>vs>>>(符号位处理)
>>:有符号右移,左边补符号位(正数补 0,负数补 1)。>>>:无符号右移,左边统一补 0(负数的二进制会变成很大的正数)。
inta=-8;// 二进制:11111111 11111111 11111111 11111000System.out.println(a>>1);// -4(保留符号位,除以 2 向下取整)System.out.println(a>>>1);// 2147483644(左边补 0,变成超大正数)日常开发中,
>>>常见于哈希算法、位图(BitMap)或底层网络报文解析。
陷阱 4:复合赋值运算符+=的“隐形强转”
byteb=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% 的开发者都会答错的点:
intresult=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()比较的是内容。
Integera=100;Integerb=100;System.out.println(a==b);// true(缓存池命中,同一对象)Integerc=200;Integerd=200;System.out.println(c==d);// false(超出 -128~127,不同对象)System.out.println(c.equals(d));// true(内容相同)铁律:包装类比较数值,永远用
equals(),不要用==。
陷阱 7:instanceof与数组/接口的“奇怪行为”
String[]arr=newString[10];System.out.println(arrinstanceofObject);// true(数组是对象)System.out.println(arrinstanceofObject[]);// trueSystem.out.println(arrinstanceofCloneable);// true(数组实现了 Cloneable)System.out.println(arrinstanceofSerializable);// true(数组实现了 Serializable)
instanceof右边可以是类、接口、数组类型。数组在 JVM 中也是对象,且有对应的类([Ljava.lang.String;)。
陷阱 8:逻辑非!与按位取反~的区别
!:逻辑非,只用于boolean,!true→false。~:按位取反,用于整数,将二进制每一位 0 变 1、1 变 0(包括符号位)。
booleanflag=true;System.out.println(!flag);// falseintnum=5;// 二进制:0000...0101System.out.println(~num);// -6(二进制:1111...1010,即 -6)陷阱 9:除零异常——整数 vs 浮点数的差异
inta=1/0;// ❌ 运行时抛出 ArithmeticExceptiondoubleb=1.0/0;// ✅ 输出 Infinity(IEEE 754 规范)doublec=0.0/0;// ✅ 输出 NaN(Not a Number)浮点数运算遵循 IEEE 754 标准,遇到除零不会抛异常,而是返回特殊值(
Infinity、-Infinity、NaN)。判断NaN要用Double.isNaN(),不要用==。
三、位运算的实战场景(让代码更优雅)
场景 1:权限系统(位掩码)
// 权限定义:读=1,写=2,执行=4publicstaticfinalintREAD=1;// 001publicstaticfinalintWRITE=2;// 010publicstaticfinalintEXEC=4;// 100intpermission=READ|WRITE;// 011(拥有读和写权限)booleancanRead=(permission&READ)!=0;// truebooleancanExec=(permission&EXEC)!=0;// false// 追加权限permission|=EXEC;// 011 | 100 = 111(拥有全部权限)// 移除权限permission&=~WRITE;// 111 & ~010 = 101(移除写权限)场景 2:高效乘除 2 的幂(底层优化)
inta=10;a<<1;// 相当于 a * 2 = 20(比乘法快)a>>1;// 相当于 a / 2 = 5(比除法快)// 但注意:右移负数时向下取整,除法向零取整intb=-5;System.out.println(b>>1);// -3(向下取整)System.out.println(b/2);// -2(向零取整)四、JVM 字节码视角(进阶)
运算符在编译后,会变成 JVM 字节码指令。以int a = 2 + 3;为例:
// Java 源码inta=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 的幂次优化 | 用>>或<<代替*/(但要注意负数取整的差异) |
六、思考题(检验是否真的懂了)
下面这段代码,每一行的输出分别是什么?为什么?
inti=0;System.out.println(i++);// 1System.out.println(++i);// 2System.out.println(i=i++);// 3intj=5;System.out.println(2<<++j);// 4booleanflag=false;System.out.println(flag=true);// 5(注意!这里不是比较)答案(选中下方空白区域查看):
0(先打印后自增)2(第一次输出后i=1,++i先自增成2再打印)2(先自增再覆盖,过程和陷阱 1 一样,i先变 3 再被栈中 2 覆盖)2 << 6 = 128(++j先算,j=6,然后2 << 6)true(注意是赋值=,不是比较==,flag被赋值为true并打印出来)
💬 互动话题:你在实际开发中,有没有因为++的前置后置、或者短路与非短路运算符搞混,导致过线上 Bug?欢迎评论区分享你的血泪史!🚀
如果觉得有收获,别忘了点赞、收藏、转发,让更多 Javaer 彻底搞懂运算符的底层逻辑!我们下篇见!👋
发布日期:2026-08-16