多态

多态,也称为动态绑定、后期绑定或运行时绑定。

封装通过合并特征和行为来创建新的数据类型。实现隐藏则通过将细节私有化,把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。继承允许将对象视为它自己本身的类型或基类型来加以处理,这允许 将多种类型(从同一基类导出的)视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。

多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们是从同一基类导出的。

再论向上转型

对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。把对某个对象的引用视为对其基类型的引用的做法称为向上转型。

//: UpcastingTest.java

public class UpcastingTest {
    public static void main(String args[]) {
        B b = new B();
        b.f();
        A a = b;    // 向上转型不需要强制转换
        a.f();    // 并不会调用基类函数
    }
} /**Output
B.f()
B.f()
*/

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

class B extends A {
    void f() {
        System.out.println("B.f()");
    }
}

转机

方法调用绑定

将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定,叫做前期绑定。

后期绑定,含义是运行时根据对象的类型进行绑定,也叫作动态绑定或运行时绑定。

Java中除了static方法和final方法,其他所有方法都是后期绑定。

产生正确的行为

知道Java中所有方法都是通过动态绑定实现多态之后,就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去判定应该做什么。

可扩展性

只与基类接口通信,这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型。我们所做的代码修改,不会对程序中其他不应受到影响的部分产生破坏。

多态是一项让程序员将改变的事物与未变的事物分离开来的重要技术。

缺陷:覆盖私有方法

只有非private方法才可以被覆盖;但是还需要密切注意覆盖private方法的现象,避免不按照我们所期望的执行。

缺陷:域与静态方法

只有普通的方法调用可以是多态的。如果某个方法是静态的,它的行为就不具有多态性。

当导出类型转型为基类型时,任何域访问操作都将由编译器解析,因此不是多态的。

构造器和多态

构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的)。

构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。

  • 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制零

  • 调用基类构造器。这个步骤会不断反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,至到最底层的导出类。

  • 按声明顺序调用成员的初始化方法。

  • 调用导出类构造器的主体。

继承于清理

尽管一般不需要做清理操作,但一旦遇到清理问题,必须用心为新类创建dispose方法。当覆盖被继承的dispose方法时,务必记住调用基类版本dispose方法;否则基类的清理动作不会发生。

构造器内部的多态方法行为

一个动态绑定的方法调用会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果在一个构造器的内部调用正在构建的对象的某个动态绑定方法,而这个方法所操纵的成员可能还未进行初始化,这会出现问题。

因此,编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。在构造器中唯一能够安全调用的那些方法是基类中的final方法,因为这些方法不能被覆盖。

协变返回类型

Java SE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:

//: CovarianReturn.java

class Grain {
    public String toString() { return "Grain"; }
}

class Wheat extends Grain {
    public String toString() { return "Wheat"; }
}

class Mill {
    Grain process() {
        return new Grain();
    }
}

class WheatMill extends Mill {
    Wheat process() {
        return new Wheat();
    }
}

public class CovarianReturn {
    public static void main(String args[]) {
        Mill m = new Mill();
        Grain g = m.process();
        System.out.println(g);
        m = new WheatMill();
        g = m.process();
        System.out.println(g);
    }
} /**Output
Grain
Wheat
*/

Java SE5与Java较早之前的主要差异是,较早的版本将强制process()的覆盖版本必须返回Grain,而不能返回Wheat,尽管Wheat是从Grain导出的。

用继承进行设计

相对继承,更好的方式是首先选择“组合”。组合不会强制我们的程序设计进入继承的层次结构中。而且,组合更加灵活,因为它可以动态选择类型;相反,继承在编译时就需要知道确切类型。

用继承表达行为间的差异,并用字段表达状态上的变化。

通过继承得到了两个不同的类。

纯继承与扩展

“是一个”和“像是一个”:

  • 是一个(is-a):只覆盖基类的方法,而不添加新方法

  • 像是一个(is-like-a):在导出类型中添加新的接口元素

向下转型于运行时类型识别

向上转型会丢失具体的类型信息。

在Java中,所有转型都会得到检查。在进入运行期时仍然会对其进行检查。

最后更新于

这有帮助吗?