跳至主要內容

多态

荒流2020年8月5日大约 4 分钟约 1168 字

在C++中,如果希望实现动态绑定,需要将成员函数声明为virtual。在Java中,动态绑定是默认的行为。如果不希望让一个方法是虚拟的,可以将它声明为final

1. 什么是多态

多态的概念

相同类型的对象,调用同一个方法时,可能展示不同的执行结果,这就是多态

比如,如下代码中,当运行时调用该ploymophicBc对象的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征:

// 编译时类型是BaseClass,运行时类型是SubClass
// 此时发生多态
BaseClass ploymophicBc = new SubClass();

注意,与方法不同,对象的实例变量不具备多态性,通过对象来访问其包含的实例字段时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。

Java的多态

绑定的概念——将一个方法调用与一个方法的方法体连接到一起,可分为编译时绑定与运行时绑定。

Java引用变量有编译时类型、运行时类型之分。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致且无报错,就是所谓的 多态(Polymorphism)

因为子类其实是一种特殊的父类,Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,这被称为向上转型(upcasting),由系统自动完成。

public class Main {
    public static void main(String[] args) {
        Father ins1 = new Father(), ins2 = new Child();

        ins1.f(); // 打印出:Father.
        ins2.f(); // 打印出:Child. 因为运行时类型为 Child

        ins1.g(); // 编译无法通过,因为完全是 Father 类型
        ins2.g(); // 编译无法通过,因为编译时类型为 Father

        ((Child) ins1).g(); // 编译通过,但运行时错误
        ((Child) ins2).g(); // 打印出:New Money. 编译运行通过,因为运行时类型为 Child
    }
}

class Father {
    public void f() {
        System.out.println("Father.");
    }
}

class Child extends Father {
    public void f() {
        System.out.println("Child.");
    }
    public void g() {
        System.out.println("New money.");
    }
}

2. 强制类型转换

编译Java程序时,引用变量不能调用它的运行时类型的方法,若确实需要这样做,则必须进行强制类型转换,使它变成运行时类型,即将一个引用类型变量转换成其子类类型。

语法:(type)variable(将variable变量转换成一个type类型的变量)。

注意:

考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前可以先通过instanceof运算符来判断是否可以成功转换。

3. instanceof运算符

instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数是一个类/接口,用于判断前面的对象是否是后面的类,或者其子类、实现类的实例:

Object hello = "Hello";

System.out.println(hello instanceof Object); //true
System.out.println(hello instanceof String); //true
System.out.println(hello instanceof Math); //false

如果xnullx instanceof C将返回false,因为null不属于任何类型。

4. 构造方法的“多态性”

构造方法不具有多态性。但如果在构造方法里要去调用别的方法,那么这个构造方法和多态性就有了联系。

package com.chuan;

// 基类
abstract class Glypy {
    Glypy() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }
    abstract void draw();
}

// 派生类
class RoundGlyph extends Glypy {
    int radius = 1;
    RoundGlyph(int radius) {
        this.radius = radius;
        System.out.println("RoundGlyph.RoundGlyph(), raidus = " + radius);
    }
    @Override
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}

// 测试类
public class RoundGlyphTest {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}

// 输出结果
/*
    Glyph() before draw()
    RoundGlyph.draw(), radius = 0      
    Glyph() after draw()
    RoundGlyph.RoundGlyph(), raidus = 5
*/

// 结果解析
// https://www.bilibili.com/video/BV1U7411A7Cy?p=22&share_source=copy_web

因而,在定义构造方法时,要注意:

  1. 用尽可可能少的动作把对象的状态设置好;

  2. 如果可以,不要调用任何非构造方法;

  3. 在构造方法内唯一能够安全调用的是在基类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性),这些方法不能被覆盖,所以不会出现上述潜在的问题。