JVM(复习)方法调用
文章目录
一,方法重载
何为静态类型,何为实际类型?
static class GrandFather{
}
static class Father extends GrandFather{
}
static class Child extends Father{
}
上面声明了三个类型:
GrandFather grandFather = new Father();
grandFather的静态类型是GrandFather,而grandFather的实际类型是真正指向的类型是Father,变量的静态类型是不会发生变化的,而变量的实际类型是可以发生变化的(多态),实际类型在运行期确定
所有依赖静态类型来定位执行哪一个方法的动作就叫做静态分派
public void test(GrandFather grandFather){
System.out.println("GrandFather");
}
public void test(Father Father){
System.out.println("Father");
}
public void test(Child Child){
System.out.println("Child");
}
//方法重载是一种静态分派行为,在编译期可以确定
public static void main(String[] args) {
//
GrandFather father = new Father();
GrandFather child = new Child();
Father father1 = new Father();
Test1 test1 = new Test1();
test1.test(father); // GrandFather
test1.test(father1);// Father
test1.test(child);// GrandFather
}
所以,不难得多,根据静态分派规则看静态类型执行相应的重载的方法
重载方法的匹配优先级
在很多情况下,重载方法的版本并不是唯一,选择调用的那个重载方法只是当前情况下最合适的一个而已
public void say(Object arg){
System.out.println("Object");
}
public void say(int arg){
System.out.println("int");
}
public void say(long arg){
System.out.println("long");
}
public void say(char arg){
System.out.println("char");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
我先这样执行:
public static void main(String[] args) {
Test1 test1 = new Test1();
test1.say('a');
}
很容易会想到输出char,因为我给的就是char类型
如果我把char类型的方法去掉
public void say(Object arg){
System.out.println("Object");
}
public void say(int arg){
System.out.println("int");
}
public void say(long arg){
System.out.println("long");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
还是一样的main方法,参数还是’a’
char类型的’a’自动类型转换为int,所以输出int
再把int对应的方法注释:
public void say(Object arg){
System.out.println("Object");
}
public void say(long arg){
System.out.println("long");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
继续上边的实验
int继续向上转型为long
对于基本类型:
-
char -> int -> long -> float -> double
-
char不能转为short或byte
继续上边实验
public void say(Object arg){
System.out.println("Object");
}
public void say(Character arg){
System.out.println("Character");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
这次char发生了一次自动装箱,char -> Character
继续注释Character
public void say(Object arg){
System.out.println("Object");
}
public void say(char ... arg){
System.out.println("char ...");
}
public void say(Serializable arg){
System.out.println("Serializable");
}
可以看到char自动装箱后找不到包装类型Character,就去找其包装类型实现的接口类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AXFgaSiX-1574957323112)(C:\Users\12642\AppData\Roaming\Typora\typora-user-images\image-20191128220737974.png)]
最后剩下两个方法
public void say(Object arg){
System.out.println("Object");
}
public void say(char ... arg){
System.out.println("char ...");
}
包装类型找不到,包装类实现的接口类型找不到,就找包装类的父类啦
二,方法重写
方法重载时编译期就根据静态类型确定了要调用的方法版本,但是方法重写时在运行期才确定
public class Test2 {
static class Fruit{
public void test(){
System.out.println("Fruit");
}
}
static class Apple extends Fruit{
@Override
public void test(){
System.out.println("Apple");
}
}
static class Banana extends Fruit{
@Override
public void test(){
System.out.println("Banana");
}
}
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit banana = new Banana();
apple.test();//apple
banana.test();//banana
}
}
对于上面的代码jvm字节码指令是这样的
重点看这一段:
public static void main(java.lang.String[]);
Code:
# new指令在堆上开辟空间来给apple分配内存
0: new #2 // class Test2$Apple
3: dup
# 调用apple的构造方法
4: invokespecial #3 // Method Test2$Apple."<init>":()V
# 将构造出来的对象引用存放在main方法局部变量表上
7: astore_1
8: new #4 // class Test2$Banana
11: dup
12: invokespecial #5 // Method Test2$Banana."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method Test2$Fruit.test:()V
20: aload_2
21: invokevirtual #6 // Method Test2$Fruit.test:()V
24: return
完成对象构造
Fruit apple = new Apple();
# new指令在堆上开辟空间来给apple分配内存
0: new #2 // class Test2$Apple
# 将apple引用复制一份放到操作数栈栈顶
3: dup
# 调用apple的构造方法
4: invokespecial #3 // Method Test2$Apple."<init>":()V
# 将构造出来的对象引用存放在main方法局部变量表上
7: astore_1
看方法的调用
apple.test();//apple
字节码指令:
16: aload_1
17: invokevirtual #6 // Method Test2$Fruit.test:()V
从字节码注释中可以看到执行的是Fruit的test方法,但是实际上是执行Apple的test方法才对,所以这是为什么呢?
且两个不同调用者的invokevirtual参数一样
17: invokevirtual #6 // Method Test2$Fruit.test:()V
20: aload_2
21: invokevirtual #6 // Method Test2$Fruit.test:()V
我们需要分析invokevirtual指令的执行步骤:
-
找到操作数栈栈顶第一个元素,刚刚也知道操作数栈顶是刚刚new出来的apple的引用,该元素记为C,其实就是确定实际类型的过程
-
如果在C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限的校验,如果通过则返回该方法的直接引用,查找过程结束,否则按照继承关系从下往上一次查找和验证
就比如:
17: invokevirtual #6 // Method Test2$Fruit.test:()V
去常量池找索引为6的描述符就是这个 Method Test2$Fruit.test:()V
可以看到他们相同的符号引用,但是却被解析到了不同的直接引用上,这是用为,invokevirtual第一步是在运行期确定方法调用者的实际类型,这也正是方法重写的本质,运行期根据实际类型确定方法执行版本的分派也叫作动态分派
针对i这种动态分派的过程,虚拟机会在方法区建立一个叫做虚方法表的数据结构
虚方法表中存放着各个方法的实际地址,如果某个方法在子类中没有重写,那么子类和父类的方法的实际地址就是一样的,都指向父类方法的实际地址,如果子类重写了父类的某个方法,那么子类虚方法表中该方法的实际地址就是子类实现版本的地址,其实虚方法表可以看成是一个哈希表,因为动态分派的的方法版本选择过程是需要运行时在类的方法元数据中搜索合适的目标方法,现在改用虚方法表可以代替元数据查询,直接找方法表得到方法实际地址,提高了性能