深入理解 i++ 与 ++i 的区别
浅层理解
i++
是先进行赋值,再对变量i
进行加1操作
++i
是先对变量i
进行加1操作,在将i
的值赋值给其他变量
int i = 0;
int j, k;
System.out.println("k = " + (k = i++)); // k = 0
System.out.println("j = " + (j = ++i)); // j = 2
问题
int i = 0;
i = i++;
System.out.println("i = " + i); // ?
深入理解
运行时内存区
知道内存划分的可以跳过这部分
先简单介绍 java程序在运行时所用到的内存的划分
由于jdk版本的不同,jvm(运行java程序的平台)也不同,对内存的划分也不同,详情请系统学习JVM
- 简单分类及理解(理解本节必须掌握的内容)
- 堆:主要存放一些new出来的对象
- 方法区:jvm会将字节码文件(.class)中的信息保存的方法区Klass中,其中包含了类的所有信息
- 栈:采用栈的数据结构来管理方法的调用。
- 每个被调用的方法的数据信息(的引用)存放在栈帧中(,可根据引用在堆,方法区找到真正的数据内容)
- 方法执行流程
- 栈帧中的内容(主要掌握)
- 局部变量表:已数组的形式存放一个方法中出现的变量
- 操作数栈
- 帧数据(略)
- 详细分类(暂时不用深入理解,但早晚得掌握)
- 每个线程都拥有的独立的内存区域
- 程序计数器:用来记录指令执行到的为止
- 方法栈(与简单分类中的栈对应)
- java虚拟机栈
- 本地方法栈
- 所有线程共享的内存区域
- 堆
- 逻辑空间划分(可能听说过,所以列举出来)
- 永久代(jdk8移除)
- 年轻代(又可以分为伊甸园区、生存者区)
- 老年代(又分为普通对象老年代、大对象区)
- 存储内容划分
- new出来的对象
- Class对象(供反射使用)
- 字符串常量池(面试热点)
- 静态变量
- 。。。。
- 逻辑空间划分(可能听说过,所以列举出来)
- 方法区(jdk8版本前后变动较频繁的区域):略
- 堆
- 每个线程都拥有的独立的内存区域
方法的执行
public static void main(String[] args) {
method1();
}
pulic static void method1() {
method2();
}
public static void method2() {
// ...
}
main 方法先入站
main方法中执行到 method1() 时,method1() 方法入栈
method1()方法中method2()时,method2()方法入栈
随后,method2方法执行完,出栈;接着method1随m2方法执行完,也执行完,出栈
。。。
局部变量表与操作数栈
这两个部分都存放在栈帧中,每个方法对应一个栈帧,所有每个方法对应一个局部变量表和操作数栈
局部变量表
-
局部变量以数组的方式记录
-
举例
public static void oneMethod(int i, int j) { int k = i+j; }
在这个案例中,局部变量有 i,j,k ;可列下表
数组索引 0 1 2 数组内容 i 变量信息 j k
操作数栈
-
同样也采用栈这种数据结构
-
存放的是计算过程中的临时值
-
举例
-
还是以上面代码为例
public static void oneMethod(int i, int j) { int k = i+j; }
-
步骤
-
当调用这个方法时(如
oneMethod(1, 2)
),会为局部变量表中的i
和j
赋值,此时初始的局部变量表数组索引 0(i对应的索引) 1(j对应的索引) 2(k对应的索引) 对应的值 1 2 -
先读取局部变量表中
i
的值(即从索引0处读取)并放入操作数栈中,此时栈:1(栈顶) -
再读取局部变量表中
j
的值(即从索引1处读取)并放入操作数栈中,此时栈:2(栈顶) 1 -
执行相加操作(从栈顶取出两个数,相加得到一个数,再放入栈中),此时操作数栈:
3(栈顶) -
从栈顶取出一个数,将该数放到局部变量表k所对应的位置,此时局部变量表
数组索引 0(i对应的索引) 1(j对应的索引) 2(k对应的索引) 对应的值 1 2 3 -
该方法结束,对应的栈帧出 方法栈(注意与操作数栈区分)
-
-
-
举例2
int m = i + j + k
先从局部变量表中分别读取到 i 和 j 的值到操作数栈
执行相加操作得到临时值( i 与 j 相加的真实值)
再从局部变量表中读取到k的值到操作数栈
再与刚才的临时值相加得到临时值(i 与 j 与 k 相加的真实值)
从栈中取出该值存放到表中m的位置
i++ 与 ++i 的区别
终于进入主题
-
首先,我们写的 每个方法 会由 jvm翻译为一段段指令(暂且这么理解)
- 解释一些出现的指令(已第一段指令为例)
- iconst_1:将常量 1 放入操作数栈中
- istore_1:取出栈顶部数据,存到表中索引1的位置(索引0是args的位置,索引1即为变量a的位置)
- iload_1:从表中索引 1 的位置读取数据到 操作数栈
- iadd:取出栈顶部两个数据,相加,将和放入操作数栈中
- invokestatic #2:调用静态方法#2(即method()方法,#2是符号引用)
- 解释一些出现的指令(已第一段指令为例)
-
进入正题
-
补充一些指令 更多指令
- iinc 1 by 2 : 对局部变量表索引1号位的值加2(直接对局部变量表的操作)
- getstatic #1 : #1处静态字段值入栈
- putstatic #1 : 将栈顶部值存到#1静态字段处
- dup :复制栈顶部数据
-
i++
public static void main(String args) { int i = 0; i = i++; // System.out.println(i) // 0 } // 先读取一份未计算的值到局部变量表 // 虽然直接对局部变量表进行+1操作,但是又被操作数栈中的原始值覆盖 iconst_0 // 将0存到操作数栈中 栈:->[0] 数组:[args, i] istore_1 // 从栈中取出一个存到数组1号位 栈:->[] 数组:[args, i=0] iload_1 // 将数组1号位的值加载入栈 栈:->[0] 数组:[args, i=0] iinc 1 by 1 // 对数组1号位的值加1 栈:->[0] 数组:[args, i=1] istore_1 // 从栈中取出一个存到数组1号位 栈:->[] 数组:[args, i=0] return
-
++i
public static void main(String args) { int i = 0; i = ++i; // System.out.println(i) // 1 } // 先对局部变量表进行+1操作,再读取一份已计算的值到操作数栈,之后即使覆盖,也是用已计算的值覆盖 iconst_0 // 将0存到操作数栈中 栈:->[0] 数组:[args, i] istore_1 // 从栈中取出一个存到数组1号位 栈:->[] 数组:[args, i=0] iinc 1 by 1 // 对数组1号位的值加1 栈:->[] 数组:[args, i=1] iload_1 // 将数组1号位的值加载入栈 栈:->[1] 数组:[args, i=1] istore_1 // 从栈中取出一个存到数组1号位 栈:->[] 数组:[args, i=1] return
-
含静态字段的 i++
private static int number = 3; public static void add1() { return number++; } // 先复制 dup 一份未计算的值,后计算,此时会剩下一个值未+1 getstatic #1 // #1处静态字段值入栈 栈:->[3] 数组:[] dup // 复制栈顶部数据 栈:->[3, 3] 数组:[] iconst_1 // 栈:->[1, 3, 3] 数组:[] iadd // 让栈顶部两个数相加 栈:->[4, 3] 数组:[] putstatic #1 // 将栈顶部值存到#1处 栈:->[3] 数组:[] ireturn // 返回栈顶部值 栈:->[] 数组:[]
-
含静态字段的++i
private static int number = 3; public static void add1() { return ++number; } // 先计算,后复制dup,此时,两个值都是已+1的值 getstatic #1 // #1处静态字段值入栈 栈:->[3] 数组:[] iconst_1 // 栈:->[1, 3] 数组:[] iadd // 栈:->[4] 数组:[] dup // 复制栈顶部数据 栈:->[4, 4] 数组:[] putstatic #1 // 将栈顶部值存到#1处 栈:->[4] 数组:[] ireturn // 返回栈顶部值 栈:->[] 数组:[]
-
总结
交给你自己了