文章目录
- 前言
- 一、向上转型回顾
- 1.忘掉对象类型
- 二、转机
- 1.方法调用绑定
- 2.产生正确的行为
- 3.可扩展性
- 三、构造器和多态
- 1.构造器调用顺序
- 2.构造器内部多态方法的行为
- 四、协变返回类型
- 总结
前言
本文是学习Java编程思想记录的笔记,主要内容介绍在 Java 中多态的概念。
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。
多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以 “生长” 的程序。
封装通过合并特征和行为来创建新的数据类型。隐藏实现通过将细节私有化把接口与实现分离。而多态是消除类型之间的耦合。继承允许把一个对象视为它本身的类型或它的基类类型。这样就能把很多派生自一个基类的类型当作同一类型处理,因而一段代码就可以无差别地运行在所有不同的类型上了。多态方法调用允许一种类型表现出与相似类型的区别,只要这些类型派生自一个基类。这种区别是当你通过基类调用时,由方法的不同行为表现出来的。
一、向上转型回顾
前面我们知道了如何把一个对象视作它的自身类型或它的基类类型。这种把一个对象引用当作它的基类引用的做法称为向上转型,因为继承图中基类一般都位于最上方。
public class Instrument {
public void play() {
System.out.println("Instrument.play()");
}
}
public class Wind extends Instrument{
@Override
public void play() {
System.out.println("Wind.play()");
}
}
public class Music {
public static void tune(Instrument i) {
i.play();
}
public static void main(String[] args) {
Wind wind = new Wind();
Music.tune(wind);
}
}
在 main() 中你看到了 tune() 方法传入了一个 Wind 引用,而没有做类型转换。这样做是允许的—— Instrument 的接口一定存在于 Wind 中,因此 Wind 继承了Instrument。从 Wind 向上转型为 Instrument 可能 “缩小” 接口,但不会比 Instrument 的全部接口更少。
1.忘掉对象类型
如果 tune() 接受的参数是一个 Wind 引用会更为直观。这会带来一个重要问题:如果你那么做,就要为系统内 Instrument 的每种类型都编写一个新的 tune() 方法。
public class Music {
public static void tune(Wind i) {
i.play();
}
public static void tune(Stringed i) {
i.play();
}
public static void main(String[] args) {
Wind wind = new Wind();
Music.tune(wind);
}
}
有一个主要缺点:必须为添加的每个新 Instrument 类编写特定的方法。这意味着开始时就需要更多的编程,而且以后如果添加类似 tune() 的新方法或 Instrument 的新类型时,还有大量的工作要做。
只写一个方法以基类作为参数,而不用管是哪个具体派生类,这正是多态所允许的。
二、转机
运行程序后,Wind.play() 的输出结果正是我们期望的,然而它看起来似乎不应该得出这样的结果。
观察 tune() 方法,它接受一个 Instrument 引用。那么编译器是如何知道这里的 Instrument 引用指向的是 Wind,而不是 Stringed 呢
1.方法调用绑定
将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做前期绑定。它是面向过程语言不需选择默认的绑定方式。
上述程序让人困惑的地方就在于前期绑定,因为编译器只知道一个 Instrument 引用,它无法得知究竟会调用哪个方法。
解决方法就是后期绑定,意味着在运行时根据对象的类型进行绑定。后期绑定也称为动态绑定或运行时绑定
。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息。
Java 中除了 static 和 final 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定
。这意味着通常情况下,我们不需要判断后期绑定是否会发生——它自动发生。
2.产生正确的行为
面向对象编程中的经典例子是形状 Shape。形状的例子中,有一个基类称为 Shape ,多个不同的派生类型分别是:Circle,Square,Triangle 等等。继承图展示了它们之间的关系:
向上转型就像下面这么简单:
Shape s = new Circle();
由于后期绑定(多态)被调用的是 Circle的 draw() 方法,这是正确的。
3.可扩展性
让我们回头看音乐乐器的例子。由于多态机制,你可以向系统中添加任意多的新类型,而不需要修改 tune() 方法。在一个设计良好的面向对象程序中,许多方法将会遵循 tune() 的模型,只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类派生出新的数据类型,从而添加新的功能。那些操纵基类接口的方法不需要改动就可以应用于新类。
三、构造器和多态
通常,构造器不同于其他类型的方法。在涉及多态时也是如此。尽管构造器不具有多态性(事实上人们会把它看作是隐式声明的静态方法),但是理解构造器在复杂层次结构中运作多态还是非常重要的。
1.构造器调用顺序
在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为 private,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器能初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是为什么编译器会强制调用每个派生类中的构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。
public class Instrument {
public Instrument() {
System.out.println("我是父类构造方法");
}
}
public class Wind extends Instrument{
public Wind() {
System.out.println("我是子类Wind构造方法");
}
}
public class Music {
public static void main(String[] args) {
Wind wind = new Wind();
}
}
2.构造器内部多态方法的行为
在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是类的派生类。
如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些bug 很隐蔽,难以发现。
public class Glyph {
void draw() {
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
public class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
@Override
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
public static void main(String[] args) {
new RoundGlyph(5);
}
}
初始化的过程是:
- 在所有事发生前,分配给对象的存储空间会被初始化为二进制 0。
- 如前所述调用基类构造器。此时调用重写后的 draw() 方法(是的,在调用 RoundGraph 构造器之前调用),由步骤 1 可知,radius 的值为 0。
- 按声明顺序初始化成员。
- 最终调用派生类的构造器。
因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的final 方法(这也适用于可被看作是 final 的 private 方法)。这些方法不能被重写,因此不会产生意想不到的结果。
四、协变返回类型
Java 5 中引入了协变返回类型,这表示派生类的被重写方法可以返回基类方法返回类型的派生类型:
public class Grain {
@Override
public String toString() {
return "Grain";
}
}
public class Wheat extends Grain{
@Override
public String toString() {
return "Wheat";
}
}
public class Mill {
Grain process() {
return new Grain();
}
}
public class WheatMill extends Mill {
@Override
Wheat process() {
return new Wheat();
}
public static void main(String[] args) {
Grain grain = new Mill().process();
System.out.println(grain.toString());
grain = new WheatMill().process();
System.out.println(grain.toString());
}
}
总结
多态意味着 “不同的形式”。在面向对象编程中,我们持有从基类继承而来的相同接口和使用该接口的不同形式:不同版本的动态绑定方法。