点击进入我的博客
在面向对象的程序设计语言中,多态是继数据抽象(封装)和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。
多态的作用是消除类型之间的耦合关系。
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 构造器内部的多态方法的行为
如果在构造器的内部调用正在构造的对象的某个动态绑定方法,会发生什么情况?
初始化的实际过程:
- 在其他任何事情发生之前,将分配给对象的存储空间初始化成二进制的零。
- 如8.3.1中那样调用基类构造器。因为在基类构造器中调用了
func()
,其实是被覆盖的func()
方法。 - 按照声明的顺序调用成员的初始化方法。
- 调用导出类的构造器主体。
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
编写构造器准则:
- 用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。
- 在构造器中唯一能够安全调用的是基类中的
final
或private
方法,因为这些方法不会被覆盖。上述代码中把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的内容不仅包括转型处理,还可以查看对象类型。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。