目录
起因
最近在学习 JavaScript,偶然发现近期 JavaScript 中似乎多了一部分新的操作符,比如 ??=
、??
、?.
等。想着学习一下,顺便温习下以往“显而易见”的一些操作符,于是就看到了逗号操作符。【说实话,这个我还真没注意过,居然还有逗号操作符😂】
学习的细节就不多说了,重要的是我看到了一段代码:
let x = 1;
x = (x++, x);
console.log(x);
// expected output: 2
然后嘞,我就给它改了改,改成了这样:👇
let x = 1;
x = x++;
console.log(x);
// my expected output: 2
一脸自信地点下了运行按钮,然而,现实给了我狠狠的一巴掌,结果居然是 1
?
Why?
不应该先走 x = x
,结果 x
为 1
;然后再走 x++
,结果 x
为 2
吗?怎么会是 1
呢?
x++ 与 ++x 的区别
不信邪的我赶紧去查了下 x++
与 ++x
的区别:
x++
:先计算x
的值,再将x
的值自增1
++x
:x
的值先自增1
,再计算x
的值
没错啊,计算 x = x++
时我也是按照先计算 x
的值,再对其进行自增的呀,怎么就错了呢?
反编译 x = x++
眉头皱紧的我想到了一个办法:将 x = x++
这句转换成一个更简单、更原生的代码片段,比如这样👇:
x = x;
x++;
如果能简化成这样的话,岂不是就能验证我的思路是没错的?然而,我没有找到这样的工具😭。
怎么能轻易放弃呢?💪
我又想到,要是能看一下 x = x++
这句在内存中到底是怎么运作的,那岂不是更方便?✌️
然而,对于 JavaScript,我还是没有找到工具😭😭。
简直了!
最终,经过一番苦逼的查找与实验,我突然一拍脑门,傻逼啊,我可以用 Java 啊!
然后嘞,我就测试了一下这段代码在 Java 中的情况:
public class Test {
public static void main(String []args) {
int x = 1;
x = x++;
System.out.println(x); // 1
}
}
果然,这个问题在 Java 中也存在!甚至,我还试了下,在 C 中、C# 中,都是存在这样的问题的!
这就好办了嘛!我大 Java,永远滴神!✌️✌️✌️
javap -c Test > _Test
,成功将代码段反编译👇:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: return
}
有点难懂😂
字节码探索
先简单学习了需要用到的字节码指令:
指令 | 指令解释 | 局部变量表1号位置 | 操作数栈顶(左侧代表栈顶) | 操作数栈2号位置 |
---|---|---|---|---|
iconst_1 | 将1放入操作数栈顶 | 空 | 1 | 空 |
istore_1 | 栈顶元素出栈并存入局部变量表1号位置 | 1 | 空 | 空 |
iload_1 | 将局部变量表1号位置元素放入栈顶 | 1 | 1 | 空 |
iinc 1, 1 | 将局部变量表1(前)号位置元素加1(后) | 2 | 1 | 空 |
iadd | 将栈顶两个元素弹出相加再放入栈顶 | — | — | — |
getstatic #2 | 调用System.out.println()方法,并将局部变量表1号位置的值作为参数 | — | — | — |
iload_1 | — | — | — | |
invokevirtual #3 | — | — | — | |
return | 返回 | — | — | — |
对 x = x++ 进行探索
为了彻底搞清楚这个问题,同时有一个对比,我写了 6 个代码片段,其核心代码如下👇:
int x= 1; x++; System.out.println(x); // 片段1
int x= 1; ++x; System.out.println(x); // 片段2
int x= 1; int y = x++; System.out.println(y); System.out.println(x); // 片段3
int x= 1; int y = ++x; System.out.println(y); System.out.println(x); // 片段4
int x= 1; x = x++; System.out.println(x); // 片段5
int x= 1; x = ++x; System.out.println(x); // 片段6
x++
先看一下 x++
怎么运作的:
理解:
- 将一个数值存起来
- 将这个存起来的数值赋给一个变量
- 将这个变量自增
- 打印
++x
再看看 ++x
是怎么运作的:
理解:
- 将一个数值存起来
- 将这个存起来的数值赋给一个变量
- 将这个变量自增
- 打印
看起来 x++
和 ++x
完全一样啊?
难道说,因为没有任何变量来接收 X++
的结果,导致 JVM 将 X++
优化成 ++x
了?
从前面可以看到,当没有变量接收 x++
/++x
的结果时,JVM 认为 x++
和 ++x
是一样的。
那么,如果我们定义一个变量来接收 x++
/++x
的结果,那又会发生什么呢?
y = x++
我们先来看 y = x++
是怎么运作的:
理解:
- 将一个数值存起来
- 将这个存起来的数值赋给一个变量
— 👆int x = 1
- 将这个变量的数值存起来
- 将这个变量自增
- 将存起来的数值赋给一个新的变量
— 👆int y = x++
- 分别打印
有些疑惑,x++
是先返回 x
的值(将 x
的值存起来),再 ++
(自增),这没有问题;但是,为什么不把存起来的值先赋给 y
呢?
因为按照我的理解,y = x++
应该等价于 👉 y = x; x++
而且算下来的值也一样啊,可是为什么字节码顺序对不上呢?
没太理解 y = x++
的背后运作原理,那先来看一下 y = ++x
吧!应该可以对照着看出一些东西来。
y = ++x
再看看 y = ++x
是怎么运作的:
理解:
- 将一个数值存起来
- 将这个存起来的数值赋给一个变量
— 👆int x = 1
- 将这个变量自增
- 将变量的数值存起来
- 将存起来的数值赋给一个新的变量
— 👆int y = ++x
- 分别打印
这下确实没问题,确实是按照我理解的顺序走的:
先 ++
(自增),再返回 x
的值(将 x
的值存起来),再把存起来的值赋给 y
。
可是,既然我理解的思路是对的,那为什么 y = x++
的字节码顺序对不上呢?🤔
还是有困惑,那就继续看一下 x = x++
的运作原理,再继续对比对比吧!
x = x++
看看 x = x++
是怎么运作的:
理解:
- 将一个数值存起来
- 将这个存起来的数值赋给一个变量
— 👆int x = 1
- 将这个变量的数值存起来
- 将这个变量自增
— 👆x++
- 将存起来的数值赋给原变量
— 👆x = x
- 打印
从上面的分析来看,似乎是先运行了 x++
,再运行 x = x
,同时为了保证在运行 x++
时先返回 x
的值,JVM 将其先暂存了起来,自增完后再赋值,结果将自增的值覆盖了。
而这也是为什么最终 x
的值为 1
的原因!
但是,为什么要多此一举呢?为什么不先赋值,再自增?
咦?等等,我忽然想到了一个问题!先自增,再赋值!
再来用 x = ++x
验证一下!
x = ++x
看看 x = ++x
是怎么运作的:
理解:
- 将一个数值存起来
- 将这个存起来的数值赋给一个变量
— 👆int x = 1
- 将这个变量自增
— 👆++x
- 将这个变量的数值存起来
— 👆x
- 将存起来的数值赋给原变量
— 👆x = x
- 打印
没问题,先自增,再赋值!按照这样的逻辑是能这样走通的,那么,我知道了!
++ 优先级 > 赋值优先级
按照这样的优先级,不管是单纯的 x++
/++x
,还是带赋值的 y = x++
/y = ++x
/x = x++
/x = ++x
,都必须先运算 x++
/++x
。
那么,当运算 x = x++
时,按照运算符优先级:
- 先走
x++
,但是为了保证先返回x
的值,因此将其暂存起来,然后给x
自增; - 第二步,进行赋值操作,因为已经将需要返回的值暂存起来了,因此将暂存起来的值赋给变量,因此最终结果为 1
终于搞明白了!
总结
按照运算符的优先级不同,需要将一个表达式合理地进行分割,不能跨多个“块”运算,需要严格遵守优先级顺序。
参考资料
- Is there a difference between x++ and ++x in java?
- x++和++x的区别是什么? - html中文网
- Java类文件研究的可视化工具classpy介绍_明明如月的专栏-CSDN博客_classpy
- Java字节码指令大全 - 简书 (jianshu.com)
- Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)
- 【JVM学习】将java文件编译成字节码文件,再到反编译,字节码指令剖析_seesun2012的专栏-CSDN博客
- jvm是如何执行i = i++ + ++i的,你知道吗? - 简书 (jianshu.com)