1. 场景的产生

先来看下下面代码展示的两个场景

@Test
void testIPP() {
int i = 0;
for (int j = 0; j < 10; j++) {
i = i++;
System.out.println(i);
}
}

@Test
void testPPI() {
int i = 0;
for (int j = 0; j < 10; j++) {
i = ++i;
System.out.println(i);
}
}

首先两个方法的开始都定义了一个int变量 i,并初始化为0,testIPP()方法里进行了一个for循环,循环内容为对i赋值,i=i++,并输出当前循环下的i的值,testPPI()方法里循环同样是对i赋值,但是为i=++i。 这两个方法一看很简单,不就是对每循环一次对变量 i 进行一次+1操作并打印i值吗,但是问题没有这么简单,可以先猜测一下这两个方法的打印结果。

 

实际打印结果:

testIPP():

0
0
0
0
0
0
0
0
0
0

testPPI():

1
2
3
4
5
6
7
8
9
10

 

可以看到testIPP方法里每次循环打印的都是0,这是与你想的不太一样,按道理结果应该是跟testPPI()一样的,从1打印到10,这是为什么呢,我们需要从JVM的字节码层面来看看这两个方法底层到底是怎么执行的。

 

2. 前置知识

jvm字节码指令

iconst_1:  将常量1压入操作数栈中,即栈顶位置。

istore_1 : 将当前操作数栈栈顶元素放到局部变量表中slot槽位置为1的地方.

iload_1: 将局部变量表中slot槽位置为1的变量压入操作数栈,即栈顶位置。

iinc 1 by 1: 将局部变量表中slot槽位置为1的变量进行自增操作,自增的数值为后面的1.

这里只解释下涉及的几个重要的字节码指令,其余的字节码指令暂时先不做解释。

 

3. 字节码分析

要想获得字节码指令文件,通常有两种方法,1是先将.java源文件编译为.class文件,然后再借助jdk提供的javap 反解析工具将.class文件解析成jvm执行的字节码指令,2是通过jclasslib 插件来查看字节码指令,可以在IDEA的插件商城中下载jclasslib 插件,编译.java文件后就能通过插件查看了,非常方便。

首先来看testIPP()的局部变量表:

 

 可以看到,在局部变量表slot槽为0的位置存放的当前对象的引用this,1的位置为变量i,j的位置为变量2.因为这是非静态方法,所以调用该方法时肯定是已经创建了实例,在jvm中所有非静态方法的栈帧中的局部变量表的第0个位置都会默认放置this.

接下来看核心的字节码指令

 0 iconst_0                    //将常量0压入操作数栈
 1 istore_1                    //将操作数栈元素放到局部变量表位置1的地方,即i=0
 2 iconst_0                    //将常量0压入操作数栈
 3 istore_2                    //将操作栈顶元素放到局部变量表位置2的地方,即j=0
 4 iload_2                     //将局部变量表2的位置的元素取出压入操作数栈,即取出j
 5 bipush 10                   //将10从字节转为int型并压入操作数栈
 7 if_icmpge 28 (+21)          //比较操作数栈中两者的大小,即for循环的判断 i<10
10 iload_1                     //此处就进入到for循环内部,从局部变量表1的位置取出压入操作数栈,即取出i,此时i=0
11 iinc 1 by 1                 //对局部变量表1的位置的元素进行自增+1,即i++;  注意到实际上此时i的值已经是1了
14 istore_1                    //将操作数栈顶元素放入局部变量表中位置1的地方,此时操作数栈顶为0,相当于i=0,又把前面自增的值给覆盖了
15 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>  //获取静态类System.out            
18 iload_1                     //将局部变量表位置1的值压入操作数栈顶,即i
19 invokevirtual #3 <java/io/PrintStream.println : (I)V>        //调用println打印i,所以每次循环一直都打印0
22 iinc 2 by 1
25 goto 4 (-21)
28 return

 

 

再来看下testPPI()的局部变量表:

可以看到跟testIPP()的位置都是一样的,没什么变化。

 

 

再来看看字节码指令:

可以看到前面的部分都是没有变化的,主要来看for循环的不同

 0 iconst_0
 1 istore_1
 2 iconst_0
 3 istore_2
 4 iload_2
 5 bipush 10
 7 if_icmpge 28 (+21)
10 iinc 1 by 1          //进入到for循环,将局部变量表中位置为1的变量进行自增+1的操作,即++i;此时i=1
13 iload_1              //将局部变量表1的位置压入操作数栈,即i,此时i=1
14 istore_1             //将操作数栈顶元素赋值到局部变量表1的位置即i=1, 可以看到此时是正常的自增操作,没有i=i++;的覆盖情况
15 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
18 iload_1              
19 invokevirtual #3 <java/io/PrintStream.println : (I)V>    //可以看到此时每次循环就会打印出自增的值
22 iinc 2 by 1
25 goto 4 (-21)
28 return

 

4. 总结

通过查看字节码指令,我们发现了i=i++; 和i=++i;的不同,i=i++;是先取i值(iload_1),然后自增(iinc 1 by 1),最后赋值(istore_1), 相当于先复制了一份i的副本,然后对原值自增,然后又把副本赋值给原值,这导致了自增一直被覆盖。而i=++i; 是先自增(iinc 1 by 1),然后取i值(iload_1),最后赋值(istore_1);这相当于先对原值自增,然后复制了一份副本, 最后把副本赋值给原值. 这就相当于每次拿的的副本都是最新的数据,所以每次自增都是正常。

 

5. 结尾

看了上面的解析,相信对内部的字节码指令执行已经有了清晰的了解,那么下面的代码允许结果是多少呢

    @Test
    void testIPP() {
        int i = 0;
        for (int j = 0; j < 10; j++) {
            i++;
        }
        System.out.println(i);
    }

    @Test
    void testPPI() {
        int i = 0;
        for (int j = 0; j < 10; j++) {
            ++i;
        }
        System.out.println(i);
    }

没错,都是10,这时for循环的内部内容其实都一样了,都是iinc 1 by 1了。

posted on 2021-11-30 15:57  Yuqi与其  阅读(232)  评论(1编辑  收藏  举报