分析Java中的`i++`、`++i`语句

分析Java中的i++++i语句

1.Java中i++++i介绍

  • ++是一种算术运算符

  • 很多语言中都有i++和++i,有些语言中i++和++i既可以作为左值又可以作为右值,但在Java语言中,这两条语句都只能作为右值,而不能作为左值。同时,它们都可以作为独立的一条指令执行。

    int i = 2;
    int j1 = i++; // 正常编译和运行
    int j2 = ++i; // 正常编译和运行
    i++; // 正常编译和运行
    ++i; // 正常编译和运行
    i++ = 2; // 编译不通过
    ++i = 2; // 编译不通过
    
  • 对于自增变量本身来说,无论++在前还是在后,自增变量本身都会自增1;但是对于自增表达式来说,++在前还是在后的结果是不同的:

    public class Test_20200623 {
          
          
        public static void main(String[] args) {
          
          
            int i = 2;
            int j1 = i++;
            //先赋值给j,后自增
            System.out.println("j1=" + j1); // 输出 j1=2
            System.out.println("i=" + i); // 输出 i=3
    				//先自增,后赋值给j
            int j2 = ++i;
            System.out.println("j2=" + j2); // 输出 j2=4
            System.out.println("i=" + i); // 输出 i=4
        }
    }
    

2.Java中i++++i的底层实现原理

  • 通过分析源代码的字节码,查看赋值过程

  • 源代码:

public class Test_20200623_2 {
    
    
    public static void main(String[] args) {
    
    

    }

    public  void test1() {
    
    
        int i = 2;
        int j = i++;
    }

    public  void test2() {
    
    
        int i = 2;
        int j = ++i;
    }
}
  • 编译源代码javac Test_20200623_2.java
  • 通过javap命令查看字节码javap -c Test_20200623_2.class
Compiled from "Test_20200623_2.java"
public class Test_20200623_2 {
    
    
  public Test_20200623_2();
    Code:
       0: aload_0
       1: invokespecial #1            // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: return

  public void test1();
    Code:
       0: iconst_2								// 生成整数2
       1: istore_1								// 将整数2赋值给1号存储单元(即变量i)
       2: iload_1									// 将1号存储单元的值加载到数据栈(此时i=2,栈顶值为2)
       3: iinc          1, 1			// 1号存储单元的值+1(此时 i=3)
       6: istore_2								// 将数据栈顶的值(2)取出来赋值给2号存储单元(即变量j,此时i=3,j=2)
       7: return								  // 返回时:i=3,j=2

  public void test2();
    Code:
       0: iconst_2								// 生成整数2
       1: istore_1								// 将整数2赋值给1号存储单元(即变量i)
       2: iinc          1, 1			// 1号存储单元的值+1(此时 i=3)
       5: iload_1									// 将1号存储单元的值加载到数据栈(此时i=3,栈顶值为3)
       6: istore_2								// 将数据栈顶的值(3)取出来赋值给2号存储单元(即变量j,此时i=3,j=3)
       7: return									// 返回时:i=3,j=3
}
  • 上述字节码中关键的地方在于下面两步骤哪一个先做:

    • 1号存储单元的值+1
    • 将1号存储单元的值加载到数据栈
  • 对于j=i++;是先将1号存储单元的值加载到数据栈(为2),然后将1号存储单元的值+1(i=3),所以当将数据栈顶的值(为2)取出并赋值给j时,j=2。

  • 对于j=++i;是先将1号存储单元的值+1(i=3),然后将1号存储单元的值加载到数据栈(为3),所以当将数据栈顶的值(为3)取出并赋值给j时,j=3。

3.Java中的i=i++

  • 源码:
public class Test_20200624_1 {
    
    
    public static void main(String[] args) {
    
    
        int i = 2;
        i = i++;

        System.out.println("i=" + i); // 输出 i=2
    }
}
  • 字节码分析:
Compiled from "Test_20200624_1.java"
public class Test_20200624_1 {
    
    
  public Test_20200624_1();
    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_2													    // 生成整数2
       1: istore_1												      // 将整数2赋值给1号存储单元(即变量i,i=2)				
       2: iload_1														    // 将1号存储单元的值加载到数据栈(此时 i=2,栈顶值为2)
       3: iinc          1, 1								    // 1号存储单元的值+1(此时 i=3)
       6: istore_1															// 将数据栈顶的值(2)取出来赋值给1号存储单元(即变量i,此时i=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 i=
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: iload_1
      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: return
}
  • 分析可知,关键在于理解数据栈和存储单元

  • i = i++是先将1号存储单元(变量i所标识)中的值(i=2)加载到数据栈,再改变1号存储单元的值+1(i=3),最把数据栈(存的是2)中的值赋给1号存储单元(i=2)。

  • 如果将i = i++改为i = ++i,那么就是先改变1号存储单元的值+1(i=3),再将1号存储单元(变量i所标识)中的值(i=3)加载到数据栈,最把数据栈(存的是3)中的值赋给1号存储单元(i=3)。即等价于++i或者i++

4.关于i++++i的更多实例

4.1

int a = 2; 
int b = (3 * a++) + a;   //b=(3*2)+3
System.out.println(b);   //  结果:9

4.2

int a = 2; 
int b = a + (3 * a++);  //2+(3*2)
System.out.println(b); // 结果:8

4.3

int i = 1;
int j = 1;
int k = i++ + ++i + ++j + j++;     //1+3+2+2
System.out.println(k);            // 结果:8

4.4

int a = 0;
int b = 0;
a = a++;
b = a++;
System.out.println("a = " + a + ", b = " + b); // a = 1, b = 0

5.Java中多线程环境下由++i操作引起的数据混乱

  • 引发混乱的原因是:++i操作不是原子操作。

  • 虽然在Java++i是一条语句,字节码层面上也是对应iinc这条JVM指令,但是从最底层的CPU层面上来说,++i操作大致可以分解为以下3个指令:

    • 取数
    • 累加
    • 存储
  • x = 10;        //语句1  原子性操作
    y = x;         //语句2  非原子性操作
    x++;           //语句3	非原子性操作
    x = x + 1;     //语句4	非原子性操作
    
  • 其中的一条指令可以保证是原子操作,但是3条指令合在一起却不是,这就导致了++i语句不是原子操作。

  • 如果变量i用volatile修饰也不可以保证++i是原子操作。至于原因,可以参考:Java并发编程:volatile关键字解析。如果要保证累加操作的原子性,可以采取下面的方法:

    • 将++i置于同步块中,可以是synchronized或者J.U.C中的排他锁(如ReentrantLock等)。
    • 使用原子性(Atomic)类替换++i,具体使用哪个类由变量类型决定。如果i是整形,则使用AtomicInteger类,其中的AtomicInteger#addAndGet()就对应着++i语句,不过它是原子性操作。

附录

javap命令

  • 反汇编一个或多个类文件。Disassembles one or more class files.
  • javap命令反汇编一个或多个类文件。输出取决于所使用的选项。如果不使用任何选项,则该javap命令将打印程序包,受保护的字段和公共字段以及传递给它的类的方法。该javap命令将其输出打印到stdout。Description:The javap command disassembles one or more class files. The output depends on the options used. When no options are used, then the javap command prints the package, protected and public fields, and methods of the classes passed to it. The javap command prints its output to stdout.
  • 在终端查看javap的用法javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
  -? -h --help -help               输出此帮助消息
  -version                         版本信息
  -v  -verbose                     输出附加信息
  -l                               输出行号和本地变量表
  -public                          仅显示公共类和成员
  -protected                       显示受保护的/公共类和成员
  -package                         显示程序包/受保护的/公共类
                                   和成员 (默认)
  -p  -private                     显示所有类和成员
  -c                               对代码进行反汇编
  -s                               输出内部类型签名
  -sysinfo                         显示正在处理的类的
                                   系统信息(路径、大小、日期、SHA-256 散列)
  -constants                       显示最终常量
  --module <模块>, -m <模块>       指定包含要反汇编的类的模块
  --module-path <路径>             指定查找应用程序模块的位置
  --system <jdk>                   指定查找系统模块的位置
  --class-path <路径>              指定查找用户类文件的位置
  -classpath <路径>                指定查找用户类文件的位置
  -cp <路径>                       指定查找用户类文件的位置
  -bootclasspath <路径>            覆盖引导类文件的位置
  --multi-release <version>        指定要在多发行版 JAR 文件中使用的版本
  • 常用命令:javap -c ClassName.class反汇编一个类文件

参考:

1.深入理解Java中的i++、++i语句

2.最通俗易懂的i++和++i详解

3.Java Platform, Standard Edition Tools Reference

4.说说javap命令

5.Java并发编程:volatile关键字解析

猜你喜欢

转载自blog.csdn.net/szw_yx/article/details/106970713