算术/类型转换指令(++运算符/宽化/窄化)
JVM 的数学课:算术与类型转换
这一节来看字节码中做算术运算和类型转换的方式。你会发现,同样是 a + b,在字节码层面可能有多种不同的实现——这是编译器优化和 JVM 指令设计的交汇点。
算术指令
加减乘除
iadd // int 相加
ladd // long 相加
fadd // float 相加
dadd // double 相加
isub // int 相减
lsub // long 相减
fsub // float 相减
dsub // double 相减
imul // int 相乘
lmul // long 相乘
fmul // float 相乘
dmul // double 相乘
idiv // int 整除
ldiv // long 整除
fdiv // float 除法
ddiv // double 除法执行规则:弹出两个操作数,第二个弹出的作为被除数/被减数/被加数,第一个弹出的作为除数/减数/加数,结果入栈。
栈: [a, b]
执行 iadd
栈: [a+b]取模
irem // int 取模
lrem // long 取模
frem // float 取模
drem // double 取模java
// 源码
int r = a % b;
// 字节码
iload_1 // a 入栈
iload_2 // b 入栈
irem // 弹出 b 和 a,计算 a % b,结果入栈取反
ineg // int 取反
lneg // long 取反
fneg // float 取反
dneg // double 取反java
// 源码
int n = -x;
// 字节码
iload_1 // x 入栈
ineg // 取反位运算指令
按位与/或/异或
iand // int 按位与
land // long 按位与
ior // int 按位或
lor // long 按位或
ixor // int 按位异或
lxor // long 按位异或java
// 源码
int flag = 0b1010 & 0b1100; // 结果:0b1000 = 8
// 字节码
bipush 10 // 0b1010
bipush 12 // 0b1100
iand移位指令
ishl // int 左移(低位补 0)
lshl // long 左移
ishr // int 算术右移(高位补符号位)
lshr // long 算术右移
iushr // int 逻辑右移(高位补 0)
lushr // long 逻辑右移java
// 源码
int a = 8 << 2; // 左移 2 位:8 * 4 = 32
int b = -8 >> 1; // 算术右移:-4
int c = 8 >>> 1; // 逻辑右移:4
// 字节码
bipush 8
iconst_2
ishl
bipush -8
iconst_1
ishr
bipush 8
iconst_1
iushri++ 和 ++i 的字节码差异
这是一个高频面试点。i++ 和 ++i 在字节码层面有显著差异:
i++(后置递增)
java
// 源码
int i = 0;
int a = i++; // a = 0, i = 1bipush 0 // i = 0
istore_1 // 存入槽 1
iload_1 // i 的值(0)入栈 → a 稍后会拿这个值
iinc 1 by 1 // 槽 1 加 1(i = 1)
istore_2 // 栈顶(0)存入槽 2(a = 0)关键点:i 先被加载到栈上(用于赋值),然后才递增。
++i(前置递增)
java
// 源码
int i = 0;
int a = ++i; // i = 1, a = 1bipush 0
istore_1 // i = 0
iinc 1 by 1 // 槽 1 加 1(i = 1)
iload_1 // i 的值(1)入栈 → a 拿这个值
istore_2 // 栈顶(1)存入槽 2(a = 1)关键点:i 先递增,然后再加载到栈上(用于赋值)。
i = i++ 的陷阱
java
// 源码
int i = 0;
i = i++;
// 结果:i = 0,而不是 1!iload_1 // 把 i(0)入栈
iinc 1 by 1 // i 递增为 1
istore_1 // 把栈顶值(0)存回 i
// → i 又变回了 0!这是一个经典的「值拷贝」陷阱。编译器生成的字节码把 i++ 的结果(0)存回 i,覆盖了递增后的值(1)。
对比总结
| 表达式 | 字节码顺序 | i 的最终值 |
|---|---|---|
i++ | 先iload,后iinc | 递增后的值 |
++i | 先iinc,后iload | 递增后的值 |
i = i++ | iload → iinc → istore | 不变(旧值覆盖了新值) |
类型转换:宽化与窄化
宽化转换(Widening Conversion)
小类型自动转大类型,不需要显式指令——编译器在编译时处理,字节码层面看起来就是直接操作。
| 转换方向 | 说明 | 示例 |
|---|---|---|
int → long | 符号扩展 | iload 的结果直接用于 ladd |
int → float | int 转 float | 可能丢失精度 |
int → double | int 转 double | |
long → float | long 转 float | 可能丢失精度 |
long → double | long 转 double | |
float → double | float 转 double |
java
// 源码
long a = 5; // int → long 自动宽化
// 字节码
iconst_5 // int 5 入栈
i2l // int → long 转换
lstore_1 // 存入槽 1窄化转换(Narrowing Conversion)
大类型转小类型需要显式的转换指令,可能丢失数据:
i2l // int → long
i2f // int → float
i2d // int → double
l2i // long → int(可能丢失高位)
l2f // long → float(可能丢失精度)
l2d // long → double
f2i // float → int(truncate,向零取整)
f2l // float → long
f2d // float → double
d2i // double → int(可能丢失精度/溢出)
d2l // double → long(可能丢失精度)
d2f // double → float(可能丢失精度)
i2b // int → byte(保留低位字节,带符号)
i2c // int → char(保留低位,零扩展为 Unicode)
i2s // int → short(保留低位,带符号扩展)窄化转换的危险
java
// 源码
int i = (int) 1_000_000_000L; // long → int
// 结果:1000000000(没问题,在 int 范围内)
int j = (int) 3_000_000_000L; // long → int
// 结果:-1294967296(溢出!)java
// 源码
double d = 3.9;
int i = (int) d; // 3(向零取整,不是四舍五入)
double e = -3.9;
int j = (int) e; // -3(向零取整)float/double → int 的取整规则
java
// 源码
double d = 3.7;
int i = (int) d; // 3,向零取整
// 如果想要四舍五入,需要 Math.round()
// Math.round(3.7) = 4f2i 和 d2i 使用的是「向零取整」(truncate),即直接丢弃小数部分,不是四舍五入。
算术指令与 NaN
浮点数运算有一个特殊的规则:当操作数中有 NaN 时,结果也是 NaN:
double nan = 0.0 / 0.0; // NaN
double inf = 1.0 / 0.0; // Infinity
double negInf = -1.0 / 0.0; // -Infinity0.0 / 0.0 → NaN
Infinity + 1 → Infinity
Infinity - Infinity → NaN
NaN + 任何数 → NaN完整示例:复杂算术表达式
java
// 源码
public double calculate(double a, double b) {
return (a + b) * (a - b) / 2.0;
}字节码:
bash
javap -c Demo.class
# public double calculate(double, double);
# Code:
# 0: dload_1 // a 入栈
# 1: dload_3 // b 入栈
# 2: dadd // a + b
# 3: dload_1 // a 入栈
# 4: dload_3 // b 入栈
# 5: dsub // a - b
# 6: dmul // (a+b) * (a-b)
# 7: ldc2_w #2 // double 2.0 入栈
# 10: ddiv // 除以 2.0
# 11: dreturn本节小结
算术与类型转换核心要点:
| 类别 | 指令 | 说明 |
|---|---|---|
| 加减乘除 | iadd、isub、imul、idiv | 弹两个,运算,入栈一个 |
| 取模取反 | irem、ineg | 模运算和取反 |
| 位运算 | iand、ior、ixor、ishl | 按位与/或/异或/移位 |
| 后置++ | iload → iinc | 先取值后递增 |
| 前置++ | iinc → iload | 先递增后取值 |
| 宽化 | 无显式指令 | 编译器处理,无运行时开销 |
| 窄化 | i2l、d2i、f2i 等 | 显式转换,可能丢失数据 |
特别注意:i = i++ 会导致 i 不变,因为 istore 会把旧值覆盖回去。
下一节,我们来看 对象/数组/方法调用/返回指令。
