一、前言
想写这篇博客,主要是最近在leetcode刷了些题,然后遇到数组中自增位置的选择,感觉挺有意思的,于是就分析下原理。
我前一篇介绍i++的文章链接:https://blog.csdn.net/weixin_44765605/article/details/110746423?spm=1001.2014.3001.5501
二、测试代码
- 大家可以想一下下面的数组输出是什么。
- 测试代码:
public static void main(String[] args) {
int length = 10;
int[] array = new int[length];
for (int i = 0; i < length; i++) {
array[i] = i;
}
int index = 0;
array[index++] = array[index] + 1;
System.out.println(Arrays.toString(array));
array[index] = array[index++] + array[index];
System.out.println(Arrays.toString(array));
array[index] = array[index] + array[++index] + array[index];
System.out.println(Arrays.toString(array));
}
- 测试结果:
[2, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[2, 3, 2, 3, 4, 5, 6, 7, 8, 9]
[2, 3, 8, 3, 4, 5, 6, 7, 8, 9]
三、从字节码上解释
- 字节码:
0 bipush 10
2 istore_1
3 iload_1
4 newarray 10 (int)
6 astore_2
7 iconst_0
8 istore_3
9 iload_3
10 iload_1
11 if_icmpge 24 (+13)
14 aload_2
15 iload_3
16 iload_3
17 iastore
18 iinc 3 by 1
21 goto 9 (-12)
24 iconst_0
25 istore_3
26 aload_2
27 iload_3
28 iinc 3 by 1
31 aload_2
32 iload_3
33 iaload
34 iconst_1
35 iadd
36 iastore
37 getstatic #2 <java/lang/System.out>
40 aload_2
41 invokestatic #3 <java/util/Arrays.toString>
44 invokevirtual #4 <java/io/PrintStream.println>
47 aload_2
48 iload_3
49 aload_2
50 iload_3
51 iinc 3 by 1
54 iaload
55 aload_2
56 iload_3
57 iaload
58 iadd
59 iastore
60 getstatic #2 <java/lang/System.out>
63 aload_2
64 invokestatic #3 <java/util/Arrays.toString>
67 invokevirtual #4 <java/io/PrintStream.println>
70 aload_2
71 iload_3
72 aload_2
73 iload_3
74 iaload
75 aload_2
76 iinc 3 by 1
79 iload_3
80 iaload
81 iadd
82 aload_2
83 iload_3
84 iaload
85 iadd
86 iastore
87 getstatic #2 <java/lang/System.out>
90 aload_2
91 invokestatic #3 <java/util/Arrays.toString>
94 invokevirtual #4 <java/io/PrintStream.println>
97 return
-
注意,下面的字节码解释都是根据官网来的:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
-
刚进入
main
方法的时候大概是这样的:
-
为什么局部变量表只有四个而不是三个呢?那是因为前面
for
循环的i
后面不用了,于是就把index
放那里了,这应该是jvm的优化。 -
此时通过字节码中的0-8标号(以下直接用数字不会再加标号),新建了一个数组,同时也初始化了三个变量到局部变量表中。
- 0-8中还有些操作是在操作数栈中完成的,这里因为不是重点就不画了。
- 然后9-21步就是给数组中元素赋值的过程,这里也不画了。
-
以下是第27步
iconst_0
开始前的场景:
-
对于
array[index++]
这一行代码,它肯定是先在前面执行的(赋值是最后做的),array[index++]
对应的是26-28步,但是这里用了比较巧妙的方式去自增:- 第26步和27步,将数组地址和
index
入操作数栈:
- 第26步和27步,将数组地址和
-
第28步,重点来了,后自增的发生就在这一步,发生在数组地址和索引入操作数栈后,局部变量表的
index
加1了:
- 然后31
aload_2
和32iload_3
这两步,把局部变量表位置的值放入操作数栈中:
-
由上图可以看出
array[index++] = array[index] + 1;
的第二个array[index]
,它的index
其实已经是1了,所以array[index++] = array[index] + 1;
这一行结束后,弹栈后的运算相当于:对0x0001的数组第0索引位置,赋值:0x0001数组第1索引位置的值+1的值,因此出现的就是数组索引0的位置的值是1+1=2的情况。 -
对于操作数栈中的运算,通过第33步的
iaload
弹出前两个元素组合成一个数组索引的位置array[1]
,再根据这个位置获取array[1]
的值并压到操作数栈中,再通过iconst_1
将常数1压入操作数栈中,然后通过第35步iadd
弹出栈顶两个元素的值进行相加得到的结果压入操作数栈中,再通过第36步iasotre
将栈顶三个元素弹出,第二次弹出的索引和第三次弹出的数组地址组合成一个地址array[0]
,并将第一次弹出的value(前面计算好的并放在栈顶的值)赋值给那个数组的位置。(后面赋值操作类似,就不讲了,主要还是自增这方面) -
对于
array[index] = array[index++] + array[index];
也同理,第三个index
因为用的是第二个index
自增后的值,因此这一步就是array[1] = array[1] + array[2]
,原理同上,所以array[1]
的值就是1+2=3; -
那么对于
array[index] = array[index] + array[++index] + array[index];
中间的前自增要怎么理解呢? -
我们直接找到第70步,第70步到第74步都和前面一样,压入数组地址和索引,第75步
iaload
是因为遇到+
,因此要提前获取到值(就是要先算,这里大概是运算符的优先级影响的,但与主题无关)放入操作数栈中,此时虚拟机栈和堆中的简图是这样的:
-
重点来了,现在不是先将索引
index
先入栈,前自增是在索引index
入栈前,先自增,前自增的位置还是局部变量表,第76步iinc 3 by 1
,局部变量表位置3的值+1变为3:
- 然后前自增结束,第77步
iload_3
才将局部变量表位置3的值3压入操作数栈中:
-
图中的操作数栈,0:数组地址,1:数组索引
index
2;2:array[2]
的值2;3:数组地址;4:数组索引index
2。 -
中间的步骤就不画了,都是差不多的;
-
因此最后这一行可以看成:
array[2] = array[2] + array[3] + array[3];
;因此array[2]
的值就是8。
三、我的用法
- 在刷题的时候,对于如下情况:
array[index] = array[index] + array[index];
index++;
可以一行就写完,同时也知道index++
的最适合位置在哪:
array[index] = array[index] + array[index++];