重载与重写
在Java程序里,如果同一个类出现多个名字相同、参数类型相同的方法,那么它无法通过编译。正常情况下,想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同。这些方法之间的关系,称之为重载。
重载的方法在编译过程中即可完成识别。
每一个方法调用,Java编译器会根据所传入参数的声明类型(与实际类型区分)来选取重载方法。选取过程:
- 在不考虑对基本类型自动装拆箱(auto-boxing, auto-unboxing),以及可变长参数的情况下选取重载方法;
- 如果在第1个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
- 如果在第2个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
如果Java编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。
除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载。
如果这两个方法的参数类型相同:如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法;如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法。
方法重写,是多态最重要的一种体现方式:它允许子类在继承父类部分功能的同时,拥有自己独特的行为。重写的调用:它会根据调用者的动态类型,来选取实际的目标方法。
- 方法重载是一个类中(或子类中)定义了多个方法名相同,而参数的数量不同或者数量相同而类型和次序不同。
- 方法重写是在子类存在方法与父类方法的名字相同,而参数的个数与类型一样,返回值也一样的方法。
- 重载是一个类(或子类与父类)的多态性表现,重写是子类与父类的一种多态性表现。
JVM的静态绑定和动态绑定
JVM识别方法的关键在于类名、方法名以及方法描述符(method descriptor)。方法描述符,是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么JVM会在类的验证阶段报错。
JVM与Java语言不同,它并不限制名字与参数类型相同,但返回类型不同的方法出现在同一个类中,对于调用这些方法的字节码来说,由于字节码所附带的方法描述符包含了返回类型,因此JVM能够准确的识别目标方法。
JVM中关于方法重写的判定同样基于方法描述符。如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,JVM才会判定为重写。对于Java语言中重写而JVM中非重写的情况,编译器会通过生成桥接方法来实现Java中的重写语义。
重载 | 重写 | |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
异常 | 可以修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
访问 | 可以修改 | 一定不能做更严格的限制,可以降低限制 |
对重载方法的区分在编译阶段已经完成,可认为JVM不存在重载这一概念。重载也被称为静态绑定(static binding),或者编译时多态(compile-time polymorphism);重写则被称为动态绑定(dynamic binding)。但在JVM中,某个类中的重载方法可能被它的子类所重写,因此Java编译器会将所有对非私有实例方法的调用编译为需要动态绑定的类型。
JVM中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。
静态方法都是静态绑定。HotSpot虚拟机针对final方法做了优化,也会静态绑定。
Java字节码中与调用相关的指令共有五种:
- invokestatic:用于调用静态方法。
- invokespecial:用于调用私有实例方法、构造器;以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法(注意使用super关键字调用的默认方法)。
- invokevirtual:用于调用非私有实例方法(可以也可能被子类重写)。
- invokeinterface:用于调用接口方法(包括普通调用接口的默认方法,不使用super关键字调用)。
- invokedynamic:用于调用动态方法。
对于invokestatic和invokespecial而言,JVM能够直接识别具体的目标方法。
对于invokevirtual和invokeinterface而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。如果虚拟机能够确定目标方法有且仅有一个,那么它可以不通过动态类型,直接确定目标方法(如目标方法被标记为final)。
调用指令的符号引用
在编译过程中,并不知道目标方法的具体内存地址。Java编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。
符号引用存储在class文件的常量池中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。在执行使用了符号引用的字节码前,JVM需要解析这些符号引用,并替换为实际引用。
对于非接口符号引用,假定该符号引用所指向的类为C,则JVM查找步骤:
- 在C中查找符合名字及描述符的方法。
- 如果没有找到,在C的父类中继续搜索,直至Object类。
- 如果没有找到,在C所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需要满足C与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
静态方法也可以通过子类来调用。另外子类的静态方法会隐藏(与重写区分)父类中的同名、同描述符的静态方法(子类调不到父类的同名静态方法了)。
对于接口符号引用,假定该符号引用所指向的接口为I,则JVM查找步骤:
- 在I中查找符合名字及描述符的方法。
- 如果没有找到,在Object类中的公有实例方法中搜索。
- 如果没有找到,则在I的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤3的要求一致。
经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用是一个方法表的索引。
总结
Java中,方法存在重载以及重写的概念,重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同并且参数类型也相同的方法之间的关系。
JVM中,识别方法除了方法名和参数类型之外,还会考虑返回类型。
JVM中,静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。由于Java编译器已经区分了重载的方法,可以认为JVM中不存在重载。
在Class文件中,Java编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。
Java的重写与JVM中的重写并不一致,但是编译器会通过桥接方法来弥补。不一致原因:
- 重写方法的返回类型不一致(Java中重写的返回值类型可以是子类);
- 泛型参数类型造成的方法参数类型不一致。