2017-10-18 17:28:01 +0000   |     java inherit polymorphism   |   Viewed times   |    

外观类型(Apparent Type)和实际类型(Actual Type)

class Father {
    public void show() { System.out.println("I am Father!"); }
}
class Son {
    public void show() { System.out.println("I am Son!"); }
}
Father f = new Son();
f.show(); // I am Son!

从后续代码的角度看f代表一个Father类型实例的引用,我们称Father是变量的外观类型(Apparent Type)。

但当我们调用f.show(),实际执行的是Son类型的show()方法。因为变量f的实际类型(Actual Type)是Son

变量的最终外观类型在编译器是可知的,而实际类型需要到运行期才能确定。

动态绑定关键步骤1: 抓住对象的“实际类型”

看下面这个例子,C类的f1()方法中调用super.f2(),因为C类继承自B类,因此super指向一个B类对象,调用的就是B类的f2()函数,输出I am f2() from B!

private static class A {
    public void f1() {
        f2();
    }
    public void f2() {
        System.out.println("I am f2() from A!");
    }
}
private static class B extends A {
    public void f2() {
        System.out.println("I am f2() from B!");
    }
}
private static class C extends B {
    public void f1() {
        super.f2();
    }
    public void f2() {
        System.out.println("I am f2() from C!");
    }
}
A a = new A();
a.f1(); // I am f2() from A!
B b = new B();
b.f1(); // I am f2() from B!
C c = new C();
c.f1(); // I am f2() from B!

动态绑定关键步骤2:如果实际类型没有目标方法的实现,就到父类中去找

为了更快地找到实际类型,虚拟机在方法区中简历了一个虚方法表(Virtual Method Table)。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的。 method-table

如上图所示,Father类和Son类继承自java.lang.Object类的方法的入口地址,和Object是一致的。而Father类新增的两个hardChoise()重载方法有他们自己的入口地址。而当这两个方法在Son类中被重写了之后,Son类中的这两个方法又有了两个新的入口地址。如果没有重写,将保持Father类中hardChoise()方法的入口地址。

看下面这个例子,变量s的实际类型是Son,但因为Son类没有重写Father类的method()方法,因此s.method()绑定的依然是Father#method()函数的入口地址。通过这个入口再调用show()函数,调用的自然是Father#show()函数。所以两个测试输出的都是Father

class Father{
    private void show(){
        System.out.println("Father");
    }
    public void method(){
        show();
    }
}
class Son extends Father{
    public void show(){
        System.out.println("Son");
    }
}
class Test {
    public static void main(String[] args) {
        Father f=new Father();
        f.method(); // Father
        Son s=new Son();
        s.method(); // Father
    }
}

关于Java继承的两点总结

  1. 子类同名成员字段不覆盖父类成员字段。
  2. 子类重写父类成员方法将更新方法区虚方法表中方法的入口地址。

本质上讲,Java子类继承父类,是对父类的一种增强。构造子类之前,都需要先构造父类实例。子类修改部分父类成员,只是又增加了一份新的同名成员(不管是字段还是方法),并且新成员占据了默认入口地址。父类成员依旧保留,随时可以通过super引用调用。

参考资料