本文是对《深入理解Java虚拟机 JVM高级特性与最佳实践》一书的学习理解与思考
Java语言多态的体现重写、重载。
类型
静态类型(外观类型)与实际类型
Human是Jack的静态类型或者外观类型,Man是Jack的实际类型
Woman是Rose的静态类型或者外观类型,Woman是Rose的实际类型
Human jack = new Man();
Women rose = new Woman();
重载
对于重载原书中给出一个案例,但是笔者对于其列举的场景进行了一个补充,因为比较好奇这些场景,源码如下
package com.gallant.test.human.overload;
import java.io.Serializable;
/**
* 重载优先级:byte->short->char->int->long->float->double->自动包装(Character)->实现接口(Serializable,如果存在多个接口优先级相同,必须指明具体接口类型,否则会报错模糊匹配)->父类(Object,自下向上)->变长参数
* @author 会灰翔的灰机
* @date 2020/1/4
*/
public class SayHelloChar {
public void sayHello(byte c) {
System.out.println("hello byte:"+c);
}
public void sayHello(short c) {
System.out.println("hello short:"+c);
}
public void sayHello(char c) {
System.out.println("hello char:"+c);
}
public void sayHello(int c) {
System.out.println("hello int:"+c);
}
public void sayHello(long c) {
System.out.println("hello long:"+c);
}
public void sayHello(float c) {
System.out.println("hello float:"+c);
}
public void sayHello(double c) {
System.out.println("hello double:"+c);
}
public void sayHello(Character c) {
System.out.println("hello Character:"+c);
}
public void sayHello(Serializable c) {
System.out.println("hello Serializable:"+c);
}
public void sayHello(Object c) {
System.out.println("hello Object:"+c);
}
public void sayHello(char... c) {
System.out.println("hello char...:"+c);
}
public void sayHello(byte c, char... cc) {
System.out.println("hello byte:"+c+",cc:"+cc);
}
public void sayHello(short c, char... cc) {
System.out.println("hello short:"+c+",cc:"+cc);
}
public void sayHello(char c, char... cc) {
System.out.println("hello char:"+c+",cc:"+cc);
}
public void sayHello(int c, char... cc) {
System.out.println("hello int:"+c+",cc:"+cc);
}
public void sayHello(long c, char... cc) {
System.out.println("hello long:"+c+",cc:"+cc);
}
public void sayHello(float c, char... cc) {
System.out.println("hello float:"+c+",cc:"+cc);
}
public void sayHello(double c, char... cc) {
System.out.println("hello double:"+c+",cc:"+cc);
}
public void sayHello(Character c, char... cc) {
System.out.println("hello Character:"+c+",cc:"+cc);
}
public void sayHello(Serializable c, char... cc) {
System.out.println("hello Serializable:"+c+",cc:"+cc);
}
public void sayHello(Object c, char... cc) {
System.out.println("hello Object:"+c+",cc:"+cc);
}
public static void main(String[] args) {
SayHelloChar sayHelloChar = new SayHelloChar();
sayHelloChar.sayHello('c');
// 取消所有注释会报错匹配到两个方法调用,模糊的方法调用,必须区分出来才可以,例如:将null强转为某个类型
// sayHelloChar.sayHello(null);
}
}
总结
重载优先级如书籍中所说:byte->short->char->int->long->float->double->自动包装(Character)->实现接口(Serializable,如果存在多个接口优先级相同,必须指明具体接口类型,否则会报错模糊匹配)->父类(Object,自下向上)->变长参数。
- 重载是根据参数数量与参数的静态类型作为判断依据,选择最匹配的版本方法
- char类型Java底层有一个隐式的类型转换可以被看作一个数字,所以在注释掉char版本的方法后会匹配到int,为什么不是byte、short?因为char转为byte、short是不安全的。数字类型中的匹配优先级如上文中描述的一致。
- 由于变长参数比较特殊,Java允许匹配到参数数量不匹配的场景,前提是多出的一个参数是变长参数,不能是其他类型。此时变长参数的jvm会为它赋一个初始值,例如上面的案例中,一个入参的调用匹配到两个入参的方法时,变长参数的初始值是:new char[0],而非null
- 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派
- 静态类型是编译期可知的,所以javac在编译期便可以根据参数的静态类型决定使用哪个重载版本。
问题
在案例代码中注释掉所有一个参数的方法后会飘红报错方法引用不明确,也就是变长参数虽然能匹配到一个入参的调用,但是优先级是稍有差异的
- 非包装类型的两个参数的方法优先级与单个参数时一致
- 包装类型的两个参数的方法优先级与单个参数时一致
- 非包装类型的两个参数的方法与包装类型的两个参数的方法冲突,编译期会报错为模糊的引用。例如:sayHello(char, char…)与sayHello(Character, char…),以及下面的报错中的场景,等等
Error:(81, 21) java: 对sayHello的引用不明确
com.gallant.test.human.overload.SayHelloChar 中的方法 sayHello(double,char...) 和 com.gallant.test.human.overload.SayHelloChar 中的方法 sayHello(java.lang.Character,char...) 都匹配
重写
重写是根据参数数量与参数的实际类型作为判断依据。例如下面的案例
输出结果:
hello man
hello women
public class Human {
public void sayHello(){
System.out.println("hello human");
}
public static void main(String[] args) {
Human tom = new Man();
Human jerry = new Woman();
tom.sayHello();
jerry.sayHello();
}
}
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("hello man");
}
}
public class Woman extends Human {
@Override
public void sayHello() {
System.out.println("hello women");
}
}
重写方法执行过程
javap -v查看字节码
Constant pool:
#1 = Methodref #11.#28 // java/lang/Object."<init>":()V
#2 = Fieldref #29.#30 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #31 // hello human
#4 = Methodref #32.#33 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #34 // com/gallant/test/human/override/Man
#6 = Methodref #5.#28 // com/gallant/test/human/override/Man."<init>":()V
#7 = Class #35 // com/gallant/test/human/override/Woman
#8 = Methodref #7.#28 // com/gallant/test/human/override/Woman."<init>":()V
#9 = Methodref #10.#36 // com/gallant/test/human/override/Human.sayHello:()V
...省略...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #5 // class com/gallant/test/human/override/Man
3: dup
4: invokespecial #6 // Method com/gallant/test/human/override/Man."<init>":()V
7: astore_1
8: new #7 // class com/gallant/test/human/override/Woman
11: dup
12: invokespecial #8 // Method com/gallant/test/human/override/Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #9 // Method sayHello:()V
20: aload_2
21: invokevirtual #9 // Method sayHello:()V
24: return
LineNumberTable:
line 14: 0
line 15: 8
line 16: 16
line 17: 20
line 18: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
8 17 1 tom Lcom/gallant/test/human/override/Human;
16 9 2 jerry Lcom/gallant/test/human/override/Human;
4行执行Man构造器初始化方法。7行将Man对象保存到本地变量表插槽1。12、15行分别是执行Woman构造器初始化方法并将Woman保存到本地变量表插槽2。16行将插槽1(即对象Human tom)压入栈顶;17行执行sayHello方法。20、21行分别是将插槽2(即对象Human jerry)压入栈顶并执行方法sayHello。两处sayHello方法执行均指向常量池中Human.sayHello的符号引用。但是最终实际执行的目标方法并不相同。
invokevirtual指令的运行时解析过程大致如下:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量池中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和校验过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接受者(方法)的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。
运行期根据实际类型确定方法执行版本的分派过程称为动态分派