Skip to content

算术/类型转换指令(++运算符/宽化/窄化)

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
iushr

i++ 和 ++i 的字节码差异

这是一个高频面试点。i++++i 在字节码层面有显著差异:

i++(后置递增)

java
// 源码
int i = 0;
int a = i++;  // a = 0, i = 1
bipush        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 = 1
bipush        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)

小类型自动转大类型,不需要显式指令——编译器在编译时处理,字节码层面看起来就是直接操作。

转换方向说明示例
intlong符号扩展iload 的结果直接用于 ladd
intfloatint 转 float可能丢失精度
intdoubleint 转 double
longfloatlong 转 float可能丢失精度
longdoublelong 转 double
floatdoublefloat 转 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) = 4

f2id2i 使用的是「向零取整」(truncate),即直接丢弃小数部分,不是四舍五入。

算术指令与 NaN

浮点数运算有一个特殊的规则:当操作数中有 NaN 时,结果也是 NaN:

double nan = 0.0 / 0.0;     // NaN
double inf = 1.0 / 0.0;     // Infinity
double negInf = -1.0 / 0.0; // -Infinity
0.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

本节小结

算术与类型转换核心要点:

类别指令说明
加减乘除iaddisubimulidiv弹两个,运算,入栈一个
取模取反iremineg模运算和取反
位运算iandiorixorishl按位与/或/异或/移位
后置++iloadiinc先取值后递增
前置++iinciload先递增后取值
宽化无显式指令编译器处理,无运行时开销
窄化i2ld2if2i显式转换,可能丢失数据

特别注意i = i++ 会导致 i 不变,因为 istore 会把旧值覆盖回去。

下一节,我们来看 对象/数组/方法调用/返回指令

基于 VitePress 构建