点击进入我的博客

在面向对象的程序设计语言中,多态是继数据抽象(封装)和继承之后的第三种基本特征。
多态通过分离做什么怎么做,从另一角度将接口和实现分离开来。
多态的作用是消除类型之间的耦合关系。

8.1 再论向上转型

对象既可以作为它自己的本类使用,也可以作为它的基类使用。

8.1.1 忘记对象类型

我们只写一个简单的方法,它接受基类作为参数,而不是那些特殊的导出类。

public class Test {
    public static void main(String[] args) {
        func(new Unicycle());
        func(new Bicycle());
        func(new Tricycle());
    }

    public static void func(Cycle cycle) {
        cycle.ride();
    }
}

class Cycle {
    void ride() {}
}

class Unicycle extends Cycle {
    void ride() {
        System.out.println("Unicycle");
    }
}

class Bicycle extends Cycle {
    void ride() {
        System.out.println("Bicycle");
    }
}

class Tricycle extends Cycle {
    void ride() {
        System.out.println("Tricycle");
    }
}

8.2 转机

func(Cycle cycle)接受一个Cycle引用,那么编译器怎么才能知道这个Cycle引用指的是哪个具体对象呢?实际上,编译器并不知道。

8.2.1 方法调用绑定

  • 绑定:讲一个方法调用同一个方法主体关联起来被称作绑定。
  • 前期绑定:程序执行前进行绑定(由编译器和连接程序实现)叫做前期绑定。
  • 后期绑定(动态绑定、运行时绑定):在运行时根据对象的类型进行绑定。
  • Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
  • 在讲解final关键字的时候讲到final关键字曾经可以提高运行效率,原因就在于它可以关闭动态绑定,必须前期绑定。

8.2.2 产生正确的行为

在编译时,编译器不需要获得任何特殊信息就能进行正确的调用。

Cycle cycle = new Tricycle();
cycle.ride();

8.2.3 可扩展性

一个良好的OOP程序中,大多数或所有方法都会遵循基类的模型,而且只与基类接口通信。
这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型。
多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。

8.2.4 缺陷:“覆盖私有方法”

  • 父类的私有方法子类是无法重载的,即子类的方法是一个全新的方法
  • 只有非private的方法才能被覆盖
  • 下述程序调用的依然是父类的对应方法
  • 约定:子类中的方法不能和父类中的private方法同名,能用起个名字解决的问题不要搞得那么复杂
public class Test {
    public static void main(String[] args) {
        Test test = new TestDemo();
        test.func();
        // Output: Test
    }

    private void func() {
        System.out.println("Test");
    }
}

class TestDemo extends Test {
    public void func() {
        System.out.println("TestDemo");
    }
}

8.2.5 缺陷:域和静态方法

  • 只有普通方法的调用是多态的
  • 当子类对象转型为父类对象时,任何域访问操作都由编译器解析,因此不是多态的
  • 如果某个方法是静态的,那么他就不是多态的

8.3 构造器和多态

构造器不具有多态性,因为它们也是隐式声明为static

8.3.1 构造器的调用顺序

  • 基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐想和那个链接,以便每个基类的构造器都能得到调用。
  • 因为只有基类的构造器才有恰当的方法和权限来初始化自己的元素,所以必须令所有构造器都得到调用,这样才能正确的构造对象。
  • 没有明确指定基类构造器,就是调用默认构造器
对象调用构造器顺序
  • 调用基类构造器(从根构造器开始)
  • 按声明顺序调用成员的初始化方法
  • 调用导出类的构造器

8.3.2 继承与清理

  • 通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常会留给GC进行处理。
  • 如果确实遇到清理的问题,在清理方法中要先写子类的清理逻辑,然后调用父类的清理方法;即清理顺序应该和初始化顺序相反

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

如果在构造器的内部调用正在构造的对象的某个动态绑定方法,会发生什么情况?

初始化的实际过程:
  1. 在其他任何事情发生之前,将分配给对象的存储空间初始化成二进制的零。
  2. 如8.3.1中那样调用基类构造器。因为在基类构造器中调用了func(),其实是被覆盖的func()方法。
  3. 按照声明的顺序调用成员的初始化方法。
  4. 调用导出类的构造器主体。
public class Test {
    public static void main(String[] args) {
        new Child(100);
    }
}

class Child extends Parent {
    private int i;
    void func() {
        System.out.println("Child func, i = " + i);
    }

    public Child(int i) {
        this.i = i;
        System.out.println("Before Child constructor, i = " + i);
        func();
        System.out.println("After Child constructor, i = " + i);
    }
}

class Parent {
    void func() {
        System.out.println("Parent func");
    }

    public Parent() {
        System.out.println("Before Parent constructor");
        func();
        System.out.println("After Parent constructor");
    }
}
Output:
Before Parent constructor
Child func, i = 0
After Parent constructor
Before Child constructor, i = 100
Child func, i = 100
After Child constructor, i = 100
编写构造器准则:
  • 用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。
  • 在构造器中唯一能够安全调用的是基类中的finalprivate方法,因为这些方法不会被覆盖。上述代码中把Parent中的func()变成private的会得到不一样的结果。

8.4 协变返回类型

子类覆盖(重写)父类的方法时,可以返回父类返回类型的子类。
这是JSE 5之后增加的功能,如下所示。Child中的func()返回的是父类返回类型List的子类ArrayList

class Child extends Parent {
    @Override
    ArrayList func() {
        return null;
    }
}

class Parent {
    List func() {
        return null;
    }
}

8.5 用继承进行设计

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

8.5.1 纯继承与扩展

纯继承
  • 只有在基类已经建立的方法才可以在导出类中被覆盖,纯粹的“is-a”的关系。
扩展
  • extends关键词的意思可以看出,仿佛是希望我们在基类的基础上扩展功能,即增加基类中不存在的方法,这可以称为“”is-like-a“”的关系。
  • 这样的缺点就是扩展部分不能被基类访问,主要是在向上转型的时候。

8.5.2 向下转型与运行时类型识别(RTTI)

  • 向上转型是安全的,因为基类不会具有大于导出类的接口。
  • 向下转型时会有运行时类型识别(Run-Time Type Identification)机制对类型进行检查,如果发现转型失败,会抛出一个运行时异常(ClassCastException)。
  • RTTI的内容不仅包括转型处理,还可以查看对象类型。

卢卡斯哔哔哔
46 声望6 粉丝