下面一段程序相信每个 Java 程序员都做过:

public class PlusPlusTest {
    @Test
    public void test0() {
        int i = 0;
        i = i++;
        System.out.println(i);
    }
}

输出结果也不难想,就是 0。

这样的结果,大家肯定会想到一个口诀“先赋值,后自增”。但是有一天当别人问我具体的原因时,我竟发现自己只是记住了答案和一段口诀。现在,我们正式深入分析这个问题。

猜测

大家都知道 i++ 在做运算的时候,是先赋值再自加1,但底层究竟是怎样实现的呢?下面,就三个例子来说明一下i++的底层实现原理。

回到上面的例子:

    @Test
    public void test0() {
        int i = 0;
        i = i++;
        System.out.println(i);
    }

程序输出为 0。

也许 i++ 在作计算的时候要引入一个临时的变量,底层可能是这样实现的:

_temp = i;
i = i + 1;
i = _temp;

先把i的值赋给一个临时变量_temp,然后再作自加1的操作,最后又把临时变量_temp的值赋给了i,看到这里有点迷糊了吧!

但是如果令 int j = i++,底层也就是这样实现的:

_temp = i;
i = i + 1;
j = _temp;

所以,无论是哪种情况,最后打印出的结果都是 0。

第二个例子:

     @Test
     public void test1() {
        int i = 0;
        int sum = (i++)+(i++);
        System.out.println(sum);
    }

程序输出结果为1。

这个题参考第一个例子,可以这么理解:

假设有一个变量 m 接收第一个i++的计算结果,那么 m 的值一定是0,而底层 i 的值变成了 1。

再假设又有一个变量n接收第二个i++的计算结果,由于底层i的值变成了1,所以n的值是1,

那么,计算过程就变成了 m + n,等于 1。

第三个例子:

    @Test
    public void test2() {
        System.out.println(ipp0());
        System.out.println(ipp1());
    }

    public static int ipp0(){
        int i = 0;
        try{
            return i++;
        }finally{
            i++;
        }
    }

    public static int ipp1(){
        int i = 0;
        try{
            return i++;
        }finally{
            return  i++;
        }
    }

输出结果为:

0
1

无论什么时候,只要有 finally 语句块,就一定会执行的,所以底层 i 的值是2。

假设有一个变量 s 接收 try 语句块中 i++ 的值,s 为 0,return s,所以j的值是0。

如果 finally 语句块中的语句改为return i++,结果是什么?结果j的值是1,因为最后的返回结果不是 try 语句块中的结果,而是 finally 语句块中结果。

执行try语句块中的语句,i的值变成了1,所以finally语句块中的语句结果是1,底层i的值是2。

再改一下,finally语句块中的语句为return ++i,结果是什么。

结果j的值是2,因为最后finally语句块中的语句是i自加1之后,再return的。

所以finally语句块中返回的是2,底层i的值是2。

反编译验证

背景知识

如果你对 Java 的底层不是特别了解,有必要仔细阅读下面的文字。

关于字节码

字节码中在每条指令(操作码)之前的数字标识了字节的位置。例如,指令 1: iconst_1 说明该指令由于没有操作数,所以只有1个字节的长度,因此接下来的字节码就在位置 2;指令1: bipushu 5就会占用两个字节,一个字节用于存储操作码 bipush,另一个存储操作数5,这种情况下,因为操作数占用了位置2,所以下一条指令就会从位置3开始。

字节码指令一般形如 const_,如上文中的 iconst_1,i 指的是 int 类型,1 指的是第 1 个元素;bipushu 5 中的 b 指的是 byte 类型,由于这里没有“_”符号,这里的 5 就是字面量,即数值 5。

关于 JVM

Java虚拟机(JVM)是基于栈结构的。对于最初的 main 方法产生的所有的方法调用,都会分配一块内存当作该线程的栈,每个栈由一系列栈帧组成。。每个栈帧对应一个方法,当线程执行方法时,就是栈帧出栈,入栈的过程。每个栈帧包含三部分数据:局部变量表(包含这个方法在执行过程中所需的所有变量,包括一个指向 this 的引用、该方法的所有参数以及其他局部定义的变量。)、操作数栈、动态链接和方法返回地址等信息。对于类方法(即static方法),其参数列表从0开始算起,而对于实例方法,位置0是用来存储this引用。本文主要涉及局部变量表和操作数栈。

局部变量表是一组变量存储空间,用于存放方法参数和方法内部定义的局部变量。

操作数(operand stack)栈也常称为操作栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。

栈帧的概念结构

本文将采用一种简化的模型,如下所示:

栈帧的简化结构

我们先编译下面这个文件:

public class IPlusPlusTest {
    public static void main(String[] args) {
        int i = 0;
        i = i++;
        System.out.println(i);
    }
}

然后使用下面的命令可以对 class 文件进行反编译:

javap -v -c IPlusPlusTest

我们得到如下结果:

Compiled from "IPlusPlusTest.java"
public class IPlusPlusTest {
  public IPlusPlusTest();
    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_0
       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
}

下面是对字节码的解释:

字节码助记符 说明
aload_0 从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶。
invokespecial 这指令用于调用实例的初始化方法,包括私有方法以及当前类的父类方法。
return 方法返回
iconst_0 把数值0 push到操作数栈
istore_1 把操作数栈写回到本地变量第2个位置
iload_1 把本地变量第2个位置的值push到操作数栈
iinc 1, 1 把本地变量表第2个位置加1
istore_1 把操作数据栈写回本地变量第2个位置

整个过程如下

i++

可以发现变量 i 在执行 iinc 的时候已经变成 1 了,但是istore_1又把变量 i 所在位置覆盖成0,所以执行完 i=i++,i 还是原来那个值。

接下来看下 ++i 的实现:

public class PlusPlusITest {
    public static void main(String[] args) {
        int i = 0;
        i = ++i;
        System.out.println(i);
    }
}

对其进行反编译:

Compiled from "PlusPlusITest.java"
public class PlusPlusITest {
  public PlusPlusITest();
    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_0
       1: istore_1
       2: iinc          1, 1
       5: iload_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
}

同样,字节码解释:

字节码助记符 说明
aload_0 从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶。
invokespecial 这指令用于调用实例的初始化方法,包括私有方法以及当前类的父类方法。
return 方法返回
iconst_0 把数值0 push到操作数栈
istore_1 把操作数栈写回到本地变量第2个位置
iload_1 把本地变量第2个位置的值push到操作数栈
istore_1 把操作数据栈写回本地变量第2个位置

整个过程实现如下

++i

和 i++ 不同的地方在于,在变量进入操作数栈之前,就先执行了iinc指令,所以进入操作数的值是加 1 后的值,最后写回的值也是最新值。

参考资料

Java Code To Byte Code-PartOne

《深入理解Java虚拟机:JVM高级特性与最佳实践(第 2 版)》

最后修改于 2019-06-08 08:14:50
上一篇