通过指令码来判断Java代码的执行顺序(++问题与return和finally的问题)
问题
在《深入理解Java虚拟机》一书中遇到了如下代码:
public int method() {
int i;
try {
i = 1;
return i;
} catch (Exception e) {
i = 2;
return i;
} finally {
i = 3;
}
}
由于曾经搜了一下return和finally的问题后,只是简单的看到了finally会执行,从而导致自己误以为只是简单地把finally的执行顺序放到return语句之前,因此判断这段代码的执行结果应该是3,可实际运行结果是1。研究后发现自己当初真是太糊涂,于是便记录下来。
工具
我们都知道,class文件中的内容就是可供JVM理解的字节码,JVM也是根据class的字节码来执行程序代码,所以class文件中就包含着程序代码最终的执行顺序。
我们可以通过官方提供的javap -c 再加上class文件的路径来得到各个方法对应的指令码。
例如:javap -c Test.class
引例
由于是打算使用JVM的指令码来解决这个问题,刚开始先以一个简单的方法来说明一下。对于如下方法:
public int method1() {
int i = 1;
return i;
}
该方法对应的指令码为:
public int method1();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: ireturn
每个指令对应着一个操作,上面的指令码意思是:
- 将int型数值0推送至栈顶
- 将栈顶int型元素存入第二个空间中
- 将第二个空间的int型元素推送至栈顶
- 返回将栈顶的int型元素并退出这个方法
由此可以看出,通过指令码,我们可以直观地看到程序代码的执行顺序,这对于解决任何执行顺序的问题是一个利器。
如果还是感觉有些不明所以,那我们可以再看看i++
和++i
的问题。对于如下代码:
// return 1
public int method2() {
int i = 1;
return i++;
}
// return 2
public int method3() {
int i = 1;
return ++i;
}
它们的指令码分别是:
public int method2();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: iinc 1, 1
6: ireturn
public int method3();
Code:
0: iconst_1
1: istore_1
2: iinc 1, 1
5: iload_1
6: ireturn
显然,这两段指令码最大的区别就是iinc 1,1
指令的位置不同,而且如果把这条指令删除,那么与method1
的指令码完全一致,对应源代码来看,这条指令就是++
这个符号的影响了。
而这个关键的iinc 1,1
指令的作用哪怕完全不懂也能猜出来,就是将第二个空间的int数据+1后再放回第二个空间。
将这个含义放到指令码中再重新捋一遍,以method2
为例:
- 将int型数值0推送至栈顶
- 将栈顶int型元素存入第二个空间中
- 将第二个空间的int型元素(1)推送至栈顶
- 将第二个空间的int数据+1后再放回第二个空间
- 返回将栈顶的int型元素并退出这个方法
需要注意的是,第三步是将1而不是整个空间推送至栈顶,所以第四步对第二个空间中的数据1加1后并没有改变栈顶的值,因此返回值为1。相对的,method2
则是:
- 将int型数值0推送至栈顶
- 将栈顶int型元素存入第二个空间中
- 将第二个空间的int数据+1后再放回第二个空间
- 将第二个空间的int型元素(2)推送至栈顶
- 返回将栈顶的int型元素并退出这个方法
所以,返回的是2。
解决
现在我们可以看最初的method
方法了,在这里再复制一遍代码:
public int method() {
int i;
try {
i = 1;
return i;
} catch (Exception e) {
i = 2;
return i;
} finally {
i = 3;
}
}
对应的指令码:
public int method();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: istore 4
5: iconst_3
6: istore_1
7: iload 4
9: ireturn
10: astore_2
11: iconst_2
12: istore_1
13: iload_1
14: istore 4
16: iconst_3
17: istore_1
18: iload 4
20: ireturn
21: astore_3
22: iconst_3
23: istore_1
24: aload_3
25: athrow
Exception table:
from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any
这段指令码不同的地方在于最后有一个异常表,我们先不用管它,先看到第一个ireturn
指令的指令码,即代码中的第9行为止的指令码:
0: iconst_1
1: istore_1
2: iload_1
3: istore 4
5: iconst_3
6: istore_1
7: iload 4
9: ireturn
这段指令码就是当没有异常时,程序执行的指令码,finally语句块的指令码已经包含在里面了:
- 将int型数值1推送至栈顶
- 将栈顶int型元素存入第二个空间中
- 将第二个空间的int型元素(1)推送至栈顶
- 将栈顶int型元素存入第五个空间中
- 将int型数值3推送至栈顶
- 将栈顶int型元素存入第二个空间中(3)
- 将第五个空间的int型元素(1)推送至栈顶
- 返回将栈顶的int型元素并退出这个方法
由此可以看出,方法返回的是第五个空间的1而不是第二个空间的3,和运行结果一致。
其中,关键的地方就是第四步以及第七步。由此可见,Java程序在执行时遇到return语句时,会先将方法的返回值保存起来,如果还有finally语句块,那么就先执行finally语句块,最后再将返回值取出后返回。
另外,如果return后跟的是表达式或者方法,那么会先计算出最终的返回值后再执行finally语句块,可自行验证。
当然,如果保存的返回值是一个引用类型的变量,那么在finally代码块中修改则会改变这个变量本身的属性,因而改变返回值的属性,毕竟finally的代码是的的确确执行过了。
例如,返回一个List,在finally中又对List进行了增加或删除,那么返回的List的内容自然也变了。
附加
关于指令码其余的部分,涉及到更多知识,在这里根据我的理解简单说一下。
这段指令码最后有一个异常表,它的含义可以简单解释为:在[from,to)的区间内,如果发生type类型的异常,那么就跳到target执行。
正因为有了异常表的存在,在出现异常时,程序可以根据产生的异常来跳到正确的位置执行接下来的代码。
[10,20]即为catch代码块对应的指令码,不过其中会把捕捉到的异常存储下来,也就是源代码中的Exception e
。[21,25]则是会把try语句块中抛出的catch没有捕捉的异常保存下来,然后执行finally的代码,最后抛出该异常结束方法。
这三片指令码都包含了finally的指令码,也就保证了源代码中finally的代码肯定会执行。
结论
Java程序在执行时遇到return语句时,会先将方法的返回值保存起来,如果还有finally语句块,那么就先执行finally语句块,最后再将返回值取出后返回。另外,如果return后跟的是表达式或者方法,那么会先计算出最终的返回值后再执行finally语句块。
笔记内容只是本人思考而写,如果有什么问题,还请指出,谢谢!
通过指令码来判断Java代码的执行顺序(++问题与return和finally的问题)的更多相关文章
- java代码块执行顺序
父类 public class Father { public Father() { System.out.println("父类构造PUBLIC father"); } stat ...
- Java基础系列5:Java代码的执行顺序
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 一.构造方法 构造方 ...
- Java——Java代码的执行顺序
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 一.构造方法 构造方 ...
- Java:面向对象(继承,方法的重写(overide),super,object类及object类中方法的重写,父子类代码块执行顺序)
继承: 1.继承是对某一匹类的抽象,从而实现对现实世界更好的建模. 2.提高代码的复用性. 3.extends(扩展),子类是父类的扩展. 4.子类继承父类可以得到父类的全部属性和方法.(除了父类的构 ...
- php课程 1-3 web项目中php、html、js代码的执行顺序是怎样的(详解)
php课程 1-3 web项目中php.html.js代码的执行顺序是怎样的(详解) 一.总结 一句话总结:b/s结构 总是先执行服务器端的先.js是客户端脚本 ,是最后执行的.所以肯定是php先执行 ...
- 创建HttpFilter与理解多个Filter代码的执行顺序
1.自定义的HttpFilter,实现Filter接口 HttpFilter package com.aff.filter; import java.io.IOException; import ja ...
- 2018.11.20-day22 类中代码的执行顺序&组合
1.类中代码的执行顺序 2.组合
- 90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(2)- TLAB预热
经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢? 面试的时候谈到 JVM,也有很多面试官喜欢问,为啥 Java 程序越执行越快呢? 一般人 ...
- Java中普通代码块,构造代码块,静态代码块执行顺序
//执行顺序:(优先级从高到低.)静态代码块>mian方法>构造代码块>构造方法. 其中静态代码块只执行一次.构造代码块在每次创建对象是都会执行. 1 普通代码块 1 //普通代码块 ...
随机推荐
- Windows提权与开启远程连接
1.提权: 建立普通用户:net user 帐户 密码 /add 提权成管理员:net localgroup administrators 帐户 /add 更改用户密码:net user 帐户 密码 ...
- 『宝藏 状态压缩DP NOIP2017』
宝藏(NOIP2017) Description 参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的m 条道路和它们的长度. 小明决 ...
- Eureka介绍
1. Eureka是什么 Eureka是一个基于REST的服务,主要用于AWS云中的定位服务,以实现中间层服务器的负载平衡和故障转移 在 Spring Cloud 微服务架构中通常用作注册中心 我们 ...
- Intent简介-Android开发
一.Intent介绍: Intent的中文意思是“意图,意向”,在Android中提供了Intent机制来协助应用间的交互与通讯,Intent负责对应用中一次操作的动作.动作涉及数据.附加数据进行描述 ...
- Lucene 03 - 什么是分词器 + 使用IK中文分词器
目录 1 分词器概述 1.1 分词器简介 1.2 分词器的使用 1.3 中文分词器 1.3.1 中文分词器简介 1.3.2 Lucene提供的中文分词器 1.3.3 第三方中文分词器 2 IK分词器的 ...
- leetcode — binary-tree-zigzag-level-order-traversal
import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Source : https://o ...
- JDK源码分析(10)之 Hashtable 相关
本文的目的并不是让你对Hashtable更加了解,然后灵活运用:因为Hashtable的一个历史遗留的类,目前并不建议使用,所以本文主要和HashMap对比,感受同样功能的不同实现,知道什么是好的代码 ...
- windows查看端口占用 windows端口占用 查找端口占用程序 强制结束端口占用 查看某个端口被占用的解决方法 如何查看Windows下端口占用情况
windows下查询端口占用情况 ,强制结束端口占用程序 查询8080端口被那个程序占用 如何强制结束windows下端口占用情况? 下面操作在win10下 在控制台执行命令 1.列出所有端口的情 ...
- C#组件系列——又一款日志组件:Elmah的学习和分享
前言:好久没动笔了,都有点生疏,12月都要接近尾声,可是这月连一篇的产出都没有,不能坏了“规矩”,今天还是来写一篇.最近个把月确实很忙,不过每天早上还是会抽空来园子里逛逛.一如既往,园子里每年这个时候 ...
- C#工具:防sql注入帮助类
SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编程时的疏忽,通过SQL语句,实现无帐号登录,甚至篡改数据库. using System; using Sy ...