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步的搜索和验证过程
因此,上述步骤就是重写的本质,也是动态分派的过程

浙公网安备 33010602011771号