javac进行编辑源文件,生成 class 字节码二进制文件。解读 class 字节码文件当中的字节码指令,可以帮助我们更好理解程序执行过程的机理。
关于前置加加、后置加加,我们通常记得的是先加1再操作、先操作再加1。在本文中,我们将以 Java底层真正执行的字节码指令角度更好理解为什么是这样,在一些比较复杂的判断执行先后顺序的时候使用字节码指令进行判断会更加的简单!
1.前置了解的知识
1.1 栈这种数据结构
- 栈是一个**先入后出(FILO-First In Last Out)**的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的 一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元 素最先删除,最先放入的元素最后删除。
1.2 局部变量表和操作数栈
每个方法在被调用时都会分配一个独立的空间,该空间中又包括 局部变量表 和 操作数栈 两个部分。
- 局部变量表 用来存储方法中定义的局部变量、方法参数等等,它是在编译时确定大小的,具体的大小可以在字节码中看到。
- 操作数栈 用来存储方法执行中的操作数据,操作数栈是一个后进先出(LIFO)的数据结构,Java 虚拟机在执行指令时会将数据压入操作数栈中,然后再从栈中取出数据进行计算。
1.3 三个字节码指令
public class ReadClass{ public static void main(String[] args){ int i = 10; } }
编译生成:ReadClass.class
如何查看字节码?javap -c ReadClass.class,以上程序字节码如下:
public class ReadClass { public ReadClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); // 主要分析下面的指令 Code: 0: bipush 10 2: istore_1 3: return }
重点研究 main 方法中的字节码含义:
bipush 10
指令:将字面量 10 压入操作数栈。istore_1
指令:将操作数栈中顶部数据弹出,然后将该数据存放到局部变量表的第1个位置(第0个位置存储的方法的参数args)。return
指令:方法结束。
public class ReadClass{ public static void main(String[] args){ int i = 10; int j = i; } }
编译生成:ReadClass.class,再 javap -c ReadClass.class,以上程序字节码如下:
public class ReadClass { public ReadClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: // 将 字面量 10 压入操作数栈 0: bipush 10 // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 1 个位置,即完成了将 10 赋值给 i 的操作 2: istore_1 // 将局部变量表中第 1 个位置存储的数据复制一份,放到操作数栈当中。 3: iload_1 // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 2 个位置,即完成了将 10 赋值给 j 的操作 4: istore_2 // 方法结束 5: return }
iload_1
指令:将局部变量表中第1个位置存储的数据复制一份,放到操作数栈当中。istore_2
指令:将操作数栈顶部数据弹出,将其存放到局部变量表的第2个位置上。
2.单独使用后置++与前置++
由于 ++ 与 – 原理相同,这里就以 ++ 为例进行演示。
2.1 后置++字节码指令
public class ReadClass{ public static void main(String[] args){ int i = 10; i++; } }
编译生成:ReadClass.class,再 javap -c ReadClass.class,以上程序字节码如下:
public class ReadClass { public ReadClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: // 将 字面量 10 压入操作数栈 0: bipush 10 // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 1 个位置,即完成了将 10 赋值给 i 的操作 2: istore_1 // 将局部变量表第 1 个位置的数据加 1,即从 10 变成了 11 3: iinc 1, 1 // 方法结束 6: return }
iinc 1, 1
指令:将局部变量表中第1个位置数据加1
2.2 前置++字节码指令
public class ReadClass{ public static void main(String[] args){ int i = 10; ++i; } }
编译生成:ReadClass.class,再 javap -c ReadClass.class,以上程序字节码如下:
public class ReadClass { public ReadClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: // 将 字面量 10 压入操作数栈 0: bipush 10 // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 1 个位置,即完成了将 10 赋值给 i 的操作 2: istore_1 // 将局部变量表第 1 个位置的数据加 1,即从 10 变成了 11 3: iinc 1, 1 // 方法结束 6: return }
iinc 1, 1
指令:将局部变量表中第1个位置数据加1
2.3 总结
分析了单独使用前置++和后置++的指令,我们发现字节码指令是一样的,实际上都是将局部变量表对应位置的数据进行加1操作。
🚩 当单独使用++
或 --
时,不需要关心其返回值,因此前置和后置的效率是一样的。实际上,在编译时,编译器可能会将单独使用的 ++
或 --
运算符优化为一条简单的指令 iinc
,因此在机器指令级别上,它们的执行效率是相同的。
3.需要返回值的情况下使用后置++与前置++
3.1 后置++字节码指令
public class ArithmeticOperator { public static void main(String[] args) { /* 后置 ++ 字节码指令: public class ArithmeticOperator { public ArithmeticOperator(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: istore_1 3: iload_1 4: iinc 1, 1 7: istore_2 8: return } */ /* 0: bipush 10:将数据 10 放到操作数栈中 2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置 */ int i = 10; /* 3: iload_1 将局部变量表第1个位置的数据 10 复制一份放入到操作数栈 4: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11 7: istore_2 将操作数栈顶数据 10 弹出赋值给变量k,即存到局部变量表第2个位置 */ int k = i++; } }
我们可以看到在 int k = i++; 这条语句中,实际执行了三个字节码指令:
iload_1
:将局部变量表第1个位置的数据 10 复制一份放入到操作数栈。iinc 1, 1
:将局部变量表第1个位置的数据 10 自加1 变为 11。istore_2
:将操作数栈顶数据 10 弹出赋值给变量k,即存到局部变量表第2个位置。
因此,我们在谈到 后置++ 时,通常说是 先操作再加1 ,那实际上:
- 这个“先操作”从字节码指令的角度看就是先将局部变量表的对应数据复制一份压入操作数栈;
- “再加1”就是再将局部变量对应数据加1,而操作数栈中保存的数据还是原本数据。
- 紧接着的 对k的赋值 操作实际是 从操作数栈顶弹出原本数据存储到局部变量表即赋值给k。
3.2 前置++字节码指令
public class ArithmeticOperator { public static void main(String[] args) { /* public class ArithmeticOperator { public ArithmeticOperator(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: istore_1 3: iinc 1, 1 6: iload_1 7: istore_2 8: return } */ /* 0: bipush 10:将数据 10 放到操作数栈中 2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置 */ int i = 10; /* 3: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11 6: iload_1 将局部变量表第1个位置的数据 11 复制一份放入到操作数栈 7: istore_2 将操作数栈顶数据 11 弹出赋值给变量k,即存到局部变量表第2个位置 */ int k = ++i; } }
3.3 总结
在需要返回值的情况下我们比较发现:
- 后置++(先操作再加1):先复制一份局部变量表对应数据压入到操作数栈,再将局部变量表对应数据加1。
- 前置++(先加1再操作):先将局部变量表对应数据加1,再复制一份局部变量表对应数据压入到操作数栈。
这里就可以看出压入到操作数栈的数据是不同的,那么最后弹出 操作数栈顶的该数据 作为 返回值 进行 赋值操作的结果也是不同的。
3.4 练习
🍀 练习一
int a = 5; int b = a++; // 先复制一份数据压入操作数栈,再将局部变量表数据+1,最后从栈中弹出数据作为返回值赋值给 b System.out.println("b = " + b); // 5 b = a++; System.out.println("a = " + a); // 7 System.out.println("b = " + b); // 6 int c = 10; int d = --c; // 先将数据-1,再复制一份压入操作数栈,再从栈中弹出该数据作为返回值赋值给 d System.out.println("c = " + c); // 9 System.out.println("d = " + d); // 9
🍀 练习二
int i = 10; /* 等式右边从左向右执行: - 先 i++:复制一份数据 10 到操作数栈,将局部变量表数据+1变为11,弹出操作数栈中数据 10 作为返回值,即 i++ 的返回值是 10,i变成了 11 - 再 ++i:将局部变量表数据 11 加1变为 12,复制一份数据 12 到操作数栈,弹出栈中数据 12 作为返回值,即 ++i 的返回值是 12,i变成了 12 - 最后 10 + 12 得到 22 赋值给 k */ int k = i++ + ++i; System.out.println(k); // 22 int f = 10; /* 等式右边从左向右执行: - 先 f++:复制一份数据 10 到操作数栈,将局部变量表数据+1变为11,弹出操作数栈中数据 10 作为返回值,即 f++ 的返回值是 10,f变成了 11 - 即 ( f++ + f ) 变为了 ( 10 + f ):此时 f 的值变成了 11 ,因此 将 (10 + 11) 的结果赋值给 m - 最后 10 + 12 得到 22 赋值给 k */ int m = f++ +f; System.out.println(m); // 21 System.out.println(f); // 11
4.⭐ 经典面试题
4.1 后置++
/* 0: bipush 10:将数据 10 放到操作数栈中 2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置 */ int i = 10; /* 3: iload_1 将局部变量表第1个位置的数据 10 复制一份放入到操作数栈 4: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11 7: istore_1 将操作数栈顶数据 10 弹出放到局部变量表第1个位置即将 10 赋值给变量i */ i = i++; System.out.println(i); // 10
4.2 前置++
/* 0: bipush 10:将数据 10 放到操作数栈中 2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置 */ int i = 10; /* 3: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11 6: iload_1 将局部变量表第1个位置的数据 11 复制一份放入到操作数栈 7: istore_1 将操作数栈顶数据 11 弹出放到局部变量表第1个位置即将 11 赋值给变量i */ i = ++i; System.out.println(i); // 11