本文转自https://time.geekbang.org/column/article/11539
在写Java代码的时候,我们可能会遇到可变长参数方法的重载造成的坑。(官方文档建议避免重载可变长参数方法)
实例代码如下
void invoke(Object obj, Object... args){...}
void invoke(String s, Object obj, Object... args) {...}
invoke(null, 1); //调用第二个 invoke 方法
invoke(null, 1, 2); //调用第二个invoke方法
invoke(null, new Object[]{1}); //只有手动绕开可变长参数的语法糖,才能调用第一个invoke 方法
某个API定义了两个同名的重载方法。其中, 第一个接收一个Object, 一级声明为Object…的可变长参数;而第二个则接收一个String、一个Object,以及声明为Object…的变长参数。
当我们想调用第一个方法时, 传入的参数为(null, 1)。也就是说,声明为Object的形式参数所对应的实际参数为null,而变长参数则对应1。
通常来说之所以不提倡可变长参数方法的重载,是因为Java编译器可能无法决定应该调用哪个目标方法。
在这种情况下,编译器会报错,并且提示这个方法调用有二义性。然而,Java编译器直接将我的方法调用识别为调用第二个方法,这是为什么呢?带着这个问题,我们来看一看Java虚拟机是怎么识别目标方法的。
重载与重写
在Java程序里,如果同一类中出现多个名字相同,并且参数类型相同的方法,那么它将无法通过编译。也就是说,在正常情况下,如果我们想要在同一个类中定义名字相同的方法,那么他们的参数类型必须不同。这些方法之间的关系,我们称之为重载。
(Hint:这个限制可以通过字节码工具绕开。也就是说,在编译完成以后,我们可以再向class文件中添加方法名和参数类型相同,而返回类型不同的方法。当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在Java编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢? 当前版本的Java编译器会直接选取第一个方法名以及参数类型匹配的方法。并且它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。)
重载的方法在编译过程中即可完成识别。具体每一个方法调用,Java编译器会根据所传入参数的声明类型(不是实际类型)来选取重载方法。选取的过程分为三个阶段:
- 在不考虑基本类型自动装拆箱(auto-boxing, auto-unboxing),以及可变长参数的情况下选取重载方法;
- 如果第1个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
- 如果第2个阶段中没有找到适配的方法, 那么允许自动装拆箱以及可变长参数的情况下选取重载方法。
如果Java编译器在同一个阶段找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。
在开头的例子中,当传入null时,它既可以匹配第一个参数中声明为Object的形式参数,也可以匹配第二个方法中声明为String 的形式参数。由于String是Object的子类,因此Java编译器会任务第二个方法更为贴切。
除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。也就是说,如果子类定义了于父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载。
那么,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型相同,那么这两个方法之间有时什么关系呢?
如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法。如果这两个方法不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法。
众所周知,Java是一门面对对象的编程语言,它的一个重要特性便是多态。而方法重写,正是多态最重要的一种体现形式:它允许子类在继承父类部分功能的同时,拥有自己独特的行为。它会根据调用者的动态类型,来选取实际的目标方法。
JVM的静态绑定和动态绑定
Java虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor)。前面两个就不需做过多的解释了。至于方法描述符,它是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么Java虚拟机会在类的验证阶段报错。
可以看到,Java虚拟机与Java语言不同,它并不限制名字与参数类型相同,但返回类型不同的方法出现在同一个类中,对于调用这些方法的字节码来说,由于字节码所附带的方法描述符包含了返回类型,因此Java虚拟机能够准确地识别目标方法。
Java虚拟机中关于方法重写的判定同样基于方法的描述符。也就是说,如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,Java虚拟机才会判定为重写。
对于Java语言中重写而Java虚拟机非重写的情况,编译器会通过桥接器方法来实现Java中的重写语义。
由于对重载方法的区分在编译阶段已经完成,我们可以认为Java虚拟机不存在重载这一概念。因此,在某些文章中,重载也被成为静态绑定(staitc binding),或者编译时多态(compile-time polymorphism);而重写则被称为动态绑定(dynamic binding)。
这个说法在Java虚拟机语境下并非完全正确。这是因为某个类中的重载方法可能被它的子类所重写,因此Java编译器会将所有对非私有实例方法的调用编译为需要动态绑定的类型。
确切的说,Java虚拟机中的静态绑定指的是解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别方法的情况。具体来说,Java字节码中的调用相关的指令共有五种:
- invokestatic: 调用静态方法
- invokespecial: 调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
- invokevirtual:调用非私有实例方法。
- invokeinterface:调用接口方法。
- invokedynamic:调用动态方法。
我们先考虑前四种。
先看如下代码:
interface 客户{
boolean isVIP();
}
class 商户{
public double 折后价格(double 原价, 客户 某客户){
return 原价 * 0.8d;
}
}
class 奸商 extends 商户{
@Override
public double 折后价格(double 原价, 客户 某客户){
if(某客户.isVIP()){ //invokeinterface
return 原价 * 价格歧视(); //invokestatic
}else{
return super.折后价格(原价, 某客户); //invokespecial
}
}
public static double 价格歧视(){
// 咱们的杀熟算法太粗暴了, 应该将客户城市作为随机数生成器的种子
return new Random(). //invokespecial
nextDouble() //invokevirtual
+ 0.8d;
}
}
在代码中,“商户”类定义了一个成员方法,叫做“折后价格”,它将接受一个double类型参数,以及一个“客户”类型的参数、这里“客户”是一个接口,它定义了一个接口方法,叫“isVIP”。
我们还定义了另一个叫做“奸商”的类,它继承了“商户”类,并且重写了“折后价格”这个方法。如果客户是VIP,那么它将被给到一个更低的折扣。在这个方法中,我们首先会调用“客户”接口的“isVIP”方法。该调用会被编译为invokeinterface指令。如果客户是VIP,那么我们会调用奸商类的一个名叫“价格歧视”的静态方法。该调用会被编译为invokestatic指令。如果客户不是VIP,那么我们会通过super关键字调用父类的“折后价格”方法。该调用会被编译为invokespecial指令。在静态方法“价格歧视”中,我们会调用Random类的构造器。该调用会被编译为invokespecial指令。然后我们会以这个新建的Random对象为调用者,调用Random类中的nextDouble方法。该调用会被编译为invokevirtual指令。
对于invokestatic以及invokespecial而言,Java虚拟机能够直接识别具体的目标方法。
而对于invokevirtual以及invokeinterface而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。唯一的例外在于,如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为final,那么它可以不通过动态类型,直接确定目标方法。
https://wiki.openjdk.java.net/display/HotSpot/VirtualCalls
https://wiki.openjdk.java.net/display/HotSpot/InterfaceCalls
调用指令的符号引用
在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。
符号引用存储在class文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。我们可以通过“javap -v”来打印某个类的常量池。
javap -v 奸商.class ...
Constant pool:
...
#16 = InterfaceMethodref #27.#29 //客户.isVIP:()Z
...
#22 = Methodref #1.#33 //奸商.价格歧视:()D
...
我们知道,在执行使用了符号引用的字节码前,Java虚拟机需要解析这些符号引用,并替换为实际引用。
对于非接口符号引用,假定该符号引用所指向的类为C,则Java虚拟机会按照如下步骤进行查找。
- 在C中查找符合名字及描述符的方法。
- 如果没有找到,在C的父类中继续搜索,直至Object类。
- 如果没有找到,在C说直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足C与该接口之间没有其它符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
从解析算法可以看出,静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏(注意与重写区分)父类中给的同名、同描述符的静态方法。
对于接口符号引用,假定该符号引用所指向的接口为I,则Java虚拟机会按照如下步骤进行查找。
- 在I中查找符合名字及描述符的方法。
- 如果没有找到,在Object类中的公有实例方法中搜索。
- 如果没有找到,则在I的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤3的要求一致。
经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用这是一个方法表的索引。
本次实践是看一下两个生成桥接方法的例子。可以通过”javap -v”来查看class文件所包含的方法。
interface Customer {
boolean isVIP();
}
class VIP implements Customer {
@Override
public boolean isVIP(){
return true;
}
}
class Merchant {
public Number actionPrice(double price, Customer customer) {
return price * 0.8d;
}
}
class NaiveMerchant extends Merchant {
@Override
public Double actionPrice(double price, Customer customer){
return price * (customer.isVIP() ? 0.7d : 1.0d);
}
}
class TMerchant<T extends Customer> {
public double actionPrice(double price, T customer){
return price * (customer.isVIP() ? 0.8d : 1.0d);
}
}
class VIPOnlyMerchant extends TMerchant<VIP> {
@Override
public double actionPrice(double price, VIP customer) {
return customer.isVIP() ? price : Double.POSITIVE_INFINITY;
}
}
编译并查看方法
javap -v Customer.class
Classfile /home/ubuntu/Documents/java/Customer.class
Last modified Aug 12, 2018; size 126 bytes
MD5 checksum c881051f69d4b4e4a43ce111928741f2
Compiled from "VIPOnlyMerchant.java"
interface Customer
minor version: 0
major version: 52
flags: ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #7 // Customer
#2 = Class #8 // java/lang/Object
#3 = Utf8 isVIP
#4 = Utf8 ()Z
#5 = Utf8 SourceFile
#6 = Utf8 VIPOnlyMerchant.java
#7 = Utf8 Customer
#8 = Utf8 java/lang/Object
{
public abstract boolean isVIP();
descriptor: ()Z
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "VIPOnlyMerchant.java"
javap -v VIP.class
Classfile /home/ubuntu/Documents/java/VIP.class
Last modified Aug 12, 2018; size 262 bytes
MD5 checksum 526e9d768d933b80822a51bc31fc5684
Compiled from "VIPOnlyMerchant.java"
class VIP implements Customer
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #3.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // VIP
#3 = Class #15 // java/lang/Object
#4 = Class #16 // Customer
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 isVIP
#10 = Utf8 ()Z
#11 = Utf8 SourceFile
#12 = Utf8 VIPOnlyMerchant.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 VIP
#15 = Utf8 java/lang/Object
#16 = Utf8 Customer
{
VIP();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public boolean isVIP();
descriptor: ()Z
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: iconst_1
1: ireturn
LineNumberTable:
line 8: 0
}
SourceFile: "VIPOnlyMerchant.java"
javap -v Merchant.class
Classfile /home/ubuntu/Documents/java/Merchant.class
Last modified Aug 12, 2018; size 367 bytes
MD5 checksum 80aecd426d798268b1bc1f154ce2a486
Compiled from "VIPOnlyMerchant.java"
class Merchant
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Double 0.8d
#4 = Methodref #16.#17 // java/lang/Double.valueOf:(D)Ljava/lang/Double;
#5 = Class #18 // Merchant
#6 = Class #19 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 actionPrice
#12 = Utf8 (DLCustomer;)Ljava/lang/Number;
#13 = Utf8 SourceFile
#14 = Utf8 VIPOnlyMerchant.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #20 // java/lang/Double
#17 = NameAndType #21:#22 // valueOf:(D)Ljava/lang/Double;
#18 = Utf8 Merchant
#19 = Utf8 java/lang/Object
#20 = Utf8 java/lang/Double
#21 = Utf8 valueOf
#22 = Utf8 (D)Ljava/lang/Double;
{
Merchant();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0
public java.lang.Number actionPrice(double, Customer);
descriptor: (DLCustomer;)Ljava/lang/Number;
flags: ACC_PUBLIC
Code:
stack=4, locals=4, args_size=3
0: dload_1
1: ldc2_w #2 // double 0.8d
4: dmul
5: invokestatic #4 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
8: areturn
LineNumberTable:
line 14: 0
}
SourceFile: "VIPOnlyMerchant.java"
javap -v NaiveMerchant.class
Classfile /home/ubuntu/Documents/java/NaiveMerchant.class
Last modified Aug 12, 2018; size 552 bytes
MD5 checksum 016c357a2551603bab0a2a995fdb7800
Compiled from "VIPOnlyMerchant.java"
class NaiveMerchant extends Merchant
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #8.#21 // Merchant."<init>":()V
#2 = InterfaceMethodref #22.#23 // Customer.isVIP:()Z
#3 = Double 0.7d
#5 = Methodref #24.#25 // java/lang/Double.valueOf:(D)Ljava/lang/Double;
#6 = Methodref #7.#26 // NaiveMerchant.actionPrice:(DLCustomer;)Ljava/lang/Double;
#7 = Class #27 // NaiveMerchant
#8 = Class #28 // Merchant
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 actionPrice
#14 = Utf8 (DLCustomer;)Ljava/lang/Double;
#15 = Utf8 StackMapTable
#16 = Class #27 // NaiveMerchant
#17 = Class #29 // Customer
#18 = Utf8 (DLCustomer;)Ljava/lang/Number;
#19 = Utf8 SourceFile
#20 = Utf8 VIPOnlyMerchant.java
#21 = NameAndType #9:#10 // "<init>":()V
#22 = Class #29 // Customer
#23 = NameAndType #30:#31 // isVIP:()Z
#24 = Class #32 // java/lang/Double
#25 = NameAndType #33:#34 // valueOf:(D)Ljava/lang/Double;
#26 = NameAndType #13:#14 // actionPrice:(DLCustomer;)Ljava/lang/Double;
#27 = Utf8 NaiveMerchant
#28 = Utf8 Merchant
#29 = Utf8 Customer
#30 = Utf8 isVIP
#31 = Utf8 ()Z
#32 = Utf8 java/lang/Double
#33 = Utf8 valueOf
#34 = Utf8 (D)Ljava/lang/Double;
{
NaiveMerchant();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method Merchant."<init>":()V
4: return
LineNumberTable:
line 18: 0
public java.lang.Double actionPrice(double, Customer);
descriptor: (DLCustomer;)Ljava/lang/Double;
flags: ACC_PUBLIC
Code:
stack=4, locals=4, args_size=3
0: dload_1
1: aload_3
2: invokeinterface #2, 1 // InterfaceMethod Customer.isVIP:()Z
7: ifeq 16
10: ldc2_w #3 // double 0.7d
13: goto 17
16: dconst_1
17: dmul
18: invokestatic #5 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
21: areturn
LineNumberTable:
line 21: 0
StackMapTable: number_of_entries = 2
frame_type = 80 /* same_locals_1_stack_item */
stack = [ double ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class NaiveMerchant, double, class Customer ]
stack = [ double, double ]
public java.lang.Number actionPrice(double, Customer);
descriptor: (DLCustomer;)Ljava/lang/Number;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=4, locals=4, args_size=3
0: aload_0
1: dload_1
2: aload_3
3: invokevirtual #6 // Method actionPrice:(DLCustomer;)Ljava/lang/Double;
6: areturn
LineNumberTable:
line 18: 0
}
SourceFile: "VIPOnlyMerchant.java"
javap -v TMerchant.class
Classfile /home/ubuntu/Documents/java/TMerchant.class
Last modified Aug 12, 2018; size 455 bytes
MD5 checksum 6378828a353085af502c79a796b6a0ab
Compiled from "VIPOnlyMerchant.java"
class TMerchant<T extends Customer> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V
#2 = InterfaceMethodref #22.#23 // Customer.isVIP:()Z
#3 = Double 0.8d
#5 = Class #24 // TMerchant
#6 = Class #25 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 actionPrice
#12 = Utf8 (DLCustomer;)D
#13 = Utf8 StackMapTable
#14 = Class #24 // TMerchant
#15 = Class #26 // Customer
#16 = Utf8 Signature
#17 = Utf8 (DTT;)D
#18 = Utf8 <T::LCustomer;>Ljava/lang/Object;
#19 = Utf8 SourceFile
#20 = Utf8 VIPOnlyMerchant.java
#21 = NameAndType #7:#8 // "<init>":()V
#22 = Class #26 // Customer
#23 = NameAndType #27:#28 // isVIP:()Z
#24 = Utf8 TMerchant
#25 = Utf8 java/lang/Object
#26 = Utf8 Customer
#27 = Utf8 isVIP
#28 = Utf8 ()Z
{
TMerchant();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 25: 0
public double actionPrice(double, T);
descriptor: (DLCustomer;)D
flags: ACC_PUBLIC
Code:
stack=4, locals=4, args_size=3
0: dload_1
1: aload_3
2: invokeinterface #2, 1 // InterfaceMethod Customer.isVIP:()Z
7: ifeq 16
10: ldc2_w #3 // double 0.8d
13: goto 17
16: dconst_1
17: dmul
18: dreturn
LineNumberTable:
line 27: 0
StackMapTable: number_of_entries = 2
frame_type = 80 /* same_locals_1_stack_item */
stack = [ double ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class TMerchant, double, class Customer ]
stack = [ double, double ]
Signature: #17 // (DTT;)D
}
Signature: #18 // <T::LCustomer;>Ljava/lang/Object;
SourceFile: "VIPOnlyMerchant.java"
javap -v VIPOnlyMerchant.class
Classfile /home/ubuntu/Documents/java/VIPOnlyMerchant.class
Last modified Aug 12, 2018; size 483 bytes
MD5 checksum ad389330d43c032997a91459b3b2f896
Compiled from "VIPOnlyMerchant.java"
class VIPOnlyMerchant extends TMerchant<VIP>
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #9.#22 // TMerchant."<init>":()V
#2 = Methodref #6.#23 // VIP.isVIP:()Z
#3 = Class #24 // java/lang/Double
#4 = Double Infinityd
#6 = Class #25 // VIP
#7 = Methodref #8.#26 // VIPOnlyMerchant.actionPrice:(DLVIP;)D
#8 = Class #27 // VIPOnlyMerchant
#9 = Class #28 // TMerchant
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 actionPrice
#15 = Utf8 (DLVIP;)D
#16 = Utf8 StackMapTable
#17 = Utf8 (DLCustomer;)D
#18 = Utf8 Signature
#19 = Utf8 LTMerchant<LVIP;>;
#20 = Utf8 SourceFile
#21 = Utf8 VIPOnlyMerchant.java
#22 = NameAndType #10:#11 // "<init>":()V
#23 = NameAndType #29:#30 // isVIP:()Z
#24 = Utf8 java/lang/Double
#25 = Utf8 VIP
#26 = NameAndType #14:#15 // actionPrice:(DLVIP;)D
#27 = Utf8 VIPOnlyMerchant
#28 = Utf8 TMerchant
#29 = Utf8 isVIP
#30 = Utf8 ()Z
{
VIPOnlyMerchant();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method TMerchant."<init>":()V
4: return
LineNumberTable:
line 31: 0
public double actionPrice(double, VIP);
descriptor: (DLVIP;)D
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: aload_3
1: invokevirtual #2 // Method VIP.isVIP:()Z
4: ifeq 11
7: dload_1
8: goto 14
11: ldc2_w #4 // double Infinityd
14: dreturn
LineNumberTable:
line 34: 0
StackMapTable: number_of_entries = 2
frame_type = 11 /* same */
frame_type = 66 /* same_locals_1_stack_item */
stack = [ double ]
public double actionPrice(double, Customer);
descriptor: (DLCustomer;)D
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=4, locals=4, args_size=3
0: aload_0
1: dload_1
2: aload_3
3: checkcast #6 // class VIP
6: invokevirtual #7 // Method actionPrice:(DLVIP;)D
9: dreturn
LineNumberTable:
line 31: 0
}
Signature: #19 // LTMerchant<LVIP;>;
SourceFile: "VIPOnlyMerchant.java"
综上所述,
Customer 有一个抽象方法isVIP
VIP 有一个公共方法isVIP
Merchat有一个公共方法返回值是Number的actionPrice
NavieMerchant有两个公共方法,一个是返回值是Double的actionPrice,一个是返回值为Number的actionPrice
TMerchant有一个公共方法返回值是double,参数列表是double和T的actionPrice
VIPOnly有两个公共方法,一个是返回值是double,参数列表是double和VIP的actionPrice,一个是返回值是double,参数列表是double,Customer的actionPrice
虽然在Java语言中,代码中的Override注解判定为重写,但是由于返回值类型、参数类型及次序或者方法名任意一种不同,Java虚拟机则不会进行重写,而是进行桥接。