显微镜下的 i++ 与 ++i

低并发编程 2022/9/29 11:18:18 责编:子非

本文来自微信公众号:低并发编程 (ID:dibingfa),作者:闪客

注意,以下讨论的语言是 Java

这个问题被网上的好多文章写烂了,但基本重复度很高,我看过后的感觉是,大部分都是错误的、误导读者的。

随便百度一下,我们打开第一条。

上来先说个结论

i++ 先赋值再运算,例如 a=i++,先赋值 a=i,后运算 i=i+1,所以结果是 a==1

++i 先运算再赋值,例如 a=++i,先运算 i=i+1,后赋值 a=i,所以结果是 a==2

然后给了成吨的例子来说明

public class Test3 {
public static void main(String[] args) {
int y=0; 
//注意 "=" 是赋值,"==" 才是相等
//这里的 y=++y 是先运算在赋值
y=++y;// y==0,++y==y+1; 结果 y=++y == y+1 == 0+1 ==1
y=++y;// y==1,++y==y+1; 结果 y=++y == y+1 == 1+1 ==2
y=++y;// y==2,++y==y+1; 结果 y=++y == y+1 == 2+1 ==3
y=++y;// y==3,++y==y+1; 结果 y=++y == y+1 == 3+1 ==4
y=++y;// y==4,++y==y+1; 结果 y=++y == y+1 == 4+1 ==5
System.out.println("y="+y);//5
int i =0;
// i==0,i++==0; 结果 i=i++ == (记住先赋值后运算)
i=i++;
i=i++;
i=i++;
i=i++;
i=i++;
System.out.println("i="+i);//0
System.out.println("================");//1
}
}

首先这个例子没有任何代表性

其次得出的结论也是极其误导人的;

但最关键的是,这无法帮助你真正理解 i++ 和 ++i 的本质是什么

所以 i++ 和 ++i 的区别请听我说

先忘掉什么“先赋值、后运算”

别着急,慢慢来,忍住看到最后

i++ 和 ++i 字节码

查看字节码用 javap 命令,或者直接用 idea 的插件,这里不做过多介绍

在某方法里写上这样一段代码

public void ipp() {
    int i = 1;
    i++;
}

查看其字节码

iconst_1
istore_1
iinc 1 1
return

然后我们在写上这样一段代码

public void ipp() {
    int i = 1;
    ++i;
}

查看其字节码

iconst_1
istore_1
iinc 1 1
return

发现没,完全一样。也就是说,在没有赋值操作时,i++ 和 ++i 编译成字节码后,都是

iinc 1 1

完全一样

有多少人之前的理解是 i++ 和 ++i 本身孤零零地放在那是有区别的呢?

看 iinc 字节码的定义

找到 JVM 官方手册

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc

看到 iinc 字节码指令的格式为

iinc index const

index 表示局部变量表的索引,const 表示将其值加上多少

所以上面的

iinc 1 1

就表示

将局部变量表索引为 1 位置的值,加上 1

局部变量表索引 0 位置处是 this,索引 1 位置处的值,是 int i = 1 这段代码设置的,也就是 1。把这个值加 1,就变成了了 2

再来回顾下上面的代码

public void ipp() {
    int i = 1;
    i++;
    System.out.println(i);
}

public void ppi() {
    int i = 1;
    ++i;
    System.out.println(i);
}

如果打印 i 的值,很容易知道,两个都是 2

所以很简单,i++ 和 ++i 本身在字节码指令中的体现都是 iinc,就是单纯把 i 所在的局部变量表那个位置的值,+1

稍稍复杂一点

我们把上面的代码稍稍复杂一点,++ 操作后,再重新赋值给 i

public void ipp() {
    int i = 1;
    i = i++;
}

public void ppi() {
    int i = 1;
    i = ++i;
}

你猜 i 的值分别是多少

别急,再次查看字节码

void ipp() --> i = i++;

iconst_1
istore_1
iload_1
iinc 1 1
istore_1
return

void ppi() --> i = ++i;

iconst_1
istore_1
iinc 1 1
iload_1
istore_1
return

这回看到不一样了,但字节码指令都相同,只是顺序不同

i = i++ 就是 先 iload_1 再 iinc 1 1

i = ++i 就是 先 iinc 1 1 再 iload_1

所以也很简单,i++ 和 ++i 只有在最终赋值给某变量时(实际上是因为参与了运算,因为直接赋值也是一种无运算符号的运算),字节码指令是不同的,而且也只是顺序上的不同。

那顺序的不同会导致结果怎样呢?下面我们通过观察 虚拟机栈 来细化整个过程

观察虚拟机栈中的变化

但你得先知道虚拟机栈是什么,也就得知道 JVM 的内存划分,这块就不帮你复习了哈,直接上 ipp () 方法入虚拟机栈后,这个方法栈帧里的初始构造

看 i = i++

下面一步步执行 ipp () 方法的字节码

iconst_1
istore_1
iload_1
iinc 1 1
istore_1
return

注意局部变量表 0 处表示的是 this,这里为了简化没有写出,然后栈帧的“帧”字写错啦,我就任性一下不改了哈

iconst_1:将立即数 1 压栈

istore_1:操作数栈顶 -> 局部变量表 1 位置

iload_1:局部变量表 1 位置 -> 操作数栈顶

iinc 1 1:局部变量表 1 位置的值 +1

istore_1:操作数栈顶 -> 局部变量表 1 位置

所以,最后 i 的值,也就是局部变量表中 1 位置处的值,就是 1

我们用动画再演示一遍

你可以感受到,i = i++ 这种写法,iinc 1 1 这一步是完全没有用的,因为最后局部变量表 1 位置处的值,在最后一步赋值操作时,会被操作数栈顶处的值覆盖,所以之前的 +1 完全没用

所以 idea 也会提示你,这里的 i++ 没用

the value changed at 'i++' is never used

再看 i = ++i

相信这个你自己也可以推导出来了

iconst_1
istore_1
iinc 1 1
iload_1
istore_1
return

iconst_1:将立即数 1 压栈

istore_1:操作栈顶 -> 局部变量表 1 位置

iinc 1 1:局部变量表 1 位置的值 +1

iload_1:局部变量表 1 位置 -> 操作栈顶

istore_1:操作栈顶 -> 局部变量表 1 位置

所以,最后 i 的值,也就是局部变量表中 1 位置处的值,就是 2

我们直接用动画演示一遍

本质区别

所以看出本质区别是什么了么?

区别就是

是 "先把局部变量表中的值 +1,再放到操作数栈中"

还是 "先放到操作数栈中,再把局部变量表中的值 +1"

仅此而已

所以网上普遍的说法,i++ 表示 先 赋值运算

赋值就是 压入操作数栈顶

运算就是 局部变量表 +1 操作

反正这俩词我是对应不上...

还有的说法是,i++ 是先把 i 拿出来使用,然后再 + 1

还有的说法是,i++ 先赋值再自增

还有的 ... ...

哥哥诶,咱别用自己造的词误导读者了好不?

所以最后用我的话总结一个没有任何歧义的

i++:先将局部变量表中的 i 放入操作数栈中,再将局部变量表中的 i 值 +1

++i:先将局部变量表中的 i 值 +1,再将 i 放入操作数栈中

来点难的

当你从这个角度理解了之后,再做类似的复杂一点的题,也不在话下,大不了在脑子里从头推导一遍即可

看题

int i = 2;
int y = i++ + ++i;
y = ?

int a = 2;
a = a++ + ++a;
a = ?

int b = 2;
b = b++ + (++b + ++b) + (b += 2);
b = ?

答案

y = 6

a = 6

b = 18

你做对了么?

我把最难的那个题的字节码展示出来

按照黄色的字可以到操作数栈的变化(从左到右就是操作数栈从栈底到栈顶),自己脑补一下动画吧,不想做了有点懒哈哈哈~

int b = 2;
b = b++ + (++b + ++b) + (b += 2);
iconst_2    ; 操作数栈 2
istore_1    ; 局部变量表 b=2
iload_1     ; 操作数栈 2
iinc 1 by 1 ; 局部变量表 b=3
iinc 1 by 1 ; 局部变量表 b=4
iload_1     ; 操作数栈 2 4
iinc 1 by 1 ; 局部变量表 b=5
iload_1     ; 操作数栈 2 4 5
iadd        ; 操作数栈 2 9(=4+5)
iadd        ; 操作数栈 11(=2+9)
iinc 1 by 2 ; 局部变量表 b=7
iload_1     ; 操作数栈 11 7
iadd        ; 操作数栈 18(=11+7)
istore_1    ; 局部变量表 b=18

再难的,我觉得就有些无聊了,大家自己给自己出题吧~

如果你对这里的入栈顺序有困惑,比如你感觉加了()数学上不是先进行运算么?怎么不是先入栈参与运算呢?

那其实这和 i++ 与 ++i 的知识就不相关了,你需要了解的是 前缀、中缀、后缀表达式,这里只举个例子不展开讲解。

简单说就是如何将数学表达式,转换成一种格式,按照这个顺序可以方便通过栈来实现计算

比如

b++ + (++b + ++b) + (b += 2)

在转成后缀表达式过程中 ++ 操作根本不受影响,先简化成

b + (b + b) + b

转换成后缀表达式后就是

b b b + + b +

照着这个顺序压栈,就是字节码中指令的顺序啦,比如 b 压栈就是 iload_1,运算符(+)压栈就是 iadd,你再回过去证明一下哦~

而这里的每一个 b 的值,就是压栈那一时刻的 b 的值

最后愤怒地再说两句

所以,网上关于 ++ 的题目,其实是两个知识点

  • i++ 与 ++i 参与运算时的字节码指令

  • 将数学表达式转换为栈操作的后缀表达式

而网上的讲解,大部分都不是从最直接的字节码指令说,还将两个知识点混为一谈,我觉得是不负责任的。

回过头来再看开头说的那篇文章的结论

i++ 先赋值在运算,例如 a=i++,先赋值 a=i,后运算 i=i+1,所以结果是 a==1

++i 先运算在赋值,例如 a=++i,先运算 i=i+1,后赋值 a=i,所以结果是 a==2

先不说它没有用字节码来说明问题,你有没有发现这说的本身就是错的,有很大的误导性。

先赋值 a=i,后运算 i=i+1

其实根本没有先赋值吧,只是把 i 丢到操作数栈中等待被运算而已,然后局部变量表 i=i+1,最后操作数栈中的 i 出栈并写入局部变量表中 a 的位置,这时才叫赋值。

总之,这类文章还是少看为好

本文来自微信公众号:低并发编程 (ID:dibingfa),作者:闪客

相关文章

关键词:java编程

软媒旗下网站: IT之家 辣品 - 超值导购,优惠券 最会买 - 返利返现优惠券 iPhone之家 Win7之家 Win10之家 Win11之家

软媒旗下软件: 魔方 云日历 酷点桌面 Win7优化大师 Win10优化大师 软媒手机APP应用