关于Java中的i++和++i

来源:互联网 发布:嘉兴软件开发制作 编辑:程序博客网 时间:2024/05/23 21:48
关于i++和++i的区别,想必每个人都很清楚——“++在前,先自增后使用;++在后,先使用后自增”。这个总结其实是非常到位和经典的。但是如果我们只知道记住某条规则,而不去从代码乃至内存的角度去理解规则背后的含义,就不能做到真正理解规则,不能做到真正理解代码和程序。甚至很多时候,题目和要求稍微发生变化,我们就不知道该如何套用熟记的规则了。
本篇博文也是作为初学者的我,在回头重新看i++和++i这种自增运算符的时候,学习到的一些以前没有注意的点,所以拿来和和大家分享。由于是初学者(好几遍的初学者^_^,由于记性不好,所以Java SE部分回头重新看了好几遍),难免很多地方用到的术语,对原理的解释有不清楚甚至说错的地方。如果有,希望大家指正。而且,我自己也会不断回头去看i++和++i还有那些地方值得注意,如果有的话,将来会更新到本篇博文里。

言归正传,要深入理解i++和++i,首先需要知道关于程序的以下两点。
①代码都是自上而下顺序执行的。
不管是我们写的java源码还是java在内存中的编译和运行,总体上都是自上而下顺序执行的。不排除遇到goto命令或者循环语句导致程序发生跳转的情况,但是总体上是顺序执行的。
②很多操作是在栈里面进行的。网上可以搜到很多关于堆(heat),栈(stack),常量池,方法区等概念和图示。大体上我们只需要知道栈里一般暂时性存放局部变量,和数字打交道,比如临时存放堆中某个对象的引用(内存地址),进行数字的计算等。而堆里往往存放的是new出来的对象。这里说的是i++和++i,不牵涉到对象,所以不展开讨论别的内容。而平常我们经常的写的赋值语句,数值运算,循环结构等都要经过进栈和出栈的一系列操作才得以实现。这些在后面的例子中都有展示。

接下来,我们就简单分析一下i++和++i在字节码和内存层面的具体情况。

首先看一个简单的i++赋值操作。
public class TestPlusPlus {public static void main(String[] args) {int a = 1;int b = a++;System.out.println(b); // 输出结果为1}}
按我们记的规则来看,a++是先使用后自增,也就是把a=1的值赋给b以后,然后a自己自增了1,所以输出结果是b的值1。没错,那么从字节码和内存的层面去看,它是怎么执行的呢?
通过javap -verbose TestPlusPlus反编译class文件查看到的字节码内容如下:
public static void main(;
  descriptor: ([LV
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
       0: iconst_1
       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: iload_2
      11: invokevirtual #3                  // Method V
      14: return
    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 7
      line 6: 14
上面只是截取了main方法的内容,全部的操作都是在main方法里完成的,这一点和我们的java源码是一样的。
至于这里面的jvm指令分别都是什么意思,建议大家去百度查找一下,很容易搜到。这里只说一下本段字节码中用到的指令。
0: iconst_1  ←将int类型常量1压入栈。这里的1是实实在在的数字1。
1: istore_1  ←将int类型值存入局部变量1。这里的1指的是局部变量区域代号为1的那个变量。这里,代号为1的局部变量值为1,是因为赋值就是1。如果写的是int a =2,那么这个代号为1的的局部变量的值就是2.千万不要混淆了。通过这两句指令java完成了一次赋值操作。
2: iload_1  ←将代号为1的局部变量压入栈。本例子中是把数值1压入栈。
3: iinc          1, 1  ←将代号为1的局部变量自增1。这个地方需要注意了。上一句命令是把数值1压入栈,这一句是把局部变量中代号为1的那个的值自增1,也就是说,局部变量a现在变成了2。
6: istore_2  ←将栈里面的值(栈顶的值)推出,存入局部变量2。这就是b=a++,其实就是把栈顶的数字1放到局部变量中。上面一直说局部变量,其实英语原词是“local variable”,翻译成本地变量更好。
7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;  ←获取静态的输出流。即:System.out。
10: iload_2  ←将代号为2的本地变量的值压入栈。本例子中是把数值1(b的值)压入栈。
11: invokevirtual #3                  // Method V  ←调用println,输出栈里的值1
14: return  ←方法结束
从上面的指令解释中可以看出,a++,是先把局部变量a的值1压入栈中,然后局部变量a自己自增1变成了2,接着把栈中的这个数值1存到代号为2的本地变量里面,也就是b里面。所以b得到了1,a自己变成了2。

接着是一个简单的++i的例子。由于上面i++的例子中已经详细解释了相关指令的含义,接下来的这个例子里,只解释一下和上面例子不一样的地方。
public class TestPlusPlus {public static void main(String[] args) {int a = 1;int b = ++a;System.out.println(b);}}
同样通过javap命令把javac编译的class文件反编译一下,结果如下:
public static void main(;
  descriptor: ([LV
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
       0: iconst_1
       1: istore_1
       2: iinc          1, 1
       5: iload_1
       6: istore_2
       7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_2
      11: invokevirtual #3                  // Method V
      14: return
    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 7
      line 6: 14
大体上和i++是一样的,唯一需要注意的是下面几行。
       2: iinc          1, 1  ←将代号为1的局部变量自增1。在本例子中,局部变量a自增1变成2。
       5: iload_1  ←将代号为1的局部变量的值压入栈。在本例子中,把自增后的局部变量的值2压入栈。
       6: istore_2  ←将栈里的数值推出栈,存入代号为2的本地变量。在本例子中,把上一步压入栈的数值2存到代号为2的本地变量中,也就是b中。
从上面的指令解释中可以看出,++a,是先把局部变量a的值自增1变成2,然后把a的值2压入栈中,接着把栈中的2存到代号为2,也就是b的局部变量中。所以,b得到了2,a自己也早已经变成了2。

通过上面的两个简单例子的对比,可以很清楚地看到i++和++i的区别。我觉得分析字节码给人最大的印象就是代码底层仍然是面向机器的。好多我们认为理所当然的事,在内存里要按照机器的规则来,一步一步走。比如最简单的赋值语句,就要进行一次复杂的进栈出栈操作。同时还要注意到,栈(stack)和局部变量(local variable)的区别,栈相当于一个为了计算而存在的临时性容器,而它的内容则会被保存到局部变量中,某种程度上来说,局部变量的值取出压入栈中以后,它就是一个单独的数值,和局部变量本身就没有太大关系了。

回到“++在前,先自增后使用;++在后,先使用后自增”这句规则上。其实我们平常用自增自减这种运算符,心理预期是它自增自减完再被我们使用,所以从这个角度来说,++i才是最符合人们的使用初衷的。因此在各种场合,使用++i都不会产生预料之外的风险。但是由于习惯,实际上我们使用i++的场合反而比较多。这种情况下,就有必要说明一下i++的“先使用后自增”中的“使用”一词具体都指哪些。

①赋值的时候,先赋值后自增。
public class TestPlusPlus {public static void main(String[] args) {int a = 1;int b = a++;System.out.println(b); // 输出结果为1}}
②输出(打印)的时候,先输出(打印)后自增。
public class TestPlusPlus {public static void main(String[] args) {int a = 1;System.out.println(a++); // 输出结果为1}}

这个再从字节码的层面分析一下。字节码内容省略,直接进入解释环节。
将数值1压入栈
将栈顶元素数值1存到本地变量a中
获取输出流System.out
将本地变量a的值1压入栈
将本地变量a的值自增1变成2
调用println,输出栈里的值1

③算术运算的时候,先算术运算后自增1。
public class TestPlusPlus {public static void main(String[] args) {int a = 1;int b = a++ + 1;System.out.println(b); // 输出结果为2}}

④逻辑运算的时候,先逻辑运算后自增1。
public class TestPlusPlus {public static void main(String[] args) {int a = 1;boolean b = a++ > 1;System.out.println(b); // 输出结果为false}}

然后看几个自增自减运算在其它知识点中的运用。

①自增自减和短路现象结合。
public class Test {public static void main(String[] args) {int a = 12, b = 20;if (a++ == 13 && ++b == 21) {System.out.println("ok");}System.out.println("a=" + a); // 输出结果为a=13;System.out.println("b=" + b); // 输出结果为b=20;}}

短路现象有两种,第一种是逻辑与运算时,如果第一个为false,则第二个不再计算。第二种是逻辑或运算时,如果第一个为true,则第二个不再计算。这样设计的初衷是为了减少不必要的计算,提高运行的效率,但是却形成了一种天然的“短路”现象,在某些特殊情况下,需要我们注意。上面的例子就属于第一种情况,在逻辑与运算时,a++==13已经不成立,那么程序就不会执行++b==12。
这里需要说明两点。
第一是不管是i++还是++i,当它执行过以后,i的值都自增了1。所以上面的例子中,程序走到a++==13的时候,是先把a的值12取出放到栈里,然后本地变量a的值12自增1变成了13,然后栈里的值12和13比的结果是不相等,所以跳出if语句。但是这里的代码执行过去以后,a的值已经变成了13。
第二是不管你在源码里写了i++还是++i,如果由于短路现象或者其他原因(如if选择语句判断结果为false时不执行)导致它不会被执行,那么它就不会引起变量i的变化。所以上面的例子中虽然出现了++b,但是由于短路现象的存在,++b并没有被执行,所以b的值一直没有变化,仍然是20。

②自增自减和do……while循环结合。
public class Test {public static void main(String[] args) {int i = 10;do {i++; // 第5行} while (i <= 10); // 第6行System.out.println(i); // 输出结果为11}}

我们都知道do……while循环是先执行一遍循环体,再判断条件是否符合,如果符合,继续执行循环体,以此类推,如果不符合,则立刻终止循环。在本例子中,通过一次进栈出栈操作,把10赋值给i,然后执行循环体。
循环体只有一个i++,也就是说本地变量i的值10自增1变成11,根本没有进栈操作。其实不管是在此处声明一个变量j,让j=i++;还是通过其它的办法,当执行i<=10的时候,本地变量i的值都会重新取出压入栈中。所以当进行条件判断时,i的值已经是11了,所以不满足条件,终止循环。之所以把这个例子拿出来,就是想说,凡是在代码中用到重新用到 i,则一定会重新将 i 的值取出进行压栈操作。所以如果在两次使用 i 的中间出现了 i 的自增自减操作,则第二次使用 i 的时候,它很可能已经不是上一次那个时间点的值了。
为了加深印象,可以看一下以下代码。
public class Test {public static void main(String[] args) {int a = 1;int b = a++ + a++;System.out.println("a=" + a); // 输出结果为a=3System.out.println("b=" + b); // 输出结果为b=3}}

如果单纯地套用“++在后,先使用后自增”,可能会无所适从,甚至可能得出错误的结果。实际上查看字节码以后它是按照下面步骤执行的。
将数值1压入栈
将栈顶元素数值1推出栈存入代号为1的局部变量中,即a中
将代号为1的局部变量的值1压入栈
将代号为1的局部变量的值1自增1变成2。此时局部变量a变成了2,栈中数值是1
将代号为1的局部变量的值2压入栈
将代号为1的局部变量的值2自增1变成3。此时局部变量a变成了3,栈中数值是2
将栈中的两个数值求和,得到3
将栈中的数值3存到代号为2的局部变量中,即b中
由此可见,即使在同一行里面,只要两次用a,它的此时此刻的值都会被重新读取到栈中。机器就是这么一步一步来的,没有一丁点的投机取巧。想必看了上面的解析,下面的代码的输出结果我们也都能计算出来了,而且无论怎么变形都无所谓。
public class Test {public static void main(String[] args) {int a = 1;int b = a++ + ++a;System.out.println("a=" + a); // 输出结果为a=3System.out.println("b=" + b); // 输出结果为b=4}}

③自增自减和for循环结合。
public class Test {public static void main(String[] args) {int a = 1;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {a++;}}System.out.println(a); // 输出结果为10}}

属于单纯两层循环,没什么好说的。

④自增自减和+=运算结合。
public class Test {public static void main(String[] args) {int x = 1, y = 2, z = 3;y += z-- / ++x;System.out.println(y); // 输出3}}

这个属于坑人的。看起来很复杂,其实相当于 y = y + (z--/++x)。按照“++在前,先自增后使用;++在后,先使用后自增”的规则,z--其实是3,++x其实是2,它们相除原本等于1.5,但是由于是两个int类型,其实得到的是1,然后y=y+1得到y的值是3。

综上所述,“++在前,先自增后使用;++在后,先使用后自增”的规则是完全成立的,但是我们要注意其中“使用”的含义都包含哪些场合,而且尽量通过字节码的层面去理解这个规则是怎么实现的。这样我们就能对i++和++i有更全面更深刻的理解。
原创粉丝点击