Java —— 多态与分派

  • 多态
  • 分派
    • 静态分派重载
      • 静态分派 之 字面量
    • 动态分派重写
    • 单分派与多分派
      • 虚方法表
  • 总结

多态

Java具备面向对象的3个基本特征 : 继承,封装(get / set)和多态。

多态 : 继承,重写(Override),向上转型(Human h=new Man())三大必要条件。



  • 方法重载 : 同一个方法名,参数或者类型不同。(Overload)
void method(String str);
void method(int value);
void method(int value,char c);



  • 方法重写 : 父类与子类有同样的方法名和参数,这叫方法覆盖。(Override)
class Human {
    void method() {
        System.out.println("do method");
    }
}

class Man extends Human {

    @Override
    void method() {
        System.out.println("do method");
    }
}



根据我们上面对多态的定义 : 继承,重写(Override),向上转型(Human h=new Man())三大必要条件。

Human h=new Man();//Man向上转型
h.method();

Human 引用指向Man对象(向上转型),并且调用method()方法时会调用Man对象去实现。这就是多态。



分派

分派调用过程将会揭示多态性特征的一些基本体现,如“重载”和“重写”。



静态分派(重载)

静态分派的典型应用就是方法重载。



下面先看一段代码,读者可以先想一下程序输出的结果。

public class OverloadTest {

    static abstract class Human {}

    static class Man extends Human {}

    static class Woman extends Human {}

    void sayHello(Human h) {
        System.out.println("hello humam");
    }

    void sayHello(Man h) {
        System.out.println("hello Man");
    }

    void sayHello(Woman h) {
        System.out.println("hello Woman");
    }

    public static void main(String[] args) {

        Human man = new Man();
        Human woman = new Woman();

        OverloadTest test = new OverloadTest();
        test.sayHello(man);
        test.sayHello(woman);
    }

}





运行结果 :

//hello humam
//hello humam

Human h = new Man();
//Human  :  静态类型
//Man  :  实际类型
  • 静态类型(Static Type) : 我们把上面代码中的Human 称为变量的静态类型,或者叫做外观类型。
    静态类型是在编译期可知的,仅仅在使用时发生,本身的静态类型不会被改变。

  • 实际类型(Actual Type) : 后面的Man则称为变量的实际类型。
    实际类型变化的结果是在运行期才可确定。


编译器在编译程序的时候并不知道一个对象的实际类型是什么。

//实际类型变化
Human man=new Man();//Human为静态类型
man=new Woman();//man为实际类型

//静态类型变化(括号里的类型)
test.sayHello((Man)man));
test.sayHello((Woman)man);

回过头再看看上面我们对方法重载的定义 :

  • 方法重载 : 同一个方法名,参数或者类型不同。(Overload)

虚拟机在重载时是通过参数的静态类型作为判定依据的。

并且静态类型是编译期可知的,因为,在编译阶段,Javac编译期会根据参数的静态类型选择决定使用哪个重载版本,和调用的对象(上述为test对象)无关。

所以说静态分派的典型应用就是方法重载



静态分派 之 字面量

所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。

静态分派发生在编译期,所以分派的动作是由编译期来执行的。

但是字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断。例如下面的代码展示 :

    public static void sayHello(char arg) {System.out.println("1 hello char value==" + arg);}

    public static void sayHello(int arg) {System.out.println("2 hello int value==" + arg);}

    public static void sayHello(long arg) {System.out.println("3 hello Long value==" + arg);}

    public static void sayHello(Character arg) {System.out.println("4 hello Character value==" + arg);}

    public static void sayHello(Serializable arg) {System.out.println("5 hello Serializable value==" + arg);}

    public static void sayHello(Object obj) {System.out.println("6 hello Object value==" + obj);}

    public static void sayHello(char... arg) {System.out.println("7 hello char[] value==" + arg);}

    public static void main(String[] args) {

        sayHello('a');
    }

上面的代码运行后会输出 :

//1 hello char value==a

因为'a'是一个char类型的数据,首先就会寻找参数类型为char的重载方法。


如果注释掉sayHello(char arg)方法,那么输出就会变为 :

//2 hello int value==97

这时发生了一次自动类型转换,'a'除了可以代表一个字符串,还可以代表ASCII码中的数字97。



以上的代码会按照我标记的顺序作为选择静态分派目标的一个过程。(后面顺序过程就不分析了)

并且这个过程是在编译期间执行完成的。

总结来说可以这么理解 : 静态分派和方法(括号内)参数的类型有关



动态分派(重写)

动态分派和方法重写(Override)有着很密切的关系。

下面先看一段代码,读者可以先想一下程序输出的结果。

public class OverrideTest {

    static abstract class Human {
        abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        void sayHello() {
            System.out.println("hello man");
        }
    }

    static class Woman extends Human {
        @Override
        void sayHello() {
            System.out.println("hello Woman");
        }
    }

    public static void main(String[] args) {

        Human man = new Man();
        Human woman = new Woman();

        man.sayHello();//实际类型man的调用
        woman.sayHello();//实际类型woman的调用

        man = new Woman();
        man.sayHello();
    }

}





运行结果 :

//hello man
//hello Woman

//hello Woman

我们这里主要分析的是实际类型man或者woman调用sayHello()方法时执行的不同行为和结果。(有兴趣的可以使用javap 命令查看Class文件方法的字节码)


  • 动态分派
    invokevirtual指令会把常量池中的类方法符号引用解析到不同对象的直接引用上,这个过程就是Java语言中的方法重写本质。也就是运行期根据实际类型确定方法执行版本的分派过程。

总结来说可以这么理解 : 动态分派和方法的调用者有关(这里的调用者是向上转型的对象)。



单分派与多分派

方法的接收者(所有者)与方法的参数统称为方法的宗量

test.sayHello(man)
//test是方法的接收者
//man是方法的参数

分派基于宗量的数量,可以分为单分派和多分派。

  • 单分派 : 根据一个宗量对目标方法进行选择。

  • 多分派 : 根据多于一个宗量对目标方法进行选择。

老样子,下面用一段代码来分析单分派和多分派 :

class News {}

class Anime {}

class Father {
    public void hobby(News arg) {
        System.out.println("father 看社会新闻");
    }

    public void hobby(Anime arg) {
        System.out.println("father 兴趣是看暴漫");
    }
}

class Son extends Father {
    public void hobby(News arg) {
        System.out.println("son 看娱乐新闻");
    }

    public void hobby(Anime arg) {
        System.out.println("son 兴趣是看七龙珠");
    }
}

public class Test {
    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();

        father.hobby(new News());//father是实际类型,News是静态类型分派过程
        son.hobby(new Anime());//son是实际类型,Anime是静态类型分派过程
    }
}




运行结果 :

//father 看社会新闻
//son 兴趣是看七龙珠
  • 静态分派 : 选择目标方法的依据有两点:一是方法的接受者(即调用者)的静态类型是Father还是Son,二是方法参数类型是News还是Anime。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型

  • 动态分派 : 由于编译期已经了确定了目标方法的参数类型(编译期根据参数的静态类型进行静态分派),因此唯一可以影响到虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Son。

father.hobby(new News());

例如上面这段代码,编译时期已经知道方法的参数类型是News,我们可以不用管News是否是向上转型的,我们只管调用hobby()方法的实际类型,所以唯一可以影响到虚拟机选择的因素只有hobby()方法的调用者是实际类型的Father还是Son,因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。



再加上上面我对静态/动态分派的总结,看起来应该会更加的清晰 :

静态分派和方法(括号内)参数的类型有关(只关注有几个参数类型)

动态分派和方法的调用者有关(只关注有几个调用者)



虚方法表

由于动态分派是非常频繁的动作,所以jvm在类的方法区中建立了一个虚方法表(Virtual Method Table,也称为vtable)。使用虚方法表索引来代替元数据查找以提高性能。

Java —— 多态与分派_第1张图片

虚方法表里存放着各个方法的实际入口地址。

如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一直的,都指向父类的实现入口。(如Object的方法)

如果子类重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。(如hobby()方法)


总结

静态类型语言在编译期确定类型,最显著的好处是编译器可以提供严谨的类型检查,这样与类型相关的问题就能在编码的时候及时发现,利于稳定性以及代码达到更大的规模。

动态语言在运行期确定类型,这可以为开发人员提供更大的灵活性,在某些静态类型语言中需要用大量“臃肿”的代码来实现的功能,由动态语言来实现可能会更加清晰和简洁,开发效率提升。

因此两种类型相辅相成。

你可能感兴趣的:(《深入理解Java虚拟机》)