多态
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。
8.1 再论向上转型
我们把对某个对象的引用视为对其基类型的引用的做法称为向上转型–因为在继承树的画法中,基类是放置在上方的。为什么我们要向上转型成基类引用呢?主要还是为了复用代码。
/**
* 基类
*/
public class Instrument {
public void play(Note n) {
System.out.println("Instrument play()");
}
}
/**
* 导出类
*/
public class Wind extends Instrument{
public void play(Note n) {
System.out.println("Wind play()"+n);
}
}
public class Music {
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
//向上转型
Wind flute = new Wind();
tune(flute);
}
}
向上转型屏蔽了对象的类型,避免了为每个对象都重载一次tune方法。
多态允许定义方法时,仅接受基类作为参数,对于导出类同样可以正常使用。
8.2 转机
编译器是如何知道tune方法指向的是哪个方法呢?实际上编译器无法得知。
将一个方法调用同方法主题关联起来被称作绑定。
- 方法调用绑定
前期绑定:在程序执行前绑定。
后期绑定:在运行时根据对象的类型进行绑定。
Java中除了static方法和final方法,其他方法都是后期绑定。通过动态绑定实现多态。
-
产生正确的行为
一旦知道Java中所有方法都是通过动态绑定实现多态之后,我们就可以编写只与基类打交道的代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。 -
可扩展性
由于有多态机制。我们就可以根据自己的需求对系统添任意多的新类型。 -
缺陷:“覆盖”私有方法
public class PrivateOverride{
private void f(){ System.out.print("private f()");}
public static void main(String[] args){
PrivateOverride po = new Derived(); po.f();
}
}
class Derived extends PrivateOverride{
public void f() { System.out.print("public f()"); }
}/output:private f()
我们所期望的输出是public f(),但是由于private方法是自动认为是final方法,而且对导出类是屏蔽的。因此,这种情况下。Derived类中的f()方法就是一个全新的方法,基类中的f()方法在子类Derived中不可见,因此也不能被重载。只有非private方法才可以被覆盖。在导出类中,对于基类中的private方法,最好采用不同的名字。
- 缺陷:缺陷:域与静态方法
只有普通的方法是多态的。
8.3 构造器与多态
构造器调用顺序
-
调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然而是下一层导出类,等等,直到最低层的导出类。
-
按声明顺序调用成员的初始化方法。
-
调用导出类构造器的主体。
在导出类可以访问基类public和protected的成员,这意味着在导出类中必须保证基类的所有成员都是有效的。所以要首先调用基类构造器,保证基类的成员有效初始化。
2. 构造器内部的多态方法的行为
根据构造器调用的层次结构引出问题:如果在一个构造器的内部调用正在构造对象的某个动态绑定方法会发生什么情况?
如果要调用构造器内部的一个动态绑定方法,子类就要对这个方法进行覆盖。
/**
* 构造器内部动态调用
*/
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
public Glyph() {
System.out.println("Glyph before draw()");
draw();
System.out.println("Glyph after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
public RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(),radius = "+radius);
}
void draw(){
System.out.println("RoundGlyph.draw,radius = "+radius);
}
}
输出结果:
Glyph before draw()
RoundGlyph.draw,radius = 0 //实现了动态调用,但radius 初始值并不为1
Glyph after draw()
RoundGlyph.RoundGlyph(),radius = 5
构造器的初始化实际过程:
1)在其他任何事发生之前,将分配给对象的存储空间初始化成二进制零。
2)调用基类构造器(调用被覆盖的draw方法),由于1的原因,radius 的值为零
3)按照声明的顺序调用成员的初始化方法
4)调用导出类的构造器主体。
所以在构造器中唯一能安全调用是是final修饰的方法,应当避免调用动态方法。
8.4协变返回类型
协变返回类型:导出类中的被覆盖的方法可以返回基类方法的返回类型的某种导出类型。
/**
* 协变返回类型
* @author Administrator
*
*/
public class CovariantReturn {
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);
}
}
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();
}
}
8.5 用继承继续设计
相比继承,组合模式则更加灵活,可以在运行时改变对象状态。
/**
* 状态模式
* @author Administrator
*
*/
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
}
class Actor{
public void act() {}
}
class HappyActor extends Actor{
public void act() {
System.out.println("HappyActor");
}
}
class SadActor extends Actor{
public void act() {
System.out.println("SadActor");
}
}
class Stage{
private Actor actor = new HappyActor();
public void change() {
actor = new SadActor();
}
public void performPlay() {
actor.act();
}
}
向下转型过程中需要进行类型转换,如果不存在则会抛出异常,这种在运行期间对类型检查的行为称作运行时类型检查
用继承表现行为的差异,用组合表现状态的变化