从面试题i++和++i来看java字节码

从面试题i++和++i来看java字节码

我们先来看一道经典面试题

1
2
3
4
5
6
7
8
public class Demo05 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}

使用

-v Demo05.class```进行反编译. -v表示输出类文件的详细信息
1
2


Classfile /E:/workplace/abc/jvmStudy/out/production/jvmStudy/demo1/Demo05.class
Last modified 2020-2-23; size 567 bytes
MD5 checksum e378885aa1eef9f5fd48fda536b4467b
Compiled from “Demo05.java”
public class demo1.Demo05
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:

#1 = Methodref #5.#22 // java/lang/Object.”“:()V

#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;

#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V

#4 = Class #27 // demo1/Demo05

#5 = Class #28 // java/lang/Object

#6 = Utf8

#7 = Utf8 ()V

#8 = Utf8 Code

#9 = Utf8 LineNumberTable

#10 = Utf8 LocalVariableTable

#11 = Utf8 this

#12 = Utf8 Ldemo1/Demo05;

#13 = Utf8 main

#14 = Utf8 ([Ljava/lang/String;)V

#15 = Utf8 args

#16 = Utf8 [Ljava/lang/String;

#17 = Utf8 a

#18 = Utf8 I

#19 = Utf8 b

#20 = Utf8 SourceFile

#21 = Utf8 Demo05.java

#22 = NameAndType #6:#7 // ““:()V

#23 = Class #29 // java/lang/System

#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;

#25 = Class #32 // java/io/PrintStream

#26 = NameAndType #33:#34 // println:(I)V

#27 = Utf8 demo1/Demo05

#28 = Utf8 java/lang/Object

#29 = Utf8 java/lang/System

#30 = Utf8 out

#31 = Utf8 Ljava/io/PrintStream;

#32 = Utf8 java/io/PrintStream

#33 = Utf8 println

#34 = Utf8 (I)V
{
public demo1.Demo05();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.”“:()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo1/Demo05;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 18
line 8: 25
line 9: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
}
SourceFile: “Demo05.java”

1
2
3
4



现在我们逐行来分析反编译后的字节码

Classfile /E:/workplace/jvmStudy/out/production/jvmStudy/demo1/Demo05.class //字节码原文件所在位置
Last modified 2020-2-23; size 567 bytes //最后修改时间,以及文件大小
MD5 checksum e378885aa1eef9f5fd48fda536b4467b //MD5签名
Compiled from “Demo05.java” //字节码文件是由哪个文件编译得来的
public class demo1.Demo05 //类名
minor version: 0 // 小版本号
major version: 52 //大版本号 52 对应java8
flags: ACC_PUBLIC, ACC_SUPER //ACC_PUBLIC表示类的访问修饰符为Public ACC_SUPER,具体见表1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17



## 表1 类访问标记(access flags)

| Flag Name | Value | Interpretation |
| -------------- | ----- | ------------------- |
| ACC_PUBLIC | 1 | 标识是否是 public |
| ACC_FINAL | 10 | 标识是否是 final |
| ACC_SUPER | 20 | 已经不用了 |
| ACC_INTERFACE | 200 | 标识是类还是接口 |
| ACC_ABSTRACT | 400 | 标识是否是 abstract |
| ACC_SYNTHETIC | 1000 | 编译器自动生成,不是用户源代码编译生成 |
| ACC_ANNOTATION | 2000 | 标识是否是注解类 |
| ACC_ENUM | 4000 | 标识是否是枚举类 |

---

Constant pool:

#1 = Methodref #5.#22 // java/lang/Object.”“:()V

#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;

#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V

#4 = Class #27 // demo1/Demo05

#5 = Class #28 // java/lang/Object

#6 = Utf8

#7 = Utf8 ()V

#8 = Utf8 Code

#9 = Utf8 LineNumberTable

#10 = Utf8 LocalVariableTable

#11 = Utf8 this

#12 = Utf8 Ldemo1/Demo05;

#13 = Utf8 main

#14 = Utf8 ([Ljava/lang/String;)V

#15 = Utf8 args

#16 = Utf8 [Ljava/lang/String;

#17 = Utf8 a

#18 = Utf8 I

#19 = Utf8 b

#20 = Utf8 SourceFile

#21 = Utf8 Demo05.java

#22 = NameAndType #6:#7 // ““:()V

#23 = Class #29 // java/lang/System

#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;

#25 = Class #32 // java/io/PrintStream

#26 = NameAndType #33:#34 // println:(I)V

#27 = Utf8 demo1/Demo05

#28 = Utf8 java/lang/Object

#29 = Utf8 java/lang/System

#30 = Utf8 out

#31 = Utf8 Ljava/io/PrintStream;

#32 = Utf8 java/io/PrintStream

#33 = Utf8 println

#34 = Utf8 (I)V

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

上面类的内容为该类的常量池

1. 第一列 #+数字代表常量池的编号
2. 第二列 Utf8/NameAndType........ 代表常量的类型
3. 第三列表示常量的具体数值,如果是#+数字,表示引用其他常量数据

---

然后我们来看下构造方法,虽然我们没有显示的写一个构造方法,但是在编译的时候,会默认创建一个无参的构造方法,

从下面的字节码可以看出:

```java
public demo1.Demo05(); //Demo05方法
descriptor: ()V //参数为空,返回值为void ---V代表Void 也就是无返回值
flags: ACC_PUBLIC //表示方法的访问标记,是 public、private 还是 protected,是否是 static,是否是 final 等 具体见表1。
Code: // 方法体
stack=1, locals=1, args_size=1 // 栈帧深度为1 局部变量表中变量数为1 该方法的形参个数。如果是实例方法,第一个形参是this引用。
0: aload_0 //aload 表示加载本地变量表的数据,这里表示加载本地变量表中第一条数据,为this
1: invokespecial #1 // Method java/lang/Object."<init>":()V
// 调用Object.init方法
4: return //返回
LineNumberTable: //行号表,表示Code的字节码对应代码的行号
line 3: 0


/*
* 局部变量表
* start表示从Code的字节码第几行开始创建该局部变量
* Length表示变量的生命周期 code字节码中的(0-4),所以为5
* Slot 表示变量的序号
* Name 表示变量名
* Name 表示变量引用
*/
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo1/Demo05;

备注:每个方法会创建一个栈帧.类似于一个先进后(FILO)出的队列

表2 方法访问标记(access flags)

方法的访问标记比类和字段的访问标记类型更丰富,有 12 种之多,如小表所示:

方法访问标记 描述
ACC_PUBLIC 0x0001 声明为 public
ACC_PRIVATE 0x0002 声明为 private
ACC_PROTECTED 0x0004 声明为 protected
ACC_STATIC 0x0008 声明为 static
ACC_FINAL 0x0010 声明为 final
ACC_SYNCHRONIZED 0x0020 声明为 synchronized
ACC_BRIDGE 0x0040 bridge 方法, 由编译器生成
ACC_VARARGS 0x0080 方法包含可变长度参数,比如 String… args
ACC_NATIVE 0x0100 声明为 native
ACC_ABSTRACT 0x0400 声明为 abstract
ACC_STRICT 0x0800 声明为 strictfp,表示使用 IEEE-754 规范的精确浮点数,极少使用
ACC_SYNTHETIC 0x1000 表示这个方法是由编译器自动生成,而不是用户代码编译产生

重点来了

下面我们来看Main方法的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static void main(java.lang.String[]);     //main方法
/*
* 方法参数为String[],最左面的[表示为数字,二维数组则为[[, --L***;--表示引用类型
* 返回值为V 也就是前面说的Void
*/
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC //方法访问表示,Public static
Code: // 方法具体的字节码
stack=2, locals=3, args_size=1 // 栈帧深度为2,方法变量表变量数为3,该方法的形参个数。因为是静态方法,不需要类引用,也就不需要this,所以只有一个String[]
0: bipush 10 //加载常量 10
2: istore_1 //将10放入局部变量表序号为1的槽位中
3: iload_1 // 将局部变量表序号为1的数据加载到栈帧(1),此时数据为10
4: iinc 1, 1 //innc 表示在局部变量表中进行自增运算,第一个1表示局部变量表中槽位的序号,第二个数表示自增的数值
7: iinc 1, 1 // 在执行一次自增 执行过后局部变量表1槽位的数为 12
10: iload_1 // 将局部变量表序号为1的数据加载到栈帧(2),此时数据为12
11: iadd //iadd 表示取出栈顶两个数,进行相加,然后放入栈中 执行过后 栈中只有一个数,22
12: iload_1 // 将局部变量表序号为1的数据加载到栈帧(2),此时数据为12
13: iinc 1, -1 // 进行自减,在局部变量表中操作,不影响栈中数据,局部变量表中数改为11
16: iadd //将栈顶两个数相加,为34,并将结果放入栈顶
17: istore_2 // 将栈顶数字取出 放入局部变量表为2的槽位
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; // 引用常量池2号的常量,为System.out类
21: iload_1 //将局部变量表中1槽位的11加载入栈
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
//调用打印方法(后面方法与前面相同,就不详细解释了)
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 18
line 8: 25
line 9: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I

从上面的字节码可以看出,i++ 和++i 的操作与其他操作不同

  1. 直接在局部变量表中操作.
  2. i++ 是先从局部变量表中加载到栈中,然后再局部变量表中运算
  3. ++i 是先在局部变量表中运算,然后再加载到栈中

这就是为什么i++ 先返回,后相加;++i是先相加后返回.也就是上面面试题打印结果为11,34的原因.


以上仅为个人观点,如有错误,欢迎指正.谢谢!

0%