第二节
- JVM如何调用方法
- JVM的异常处理
JVM调用方法
如果说,在类中的方法名都是不同,那对于JVM来讲是十分友善的.
但是对处女座的程序员来讲,就是灾难了.
起个名字是很难的好吧.要是想到要起那么多的函数名,我就脑瓜疼.
所以,为了我们爽,就得让机器麻烦点.于是就有了重载和重写
方法的重载与重写
先回顾一下什么是重载与重写.
重载
在Java语言中,当方法名相同时,会出现重载.Java要在JVM上运行,自然JVM也有重载.
但是,在两者之间有些不同.
方法签名 | |
---|---|
Java语言 | 类名+方法名+参数类型及个数 |
JVM | 类名+方法名+参数类型及个数+返回值类型 |
JVM会根据返回值类型的不同,加以区别.所以只有连返回值类型都相同,JVM才会重写的.
但是,我们在Java程序中重写,但是在JVM中却不认为是重写.怎么办?
不用担心编译器会帮我们做好了桥接方法.
下面的链接,对桥接方法有个简单的介绍,这里不展开说了.
重写
重写一般存在于
- 重写接口中的方法
- 父类中签名相同的非静态方法
(静态方法则是将父类方法隐藏)
JVM在选取重载方法时,最多会采用三次选取
如果选取到,就不再选取.
查询方法
最多会查询三次,一般不存在第三次选不到的情况.因为Java编译会报错的.
当然,你如果要用字节码工具修改,也是可以的…
是否考虑拆装箱 | 是否考虑可变参数 | |
---|---|---|
第一次 | 否 | 否 |
第二次 | 是 | 否 |
第三次 | 是 | 是 |
但是,也会有一个疑问.如果在一个参数(例如null),
在第N次中找到了多个匹配的合适的方法.怎么办啊?
那就选取最合适的一个呗.(子类比父类更合适)
静态绑定与动态绑定
静态绑定
在编译时,就能够确定的方法才能被称为静态绑定.
例如
- private 私有的,肯定能确定啊
- static 类方法,只能被隐藏,不能被重写
- final 可以继承,但是不能重写
动态绑定
只有程序运行时,才能够根据参数的不同,来调用不同的方法.
怎么寻找最合适的方法呢.根据下面的顺序,依次寻找.
类
- 在本类中寻找
- 在父类中寻找
- 在直接或间接实现的接口中找非私有非静态的方法
接口
- 本接口
- Object中存在的方法
- 超接口的方法
每次寻找还要结合着重载中提到的考虑装箱不装箱和考虑有没有可变不可变参数
因此这样的查询有些消耗资源.有没有什么办法,可以快速地查询到要找到的方法呢?
不如建一个方法表.等参数来了,就根据方法表来指定使用哪一个方法.
是不是很机智的方法?
方法表
为了更好的说明,写点代码.下面的内容纯属玩笑,并非地域黑.
定义一个抽象类 Ren
public abstract class Ren {
public abstract void eat();
@Override
public String toString() {
return "Ren [getClass()=" + getClass() + ", hashCode()=" + hashCode() + ", toString()=" + super.toString()
+ "]";
}
}
定义东北人和广东人
public class DongBeiRen extends Ren{
@Override
public void eat() {
System.out.println("一天三顿小烧烤");
}
public void bath() {
System.out.println("泡澡");
}
}
public class GuangDongRen extends Ren{
@Override
public void eat() {
System.out.println("胡建人");
}
}
测试
public class Test {
public static void main(String[] args) {
Ren ren = new GuangDongRen();
Ren ren2 = new DongBeiRen();
ren.eat();
ren2.eat();
DongBeiRen dbr = (DongBeiRen)ren2;
dbr.bath();
}
}
方法表示例
Ren的方法表
方法 | index |
---|---|
toString() | 0 |
eat() | 1 |
广东人
方法 | index |
---|---|
toString() | 0 |
eat() | 1 |
东北人
方法 | index |
---|---|
toString() | 0 |
eat() | 1 |
bath() | 2 |
我们可以看出来,继承父类有的方法,要与父类中index相同的.
这样可以方便检索.是不是很方便
JVM 处理异常
说道异常,我们要知道异常的继承关系.
我一般把异常简单地分为两种.
- 检查异常
- 运行时异常
异常的构建,其实是非常消耗资源的.你可以回忆一下,当你的控制台出现异常.
是不是会有一堆信息提示你,哪里可能出现了错误.
抄了一个同学的回答,他总结的很好.
使用异常捕获的代码为什么比较耗费性能?
因为构造异常的实例比较耗性能。这从代码层面很难理解,
不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。
这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,
方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。
虽然具体不清楚JVM的实现细节,但是看描述这件事情也是比较费时费力的。
finally是怎么实现无论异常与否都能被执行的?
这个事情是由编译器来实现的,
现在的做法是这样的,编译器在编译Java代码时,会复制finally代码块的内容,
然后分别放在try-catch代码块所有的正常执行路径及异常执行路径的出口中
JVM中对异常的处理有些复杂,我要再看两天… 2019年2月10日
JVM中的异常处理
两大要素
异常处理的两大要素,控制了程序的非正常转移
- 抛出异常
- 捕获异常
异常的抛出
-显式:通过 throw
关键字,手动抛出异常实例
-隐式:JVM 运行到无法运行的地方,自动抛出异常
常见形式
- try-catch-finally
- fianlly 的实现,JVM中将其中的代码分别放到出现异常和不出现异常的出口后
- try-with-resources
- 语法糖,可以简化资源的关闭时异常的处理
异常继承关系
其中RuntimeException 和 Error 属于Java中的非检查异常
其它属于检查异常,所有检查异常需要程序显式地捕获或者在方法
中声明 throws .
异常实例的构造十分的昂贵.
这是由于构造异常实例时,JVM需要生成该异常的栈轨迹(Stack trace).
需要记录各种调试的信息,包括方法名,在哪行等等.