JVM实验三:方法调用

1.方法调用简介

(1)重写与重载

重写:

1.子类对父类方法的重写,方法名相同

2.参数列表必须相同或者子类

3.返回类型必须相同或者子类

4.访问修饰符不能比父类宽泛

5.不能抛出新异常,或者异常类型不能比父类的宽泛

重载:

1.相同的方法名

2.参数列表必须不同

3.返回类型可以不同(不作为评判标准)

区别点

重载方法

重写方法

参数列表

必须不同

必须相同或者子类

返回类型

可以不同

必须相同或者子类

异常

可以不同

不能抛出新的或更广的异常

访问

可以不同

可以降低限制

 

(2)方法调用

invokestatic:用于调用静态方法

invokespecial:用于调用实例构造器的方法、私有方法、父类方法

invokevirtual:用于调用非私有实例方法

invokeinterface:用于调用接口方法

invokedynamic:用于调用动态方法

非虚方法(包括了invokestatic和invokespecial的所有方法类型,以及invokevirtual的final方法):在解析阶段确定唯一调用版本,包括静态方法、父类方法、构造器、私有方法,在类加载阶段将符号引用转换为直接引用,这个转换过程成为解析调用(静态绑定),与之对应的成为分派(动态绑定)

虚方法:非私有非final的实例invokevirtual方法,或者说“可以被覆写的方法”。虚方法体现在重载上的过程叫做静态分派,体现在重写上的过程叫做动态分派

 

2.方法调用中的桥接

Java识别方法只看方法名和参数类型;

jvm识别方法看方法名,参数类型和返回类型(后两者组成方法描述符)

(1)重写方法的返回类型是其父类返回类型的子类型

case1:一个类中两个方法,方法名相同、参数类型相同、返回类型不同,Java编译器认定不合法(非重载),jvm认定合法

case2:子类方法和父类方法,方法名相同、参数类型相同、返回类型不同(继承类),Java编译器认定为重写,jvm认定非重写

对于case2中的问题,在下面的代码中得以体现,由于父子类中方法的返回值不一致,从编译器层面认定这是重写;从jvm层面认为这两个方法无法直接调用,不是重写方法,为了符合重写的语义,虚拟机创建了一个桥接方法,在桥接方法内调用原方法

 1 //父类的方法返回值是Number,子类的方法返回值是Double
 2 public class Father {
 3     Number test(){
 4         return 1;
 5     }
 6 }
 7 public class Son extends Father {
 8     @Override
 9     Double test() {
10         return 1.0;
11     }
12 }
13 
14 //查看Son类的字节码,会发现有两个test方法,一个返回值是Number,另一个方法是桥接方法,返回值是double
15   // access flags 0x0
16   test()Ljava/lang/Double;
17    L0
18     LINENUMBER 7 L0
19     DCONST_1
20     INVOKESTATIC java/lang/Double.valueOf (D)Ljava/lang/Double;
21     ARETURN
22    L1
23     LOCALVARIABLE this Ljvm/method/Son; L0 L1 0
24     MAXSTACK = 2
25     MAXLOCALS = 1
26 
27   // access flags 0x1040
28   synthetic bridge test()Ljava/lang/Number;
29    L0
30     LINENUMBER 4 L0
31     ALOAD 0
32     INVOKEVIRTUAL jvm/method/Son.test ()Ljava/lang/Double;
33     ARETURN
34    L1
35     LOCALVARIABLE this Ljvm/method/Son; L0 L1 0
36     MAXSTACK = 1
37     MAXLOCALS = 1
 1  //这里的桥接方法,反编译为Java语言如下
 2      @Override
 3     Double test() {
 4         return 1.0;
 5     }
 6     //实际调用的桥接方法,返回类型为Double,向上转型为
 7     Number test() {
 8         return this.test();
 9     }
10 //所以实际的返回过程是这样的:return (Double)(Number)new Doublle();注意:jvm实际调用的是桥接方法!!!

(2)重写泛型方法生成桥接

 1   interface Fruit {
 2         void eat();
 3     }
 4 
 5     static class Apple implements Fruit {
 6         @Override
 7         public void eat() {
 8             System.out.println("this is a apple !");
 9         }
10     }
11     static class Orange implements Fruit {
12         @Override
13         public void eat() {
14             System.out.println("this is a orange !");
15         }
16     }
17 
18     static class Shop<T extends Fruit> {
19         public void buy(T fruit) {
20             System.out.println("buy a fuit !");
21         }
22     }
23 
24     static class AppleShop extends Shop<Apple> {
25         @Override
26         public void buy(Apple apple) {
27             System.out.println("buy a apple !");;
28         }
29     }
30 
31 
32     public static void main(String[] args) {
33         Shop shop = new AppleShop();
34         // 调用实际的方法
35         shop.buy(new Apple());
36         // 调用的是桥接方法,出现 java.lang.ClassCastException 的异常
37         shop.buy(new Orange());
38     }

我们知道,泛型在jvm层面是会被擦除的,所以反编译之后的类是这样的

 1 class Shop {
 2     public void buy(Fruit fruit) {
 3         System.out.println("buy a fuit !");
 4     }
 5 }
 6 //子类的代码
 7 class AppleShop extends Shop<Apple> {
 8     @Override
 9     public void buy(Apple apple) {
10         System.out.println("buy a apple !");
11     }
12 //这里生成一个桥接方法,也就是说实际调用的是桥接方法,因此当使用shop.buy(new Orange())时候会出错,这是因为(Apple) new Orange()是不合理的
13     public void buy(Fruit apple) {
14         this.buy((Apple) apple);
15     }
16 }

 

3.分派

分派是指编译期间虚拟机无法确定方法的实际调用目标,因此需要在运行期间进行动态的指定。一般发生在非私有非final的实例方法之中。而在这类“可以被覆写”的方法中,又有两种特殊情况,重载和重写,它们的分派过程分别是静态分派和动态分派,其中静态分派是编译期间完成的,而动态分派是在虚拟机运行期间完成的

(1)静态分派

 1 public class StaticDispatch {
 2     static abstract class Human {}
 3     static class Man extends Human {}
 4     static class Woman extends Human {}
 5     public void sayHello(Human guy) {
 6         System.out.println("hello,guy!");
 7     }
 8     public void sayHello(Man guy) {
 9         System.out.println("hello,gentleman!");
10     }
11     public void sayHello(Woman guy) {
12         System.out.println("hello,lady!");
13     }
14     public static void main(String[] args) {
15         Human man=new Man();
16         sayHello(man);//静态类型Human,实际类型Man,输出结果hello,guy!
17         sayHello((Man)man);//静态类型Man,实际类型Man,输出结果hello,gentlemen!
18         man=new Woman(); 
19         sayHello(man);//静态类型Human,实际类型Woman,输出结果hello,guy!
20         sayHello((Woman)man);//静态类型Woman,实际类型Woman,输出结果hello,lady!
21     }
22 }
23 //运行结果
24 hello,guy!
25 hello,gentlemen!
26 hello,guy!
27 hello,lady!

Human man=new Man()中,Human是静态类型,Man是实际类型,编译器会根据静态类型决定使用哪个重载方法

 

(2)动态分派

 1 public class DynamicDispatch {
 2     static abstract class Human {
 3         protected abstract void sayHello();
 4     }
 5     static class Man extends Human {
 6         @Override
 7         protected void sayHello() {
 8             System.out.println("man say hello");
 9         }
10     }
11     static class Woman extends Human {
12         @Override
13         protected void sayHello() {
14             System.out.println("woman say hello");
15         }
16     }
17     public static void main(String[] args) {
18         Human man = new Man();
19         Human woman = new Woman();
20         man.sayHello();
21         woman.sayHello();
22         man = new Woman();
23         man.sayHello();
24     }
25 }
26 //运行结果
27 man say hello
28 woman say hello
29 woman say hello
30 
31 //通过查看第20行和21行的字节码,发现两个方法调用的字节码完全一致,但是为什么运行结果不一致呢
32    L2
33     LINENUMBER 22 L2
34     ALOAD 1
35     INVOKEVIRTUAL jvm/dispatch/DynamicDispatch$Human.sayHello ()V
36    L3
37     LINENUMBER 23 L3
38     ALOAD 2
39     INVOKEVIRTUAL jvm/dispatch/DynamicDispatch$Human.sayHello ()V

查看字节码发现,两个方法调用的字节码完全一致,但是为什么运行结果不一致呢?

invokevirtual的运行本质是:

1.找到操作栈顶第一个元素指向的实际类型,成为C

2.若在C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验

3.否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程

因此,上述步骤就是重写的本质,也是动态分派的过程

 

 

 

posted @ 2021-01-06 00:02  kozz  阅读(206)  评论(0)    收藏  举报