云计算百科
云计算领域专业知识百科平台

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

从短路运算的 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。

操作操作数栈局部变量 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,右边根本不执行。

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(注意!这里不是比较)

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

  • 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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java 运算符:i = i++ 到底等于几?一文彻底搞懂 12 类运算符的底层逻辑
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!