分派(dispatch)
分派根据宗量数的多少分为单分派和多分派,分派调用有可能是静态的,有可能是动态的,因此两两组合可以分为:静态单分派,动态单分派,静态多分派,动态多分派
重载
定义
重载就是在一个类中,存在多个方法名称相同,参数列表不同的方法
参数列表不同表现为:参数个数,参数类型,参数顺序的不同
符号引号的角度
在java语言中,要重载一个方法,除了要求与原方法具有相同的建单名称之外,还必须要求又有一个与原方法不同的特征签名。
特征签名:
就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是返回值不会包含在特征签名中,因此在java中无法依靠返回值重载一个方法,但是在class文件格式中,特征签名的范围更大,只要描述符不完全一致的两个方法是可以共存的,也就是说只有返回值不同的两个方法是可以共存在一个class文件中。
描述符:
用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值
分派角度
package com.dispatch;
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public void sayHello(Human human){
System.out.println("hello ,human");
}
public void sayHello(Man man){
System.out.println("hello ,man");
}
public void sayHello(Woman woman){
System.out.println("hello ,woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
运行的结果:
hello ,human
hello ,human
为什么选择执行的方法版参数是Human类型?
首先 在Human man = new Man() 中,Human为变量的静态类型或者外观类型,后面的Man 为变量的实际类型。静态类型和实际类型在程序中都可以发送一些变化,区别是静态类型的变化仅仅是在使用阶段发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译阶段可知的,而实际类型的变化结果是在运行阶段才可以确定的,编译器在编译一个变量的时并不知道变量的实际类型是什么。
在上面demo中,在已经确定了方法的调用者staticDispatch ,要重载哪个方法取决于参数的个数和类型,man ,woman 是两个静态类型都为 Human,动态类型不同的两个变量,但是虚拟机(准确是编译器)在重载时是通过变量的静态类型作为依据,而不是实际类型。并且变量的静态类型在编译期是可知的,因为javac编译器会根据变量的静态类型决定重载的哪个版本,所以上述demo选择的是Human版本
所有依赖静态类型定位执行方法版本的分派称为静态分派,静态分派的典型应用就是重载。
静态分派发生在编译阶段,因此静态分派并不是又虚拟机执行的,而是又编译器决定重载哪个版本。而静态分派不是唯一确定的 ,而是找一个更适合的版本去执行,例如:
package com.dispatch;
import java.io.Serializable;
public class StaticDispatchDemo2 {
public static void sayHello(Object arg) {
System.out.println("Hello Object");
}
public static void sayHello(int arg) {
System.out.println("Hello int");
}
public static void sayHello(long arg) {
System.out.println("Hello long");
}
public static void sayHello(Character arg) {
System.out.println("Hello character");
}
public static void sayHello(char arg) {
System.out.println("Hello char");
}
public static void sayHello(char... arg) {
System.out.println("Hello char...");
}
public static void sayHello(Serializable arg) {
System.out.println("Hello serializable");
}
public static void main(String[] args) {
sayHello('a');
运行结果:
Hello char
若是注释掉sayHello(char arg) 方法,运行结果:
Hello int
因为 'a' 不仅是一个字符,还是一个数字97,因此选择重载的版本是sayHello(int arg)
本质
编译器在编译阶段根据变量的静态类型来选择更适合的重载版本
重写
定义
重写是子类对父类允许访问的方重写编写,返回值和参数列表都不能改变。即外壳不变,核心重写,规则:一大两小两同
方法名,参数列表必须完全与被重写方法的相同-------两同
返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。-----两小之一
重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。-----两小之一
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。------一大
父类的成员方法只能被它的子类重写。
声明为 final 的方法不能被重写。
声明为 static 的方法不能被重写,但是能够被再次声明。
子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法
构造方法不能被重写。
如果不能继承一个方法,则不能重写这个方法。
分派角度
package com.dispatch;
public class StaticDispatch {
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public void sayHello(Human human){
System.out.println("hello ,human");
}
public void sayHello(Man man){
System.out.println("hello ,man");
}
public void sayHello(Woman woman){
System.out.println("hello ,woman");
}
public static void main(String[] args) {
Human man = new Man();
man.sayHello();
Human woman = new Woman();
woman.sayHello();
man = new Woman();
man.sayHello();
运行结果:
man say hello
woman say hello
woman say hello
显然,重写不是根据变量的静态类型确定,通过javap 命令查看字节码如下:
可以看到最终都要中执行虚方法:
Method cn/hewie/dispatch/dynamicdispatch/DynamicDispatch$Human.sayHello:()V:invokevirtual #6
astore_1,astore_2 是把刚刚创建的man ,woman两个对象压入栈顶,这两个对象是将要执行sayhello方法的所有者,称为接收者。17和21行是方法调用,指令为invokevirtual,参数是是常量池中的低6项,也就是Human.sayHello(),但是这两个对象的这些都是完全一致的,但是指令最终的执行效果完全不同,其中的原因是invokevirtual 指令运行过程造成的
invokevirtual 指令的运行过程:
1.找到操作数栈顶的第一个元素所指向的实际类型,记作C;
2如果在类型C中找到与常量中的描述符和建单名称相同都相符的方法,则进行权限访问校验,通过则返回这个方法引用,查找结束,若是不通过报错java.lang.IllegalAccessError;
3.否则按照继承关系从下到上对C的各个父类按照第二部搜索验证
本质
因此上述demo在运行期确定变量的实际类型,而invokevirtual 指令把常量池中的类方法引用解析到了不同的直接引用上。我们把运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
虚拟机动态分派的实现
由于动态分派的方法版本选择过程需要在运行时在类的方法元数据中搜索合适的目标方法,因此基于性能考虑都不会如此频繁的搜索动态最常用的最稳定方法是在方法区中建一个虚方法表vtable,与之对应还有一个接口方发表itable,使用虚方法表来代替元数据查找
例如:
package com.dispatch;
import java.io.Serializable;
public class StaticDispatchDemo3 {
static class QQ{}
static class _360{}
public static class Father{
public void hardChoice(QQ args){
System.out.println("father choose QQ");
}
public void hardChoice(_360 args){
System.out.println("father choose _360");
}
}
public static class Son extends Father{
public void hardChoice(QQ args){
System.out.println("son choose QQ");
}
public void hardChoice(_360 args){
System.out.println("son choose _360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
虚方发表:
由此可见,虚方法表中存放着各个方法的实际入口地址。如果某个方法没有被之类重写,那么虚方法表中地址入口与和父类相同方法的地址入口是一致的都指向父类实现的入口;若是之类重写这个方法,之类方发表的地址将会替换为之类实现的版本的入口地址。