ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java 运算符:i = i++ 到底等于几?一文彻底搞懂 12 类运算符的底层逻辑

2026/8/17 21:00:50 拓冰建站 浏览量
Java 运算符:i = i++ 到底等于几?一文彻底搞懂 12 类运算符的底层逻辑

从短路运算的 NPE 陷阱,到位运算的权限校验,从字节码到优先级,这篇带你全面通关

我们之前聊过:

  • 数据类型:定义“值长什么样”(intStringchar
  • 变量类型:定义“值存在哪、活多久”(局部、实例、静态)
  • 修饰符:定义“谁能用、有什么特殊功能”(publicstaticvolatile

今天我们来补上 Java 语法体系的最后一块核心拼图:运算符(Operators)

如果说变量是“数据容器”,那么运算符就是“数据加工厂”。理解了运算符,你就理解了 Java 程序如何“动”起来。

一、Java 运算符全景分类(12 大类)

分类运算符优先级(大致)
算术运算符+-*/%++--
关系/比较运算符==!=><>=<=
逻辑运算符&&`
位运算符&`^` `~` `<<` `>>` `>>>`
赋值运算符=+=-=*=/=%=&=`=^=` `<<=` `>>=` `>>>=`
条件运算符(三元)? :
instanceofinstanceof
其他括号()、数组下标[]、成员访问.最高

🔥核心结论括号()优先级最高,赋值=优先级最低。掌握这个,就能避免 90% 的优先级混乱。

二、9 大高频陷阱与硬核解析(面试必看)

陷阱 1:i = i++到底等于几?(经典送命题)

inti=0;i=i++;System.out.println(i);// 输出:0

底层字节码解析(JVM 视角)
编译器在处理i++时,会先将当前值0压入操作数栈(临时存储),然后给i1i变成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缓存池)

  • 基本类型intchar等):==比较的是数值本身
  • 引用类型StringInteger等):==比较的是内存地址(引用)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!truefalse
  • ~:按位取反,用于整数,将二进制每一位 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-InfinityNaN)。判断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)iaddisubimulidivirem
++--(int)iinc(直接在局部变量表自增,无需压栈)
==(int)if_icmpeqif_icmpne
&&(boolean)ifeq(条件跳转,实现短路)
<<>>>>>ishlishriushr

关键iinc指令是直接在局部变量表上操作的,不经过操作数栈,所以i++的效率其实非常高。但i = i++多了一步istore覆盖,导致了上面的陷阱。

五、实战避坑总结

场景推荐做法
判空后再调用方法&&短路运算,不要用&
包装类/对象比较永远用equals()==只用来比较基本类型或判断是否为null
浮点数判断是否为 0Math.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(注意!这里不是比较)

答案(选中下方空白区域查看):

  1. 0(先打印后自增)
  2. 2(第一次输出后i=1++i先自增成2再打印)
  3. 2(先自增再覆盖,过程和陷阱 1 一样,i先变 3 再被栈中 2 覆盖)
  4. 2 << 6 = 128++j先算,j=6,然后2 << 6
  5. true(注意是赋值=,不是比较==flag被赋值为true并打印出来)

💬 互动话题:你在实际开发中,有没有因为++的前置后置、或者短路与非短路运算符搞混,导致过线上 Bug?欢迎评论区分享你的血泪史!🚀


如果觉得有收获,别忘了点赞收藏转发,让更多 Javaer 彻底搞懂运算符的底层逻辑!我们下篇见!👋

发布日期:2026-08-16