Java 多态与动态绑定

class Animal {
    public void cry() {
        System.out.println("xxx");
    }
}

class Cat extends Animal {
    public void cry() {
        System.out.println("yyy");
    }

    public void bite() {
        System.out.println("zzz");
    }
}

public class PolymorphicTest {
    public static void main(String ...args) {
        Animal cat = new Cat();
        cat.cry();                  // yyy
        ((Cat)cat).bite();          // zzz
    }
}

多态存在的三个必要条件:继承、重写、父类变量引用子类对象

数据转型

  • 向上转型(up casting):就是将子类的对象转型成父类的引用(就是父类引用指向了子类的对象,此时此引用只能访问父类的成员)。
  • 向下转型(down casting):将向上转型的引用再转回来,就叫向下转型(此时引用能访问子类的成员变量)

多态中成员的特点

  • 在多态中成员函数的特点:动态绑定
    在编译时期:参阅引用型变量所属的类中是否有调用的方法。如果有,编译通过,如果没有编译失败
    在运行时期:参阅对象所属的类中是否有调用的方法
    简单总结就是:成员函数在多态调用时,编译看左边,运行看右边

  • 在多态中,成员变量的特点:静态绑定
    无论编译和运行,都参考左边(引用型变量所属的类)

  • 在多态中,静态成员函数的特点:静态绑定
    无论编译和运行,都参考左边
    因为静态只看类,没有重写,只有隐藏

多态好处

不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态性的实现主要通过动态绑定

程序绑定

程序绑定是一个成员的调用与成员所在的类(成员主体)关联起来。对 Java 来说,绑定分为静态绑定和动态绑定

静态绑定(前期绑定)

在编译期已经可以确定的信息,就在程序执行前被绑定,此时由编译器或其它连接程序实现。针对 Java 简单的可以理解为程序编译期的绑定,绑定的是类信息

  • Java 中的方法只有 final,static,private,重载方法和构造方法是静态绑定
  • 所有的变量都是静态绑定

动态绑定(后期绑定)

编译器在编译阶段不知道要调用哪个方法,在运行时根据具体对象的类型进行绑定,绑定的是对象信息

若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息

重写方法使用的是动态绑定

方法调用

以 obj.func(param) 为例,隐式参数 obj 声明为 Cat 类的对象

  1. 编译器查看对象的声明类型和方法名
    需要注意的是,有可能存在多个名字为 func 但参数签名不一样的方法。例如,可能存在方法 func(int) 和 func(String)。编译器将会一一列举所有 Cat 类中名为 func 的方法和其父类 Animal 中访问属性为 public 且名为 func 的方法(超类中的私有方法不可访问)
    这样,编译器就获得了所有可能被调用的候选方法列表
  1. 接下来,编译器将检查调用方法时提供的参数签名
    如果在所有名为 func 的方法中存在一个与提供的参数签名完全匹配的方法,那么就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,如果调用 func("hello"),编译器会选择 func(String),而不是 func(int)。由于自动类型转换的存在,例如 int 可以转换为 double,Cat 可以转换成 Animal 等,如果没有找到与调用方法参数签名匹配的方法,就进行类型转换后再继续查找。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,那么编译错误
    这样,编译器就获得了需要调用的方法名字和参数类型

  2. 如果是 private、static、final,或者是构造方法,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,这就需要在运行时通过动态绑定确定调用方法,比如上面的 cat.cry()

  3. 当程序运行,并且釆用动态绑定调用方法时,JVM 一定会调用与 obj 所引用对象的实际类型最合适的那个类的方法。我们已经假设 obj 的实际类型是 Cat,它是 Animal 的子类,如果 Cat 中定义了 func(String),就调用它,否则将在 Animal 类及其父类中寻找

方法表

每次调用方法都要进行搜索,时间开销相当大,因此,JVM 预先为每个类创建了一个方法表,其中列出了所有方法的名称、参数签名和所属的类。这样一来,在真正调用方法的时候,JVM 仅查找这个表就行了。在上面的例子中,JVM 搜索 Cat 类的方法表,以便寻找与调用 func("hello") 相匹配的方法。这个方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果调用 super.func("hello"),编译器将对父类的方法表进行搜索

假设 Animal 类包含 cry()、getName()、getAge() 三个方法,那么它的方法表如下:

cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()

实际上,Animal 有默认的父类 Object,会继承 Object 的方法,所以上面列举的方法并不完整

假设 Cat 类覆盖了 Animal 类中的 cry() 方法,并且新增了一个方法 climbTree(),那么它的参数列表为:

cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()

在运行的时候,调用 obj.cry() 方法的过程如下:

  • JVM 首先提取 obj 的实际类型的方法表,可能是 Animal 类的方法表,也可能是 Cat 类及其子类的方法表
  • JVM 在方法表中搜索与 cry() 签名匹配的方法,找到后,就知道它属于哪个类了
  • JVM 调用该方法

总结

  • 先通过对象类型与方法名在方法表中找
  • 通过参数签名进行重载解析
  • 确认属于静态绑定还是动态绑定,最后调用

你可能感兴趣的:(Java 多态与动态绑定)