分析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:Thejavap
command disassembles one or more class files. The output depends on the options used. When no options are used, then thejavap
command prints the package, protected and public fields, and methods of the classes passed to it. Thejavap
command prints its output tostdout
. - 在终端查看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
反汇编一个类文件
参考: