前言

最近在看JVM的视频,发现了一个很有意思的知识点,让我感觉我三年编程白学了。话不多说,上代码:

public class Main {
    public static void main(String[] args) {
        int i = 1;
        i = i++ ;
        int j = i++;
        int k = i + ++i * i++;
        System.out.println(i);
        System.out.println(j);
        System.out.println(k);
    }
}

根据我的经验,i++是先取值再自增,++i是先自增再取值,所以 i = i++的结果应该是i=2;j=i++的结果应该是i=3;j=2;最后k的结果应该是3 + 4 * 3 = 15。那么究竟对不对呢?我们运行一下程序,查看结果:
image.png
是不是发现自己一个都没对呢?那我们来看看为什么会这样。
了解原因之前,我们需要了解两个知识点

局部变量表和操作数栈

局部变量表

局部变量表是在Java虚拟机中用来存放函数参数以及实际变量信息的一段内存,例如当我们定义了i=1时,Java虚拟机就会在局部变量表中划分一段内存存放i=1这个结果,当方法执行完毕后,随着函数栈帧的销毁,局部变量表也随之销毁释放空间。

操作数栈

首先我们要了解什么是栈?学过数据结构的朋友们应该会很清楚:栈是一个线性表,它用来存放基本类型变量和对象的引用(这里强调引用两个字,因为对象本身是存放在堆中)。
那么操作数栈,顾名思义,就是用来存储程序运行过程中产生的操作数的,不过这些操作数都是临时使用的,就好像我们在进行计算时所使用的草稿纸一样,在计算完之后这些操作数就会被销毁。

程序运行时的局部变量表和操作数栈

在程序运行时,当我们定义一个变量时,若没有进行计算,则Java虚拟机会将变量直接放在局部表空间中。若在定义了变量时进行了算式运算,则会将参与运算的数放在操作数栈中进行临时存储。
例如:

int i = 1;
int j = i + 1;

在上面这段代码中,在进行i=1时,没有进行变量算式运算,不会产生临时的数据,因此将 i = 1放在局部表空间中存储。
在计算j = i + 1时,涉及到了变量算式运算,因此在这段代码的执行过程中,先进行等式右边的计算,首先将1(i)压入操作数栈,再进行 1 + 1的操作,再将计算的结果:2 压入操作数栈,最后将操作数栈的2赋值给 i ,将i=2放入局部变量表中进行存储。

代码解析

在上面的代码中,我们一步一步的进行解析

i = i++;

在这段代码中,先进行等式右边的计算:i++,在这个过程中,首先取值 i = 1,将1推入操作数栈,之后进行 i = i+1的计算,此时将i = 2 放入局部变量中,此时的局部变量中 i = 2,操作数栈中为 1 ,最后进行赋值运算,将操作数栈中的1赋值给i,最终得到i=1。

j = i++;

在这段代码中,先进行等式右边的计算:i ++,在这个过程中,首先取值 i = 1,将1推入操作数栈中,再进行 i = i +1的运算,此时将 i = 2 放入局部变量中,在进行赋值运算,将操作数栈中的1赋值给j,此时j = 1,i = 2。

k = i + i * i;

这段代码比较复杂,但仍然先进性右边的计算。在这里我们需要注意的是,即使乘法的优先级大于加法,但是在JVM中,仍然是从左往右进行操作数栈的入栈操作。

  • 首先,将i = 2压入操作数栈中,此时i = 2,操作数栈01为 2。
  • 再进行i的入栈操作,在i中,我们先进行i+1的操作,取得3压入操作数栈中,此时 i = 3,操作数栈02中为 3。
  • 接着进行 i++的入栈操作,先取值i=3,将3压入操作数栈中,再进行i+1的操作,此时 i = 4,操作数栈03中为3。
  • 最后进行算式运算,为:2 + 3 * 3 = 11,再将11赋值给k。

最终结果

经过上面的运算,此时i = 4,j = 1,k = 11。这样,我们就成功的计算出了正确的结果。

总结

  • 先进行等式右边的运算,赋值操作最后运行
  • 等式右边的计算中,依次从左往右压入操作数栈
  • 算式运算的过程中依照实际运算优先级进行计算,先乘除后加减
  • ii中的自加自减操作时都是之间修改局部变量表中的值

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议