重铸基础 | Java | i++ 与 ++i 的区别到底在哪里

354 阅读4分钟

初学编程时,i++ 与 ++i 经常搞错,哪怕是在工作多年后,也只是知道 ++i 是先自增运算赋值再进行操作,i++ 是先进行操作,然后再进行自增运算赋值,只能说是知其然,却不知其所以然,作为研究 JVM 第一站,先剖析一下这个经典的问题。

首先,先列出今天的测试代码:

/**
 * 从字节码层面分析 i++ 和 ++i 为什么不同
 */
public class AnalyseBytecode {

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

}

代码很简单,就不过多介绍了。

其次,执行 javac -g:vars 具体文件路径/AnalyseBytecode .java 将 java 文件编译成 class 文件。

注意,-g:vars 一定要加上,要不然 LocalVariableTable 不会出现。

然后,通过执行 javap -v 文件路径/AnalyseBytecode.class > 指定路径/AnalyseBytecode.txt 实现反汇编,就可以进入正题了。

Classfile ~/AnalyseBytecode.class
  Last modified 2021-1-5; size 752 bytes
  MD5 checksum 06e2e2dd5a25824e2d4033df5539f693
public class com.wd.clazz.AnalyseBytecode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #11.#25        // java/lang/Object."<init>":()V
   #2 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #28            // java/lang/StringBuilder
   #4 = Methodref          #3.#25         // java/lang/StringBuilder."<init>":()V
   #5 = String             #29            // num:
   #6 = Methodref          #3.#30         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Methodref          #3.#31         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #8 = Methodref          #3.#32         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = Class              #35            // com/wd/clazz/AnalyseBytecode
  #11 = Class              #36            // java/lang/Object
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/wd/clazz/AnalyseBytecode;
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               i
  #23 = Utf8               I
  #24 = Utf8               num
  #25 = NameAndType        #12:#13        // "<init>":()V
  #26 = Class              #37            // java/lang/System
  #27 = NameAndType        #38:#39        // out:Ljava/io/PrintStream;
  #28 = Utf8               java/lang/StringBuilder
  #29 = Utf8               num:
  #30 = NameAndType        #40:#41        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #31 = NameAndType        #40:#42        // append:(I)Ljava/lang/StringBuilder;
  #32 = NameAndType        #43:#44        // toString:()Ljava/lang/String;
  #33 = Class              #45            // java/io/PrintStream
  #34 = NameAndType        #46:#47        // println:(Ljava/lang/String;)V
  #35 = Utf8               com/wd/clazz/AnalyseBytecode
  #36 = Utf8               java/lang/Object
  #37 = Utf8               java/lang/System
  #38 = Utf8               out
  #39 = Utf8               Ljava/io/PrintStream;
  #40 = Utf8               append
  #41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = Utf8               (I)Ljava/lang/StringBuilder;
  #43 = Utf8               toString
  #44 = Utf8               ()Ljava/lang/String;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               println
  #47 = Utf8               (Ljava/lang/String;)V
{
  public com.wd.clazz.AnalyseBytecode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wd/clazz/AnalyseBytecode;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: iinc          1, 1
         6: istore_2
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: new           #3                  // class java/lang/StringBuilder
        13: dup
        14: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        17: ldc           #5                  // String num:
        19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: iload_2
        23: invokevirtual #7                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        26: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: iinc          1, 1
        35: iload_1
        36: istore_2
        37: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        40: new           #3                  // class java/lang/StringBuilder
        43: dup
        44: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        47: ldc           #5                  // String num:
        49: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        52: iload_2
        53: invokevirtual #7                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        56: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        59: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        62: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      63     0  args   [Ljava/lang/String;
            2      61     1     i   I
            7      56     2   num   I
}

先看第一部分关键代码:

0: iconst_0 // 将 0 压入栈中
1: istore_1 // 将栈顶的值放入变量 1 中并出栈,从 LocalVariableTable 中查询,变量 1 为 i
2: iload_1 // 将变量 1 放入栈顶
3: iinc          1, 1 // 将变量 1 的值加 1
6: istore_2 // 将栈顶的值放入变量 2 中并出栈,从 LocalVariableTable 中查询,变量 2 为 num

敲黑板,关键点解析,3: iinc 这一步虽然将 i 值递增了,但是并没有将递增后的结果重新入栈,所以此时栈顶的值还是旧值 0,6: istore_2 又将栈顶的值赋值给变量 2 ,此时的值还是变量 1 未被重新赋值时的数值,所以此时 num 为 0。

再看第二部分:

32: iinc          1, 1 // 变量 1 递增 1
35: iload_1 // 变量 1 入栈
36: istore_2 // 将栈顶的值赋值给变量 2 并出栈

这里 num 的值就是 i 递增后的值。

总结:i++ 操作,先入栈,再递增,此时栈中的值还是旧值;++i 操作,先递增,再入栈,此时栈中的值是最新值。iload_iinc 的顺序导致了结果的不同。