在很多高级编程语言中,都会有i++和++i这种语法。例如在Java语言中,这两条语句都只能作为右值,而不能作为左值。同时,它们都可以作为独立的一条指令执行。下面用示例来讲解一下这两条语句。
int i = 0;
int j = i++; // 正确
int k = ++i; // 正确
i++; // 正确
++i; // 正确
i++ = 3; // 编译不通过
++i = 3; // 编译不通过
关于i++和++i的区别,大家应该都是有所了解的,本文将通过实例来简单地解释一下。
{
int i = 1;
int j = i++;
System.out.println("j=" + j1); // 输出 j=1
System.out.println("i=" + i); // 输出 i=2
}
{
int i = 1;
int j = ++i;
System.out.println("j=" + 2); // 输出 2=2
System.out.println("i=" + i); // 输出 i=2
}
上面的例子中可以看到,无论是i++和++i指令,对于i变量本身来说是没有任何区别,指令执行的结果都是i变量的值加1。而对于 j 来说,这就是区别所在
int i = 1;
int j1 = i++; // 先将i的原始值(1)赋值给变量j1(1),然后i变量的值加1
int j1 = ++i; // 先将i变量的值加1,然后将i的当前值(2)赋值给变量j1(2)
本文将在此基础上更加深入地研究其实现原理和陷阱,也有一定的深度。在读本文之前,您应该了解:
- 多线程相关知识
- Java编译相关知识
- JMM(Java内存模型)
本文接下来的主要内容包括:
- Java中i++和++i的实现原理
- -在使用i++和++i时可能会遇到的一些“坑”
i++和++i的底层实现原理
为了了解i++和++i的实现原理,方便对比,我将这两个指令分别放在2个不同的方法中执行,源代码如下:
public class Test {
public void testIPlusPlus() {
int i = 0;
int j = i++;
}
public void testPlusPlusI() {
int i = 0;
int j = ++i;
}
}
将上面的源代码编译之后,使用javap命令查看编译生成的代码,主要代码如下:
...
{
...
public void testIPlusPlus();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0 // 生成整数0
1: istore_1 // 将整数0赋值给1号存储单元(即变量i)
2: iload_1 // 将1号存储单元的值加载到数据栈(此时 i=0,栈顶值为0)
3: iinc 1, 1 // 1号存储单元的值+1(此时 i=1)
6: istore_2 // 将数据栈顶的值(0)取出来赋值给2号存储单元(即变量j,此时i=1,j=0)
7: return // 返回时:i=1,j=0
LineNumberTable:
line 4: 0
line 5: 2
line 6: 7
public void testPlusPlusI();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0 // 生成整数0
1: istore_1 // 将整数0赋值给1号存储单元(即变量i)
2: iinc 1, 1 // 1号存储单元的值+1(此时 i=1)
5: iload_1 // 将1号存储单元的值加载到数据栈(此时 i=1,栈顶值为1)
6: istore_2 // 将数据栈顶的值(1)取出来赋值给2号存储单元(即变量j,此时i=1,j=1)
7: return // 返回时:i=1,j=1
LineNumberTable:
line 9: 0
line 10: 2
line 11: 7
}
...
i++和++i在使用时存在的一些坑
i = i++的导致的结果“异常”
int i = 0;
i = i++;
System.out.println("i=" + i); // 输出结果 i=0
正常来说执行的结果应该是:i=1,实际结果却是:i=0,这多少会让人有些不明所以。为什么会出现这种情况呢?我们来从编码后的代码中找答案。上面的代码编译后的核心代码如下:
0: iconst_0 // 生成整数0
1: istore_1 // 将整数0赋值给1号存储单元(即变量i,i=0)
2: iload_1 // 将1号存储单元的值加载到数据栈(此时 i=0,栈顶值为0)
3: iinc 1, 1 // 1号存储单元的值+1(此时 i=1)
6: istore_1 // 将数据栈顶的值(0)取出来赋值给1号存储单元(即变量i,此时i=0)
7: getstatic #16 // 下面是打印到控制台指令
10: new #22
13: dup
14: ldc #24
16: invokespecial #26
19: iload_1
20: invokevirtual #29
23: invokevirtual #33
26: invokevirtual #37
29: return
从编码指令可以看出,i被栈顶值所覆盖,导致最终i的值仍然是i的初始值。无论重复多少次i = i++操作,最终i的值都是其初始值。
i++会产生这样的结果,那么++i又会是怎样呢?同样的代码顺序,将i++替换成++i如下:
int i = 0;
i = ++i; // IDE抛出【The assignment to variable i has no effect】警告
System.out.println("i=" + i); // 输出i=1
可以看到,使用++i时出现了“正确”的结果,同时Eclipse IDE中抛出【The assignment to variable i has no effect】警告,警告的意思是将值赋给变量i毫无作用,并不会改变i的值。也就是说:i = ++i等价于++i。
多线程并发引发的混乱
引发混乱的原因是:++i操作不是原子操作。
虽然在Java中++i是一条语句,字节码层面上也是对应iinc这条JVM指令,但是从最底层的CPU层面上来说,++i操作大致可以分解为以下3个指令:
取数
累加
存储
虽然说其中的一条指令可以保证是原子操作,但是3条指令合在一起却不是,这就导致了++i语句不是原子操作。
如果变量i用volatile修饰是否可以保证++i是原子操作呢,实际上这也是不行的。如果要保证累加操作的原子性,可以采取下面的方法:
- 将++i置于同步块中,可以是synchronized或者JUC中的排他锁(如ReentrantLock等)。
- 使用原子性(Atomic)类替换++i,具体使用哪个类由变量类型决定。如果i是整形,则使用AtomicInteger类,其中的AtomicInteger#addAndGet()就对应着++i语句,不过它是原子性操作。