i=i++问题在各语言中的呈现和我的一些深入思考

以Java为例,下面的代码会输出几?

1
2
3
4
5
6
7
public class Main {
public static void main() {
int i = 1;
i = i++;
System.out.println(i);
}
}

如果把第4行的i = i++;换成i = ++i;结果又是如何?
我做了实验并查阅了一些资料,结果呈现如下:

语言 i=i++ i=++i 说明
Java 1 2 在JDK 8下执行实验
JavaScript 1 2 在Blink、WebKit内核解释执行结果
C++ 2 2 查阅资料,i=i++在C++17前为未定义行为,i=++i在C++11前为未定义行为(UB:Undefined Behaviour)
Go 语法错误 语法错误 i++只有一种用法,即作为「语句」单独放一行,没有++i的写法,也不能进行赋值操作,“++”不是「操作符」
Python 没有++操作符 没有++操作符
Scala 没有++操作符 没有++操作符 与Java同为JVM上执行的语言,但语法层面没有“++”操作符

答案都列出来了,这个问题是不是就到此为止了呢?其实问题的答案并不重要,因为不会真有人这么写代码。如果出选择题来考这个问题,我也会对出题人的水品表示怀疑。
单从语义层面讲,i++++i都是在i原本的值之上自增1,区别在于i++表示“先取值,后自增”,而++i表示“先自增,后取值”。不管是哪种操作,都自带了对i值变化的语义,所以i=i++的赋值操作本身在逻辑上就不成立,使用者想表达的语义应该是i+=1或者i=i+1。如果要扣字眼,我是比较倾向于C++的答案2的,虽然还是有点奇怪,但相比1来说至少不会令人惊讶。
这个例子的执行结果并不重要,但是我们换个角度,通过这个例子去探究代码内部执行过程,对学习更底层的原理会有很直观的帮助。多问一个为什么,知其所以然,就能发现这个问题背后的一些价值。
下面先以Java为例,来探究一下为什么会出现执行i=i++后值不变的现象。

要解释这个问题,需要对JVM有一定的了解,简单说明一下JVM运行时的数据区的构成和栈帧数据结构,这块内容可以在ORCAL官方文档2.5和2.6节里找到。

运行时数据区

JVM运行时数据区域包括「程序计数器」、「Java虚拟机栈」、「堆」、「本地方法栈」、「方法区」、「运行时常量池」,通常我们关注较多的在堆、栈(虚拟机栈)以及方法区。关于运行时数据区,如果有读过周志明这本著名的《深入理解Java虚拟机(第3版)》,会发现ORCAL官方文档里少个「Direct Memory」。官方文档并没有错,Java NIO用到的「直接内存」其实并不属于JVM规范,这块内存不归JVM管,属于操作系统底层的实现。
回到i=i++的问题,i作为方法内的局部变量,我们需要关注的是「Java虚拟机栈」。

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames.

每个在Java虚拟机上运行的线程都有一个私有的「Java虚拟机栈」,这个栈在线程被创建的时候产生,「Java虚拟机栈」存储「栈帧」这种数据结构。

栈帧

每当方法被调用的时候,JVM都会创建一个新的「栈帧」,其内部包括「局部变量表」(Local Variables)、「操作数栈」(Operand Stacks)、「动态连接」(Dynamic Linking)、「方法返回地址」(Normal Method Invocation Completion or Abrupt Method Invocation Completion)
解释i=i++的问题,只需要关注栈帧内部的「局部变量表」和「操作数栈」。
编译代码后执行javap -c Main.class可以打印出JVM的字节码指令。
int i=1;i=i++;
int i=1;i=++i;
我们需要关注的是1到5行的字节码指令,查阅一下它们有什么作用。

iload_<n>

Load int from local variable.
The <n> must be an index into the local variable array of the current frame. The local variable at must contain an int. The value of the local variable at is pushed onto the operand stack.

istore_<n>

Store int into local variable.
The <n> must be an index into the local variable array of the current frame. The value on the top of the operand stack must be of type int. It is popped from the operand stack, and the value of the local variable at is set to value.

iconst_<i>

Push int constant
Push the int constant <i> (-1, 0, 1, 2, 3, 4 or 5) onto the operand stack.

iinc

Increment local variable by constant.
The index is an unsigned byte that must be an index into the local variable array of the current frame. The const is an immediate signed byte. The local variable at index must contain an int. The value const is first sign-extended to an int, and then the local variable at index is incremented by that amount.

其中iconstiload是入栈操作,istore是出栈操作,iinc直接操作局部变量表,我做了个动画可以直观的对比两种写法的执行过程。(有点小问题,main是静态方法,局部变量表的第0位不是this指针而是args参数的引用)

JVM中指令执行步骤

所以i=i++值未变化现象的根源在于iinc这条指令被放在了iloadistore之间。
那为什么Java和JS都有这种现象,C++却不一样呢?C++更接近操作系统底层,编译后的字节码是CPU提供的指令集。如果和JVM的指令集运行环境类比的话,CPU中的「寄存器」有点像没有「LIFO」特性的「操作数栈」,而「局部变量表」就像「内存单元」。汇编代码里对「内存单元」的读写操作,是需要在「寄存器」里中转的。在C++17之前,i=i++这种写法本来就是「未定义行为」,编译器想怎么做都行,不同的平台或编译器有可能产生不一样的结果。C++17以后,明确了这种情况的结果是2。
还有一点也是我的猜想,Java和JS的指令执行需要运行在虚拟机/执行引擎中,在设计上都有操作数栈这种「LIFO」特性的数据结构,所以当自增操作夹在了入栈和出栈之间时,就会出现这种自增赋值被还原的现象。而C++编译后的指令是直接由CPU执行的,通过「寄存器」中转来对内存进行读写,也就不容易出现结果是1的情况。
和JS同为解释执行的Python以及和Java同为JVM虚拟机上执行的Scala都不支持“++”操作。
有意思的是Go语言中的“++”有且只有一种用法,即只能写成i++作为语句单独放在一行。
确实,“++”操作的使用场景有限,能想到的主要场景也就是在for循环中了for(int i = 0; i < n; i++),除此之外似乎没什么高频场景,最多在数组下标中使用一下a[i++]=b。所以支持for in range语法糖的语言去掉容易产生不明语义的++操作符也是情理之中了(以“灵活多变”著称的JS表示没有必要- -)。

心得

  • i=i++的结果并不重要,重要的是借助这个命题进行思考的过程
  • 遇到问题多问一个为什么,知其所以然,会有意想不到的收获
  • 所以为什么会有需要操作数栈这种数据结构,答案很容易想到——这样分配无需「GC」
  • 「Java虚拟机栈」的内存地址需要连续么,「栈帧」的内存可以位于堆中么?官方文档里有让人惊讶的答案
  • 有时候逻辑结构和物理实现并非一一对应,这也是计算机科学里随处可见分层思想的魅力
  • 我制作动画的时候刚开始因为疏忽把i=i++i=++i的指令看反了,强行解释了一波,竟然得到了如下“负负得正的结果”,好在自己及时发现。所以有时候查阅官方文档是非常有必要的
    错误的演示动画!!!

留在最后的思考

最后留下我在思考过程中延伸出的两个命题,独乐乐不如众乐乐。

不正经的问题,掀桌掀桌 !!(╯’ - ‘)╯︵ ┻━┻

在Java中如下的代码输出什么结果?

正经的问题,放平放平 ┬─┬ ノ( ‘ - ‘ノ)

Java中同样的问题,把局部变量int i = 0;变成静态变量后public static int i = 0;又会是什么结果?

文章作者: YueYang
文章链接: https://liuyueyang.top/022814.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 YY的闲时小站
文章订阅: 微信搜索:yueyang_top,关注公众号:悦洋的闲时小站,即可订阅本站文章实时动态
长按识别,交个朋友 (๑•̀ㅂ•́)و✧