[Java编程思想] 第八章 多态


第八章 多态

??“我曾经被问到‘求教,Babbage先生,如果你向机器中输入错误的数字,可以得到正确的答案吗?’我无法恰当地理解产生这种问题的概念上地混淆” ——Charles Babbage(1791-1871)

??再面向对象地程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。

??“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过讲细节“私有化”把接口和实现分离开来。继承允许将对象视为它自己本身的类型或其基类类型来处理。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出来的。这种区别是根据方法行为的不同表现出来的,虽然这些方法都可以通过同一个基类来调用。

继承与多态:

继承:类与类之间的关系

多态:方法行为的不同表现

8.1 再论向上转型

??对象既可以作为它自身的类型使用,也可以作为它的基类使用。

public enum Note{}
class Obj{
	public void play(Note n){
		System.out.println("Obj.play()");
	}
}

class A extends Obj{
	public void play(Note n){
		System.out.println("A.play()");
	}
}
class M{
	public static void tune(Obj o){			// 提供向上转型方法
		o.play(Note);
	}
    public static void main(String[] args){
    	A a = new A();
        tune(a);		// 向上转型,将A转为Obj
    }
}

??M.tune()可以接受Obj类型,也可以接受任何导出自Obj的类。

8.1.1 忘记对象类型

??忘记对象的类型,只记住它们的基类。

class B extends Obj{
	public void play(Note n){
		System.out.println("B.play()");
	}
}
class C extends Obj{
	public void play(Note n){
		System.out.println("C.play()");
	}
}
class M{
	public static void tune(A o){		// 为每一个新Obj导出类编写方法
		o.play(Note);
	}
	public static void tune(B o){
		o.play(Note);
	}
	public static void tune(C o){
		o.play(Note);
	}
    public static void main(String[] args){
    	A a = new A();
        B b = new B();
        C c = new C();
        tune(a);
        tune(b);
        tune(c);
    }
}

??可以这样做,但有一个主要缺点:必须为每一个新Obj类编写特定代码。

8.2 转机

??观察tune()方法:

	public static void tune(Obj o){
		o.play(Note);
	}

??接受一个Obj引用,编译器怎么知道这个Obj是A还是B?

8.2.1 方法调用绑定

??将一个方法调用同一个方法主体关联起来被称作绑定。

??上述程序疑问,主要是因为前期绑定。解决的办法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定。就是说,编译器一直不知道类型,但是方法调用机制能找到正确的方法体,并加以调用。

??Java中除了static方法和final方法(private属于final)之外,其他所有的方法都是后期绑定。通常我们不必判断,因为它会自动发生。

8.2.2 产生正确的形为

??了解Java所有方法都是通过动态绑定实现多态之后,就可以只编写与基类打交道的程序代码了。

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

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

??只有非private方法才能被覆盖,但是还需要注意覆盖private方法的现象,重写的private是全新的方法。

8.2.4 缺陷:域和静态方法

??多态只适用于普通方法,不适用于类成员变量。

??静态方法不具有多态性,静态方法是与类,而非与单个的对象相关联。

8.3 构造器和多态

??尽管构造器并不具有多态性(实际上是隐式static方法),但了解构造器的运作有助于避免一些困扰。

8.3.1 构造器的调用顺序

??基类构造器总是在导出类构造过程中被调用,而且按照继承层次逐渐向上,因为构造器有一项特殊任务:检查对象是否被正确构造。导出类只能访问自己的成员,基类成员通常是private,只有基类构造器才能恰当的对自己的元素进行初始化。因此必须另所有构造器都得到调用,否则就不可能正确构建完整对象。

??构造器调用顺序:

  1. 调用基类构造器。逐层调用。
  2. 按声明顺序调用(基类)成员的初始化方法。
  3. 调用导出类构造器主体。

8.3.2 构造器内部的多态方法的形为

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

??当在基类构造器中调用导出类重写过的方法,实际运行的是导出类的方法,并且这个导出类的成员可能还未初始化。

??构造器中唯一安全调用的方法是基类中final(final包括private)方法,因为这些方法不能被覆盖。

class A{
	void draw(){System.out.println("A.draw()");}
	A(){
		System.out.println("A() bdfore draw()");
		draw();						// 基类中多态调用,实际执行B.draw()方法
		System.out.println("A() after draw()");
	}
}

class B extends A{
	private int radius = 1;
	B(int r){
		radius = r;
		System.out.println("B.B(), radius = " + radius);	//初始化完成,构造器赋值radius=5
	}
	void draw(){
		System.out.println("B.draw(), radius = " + radius);
	}
}

public class M{
	public static void main(String[] args){
		new B(5);
	}
}

输出

A() bdfore draw()			
B.draw(), radius = 0		// 基类调用的实际是导出类多态方法,此时导出类成员还未初始化完成
A() after draw()
B.B(), radius = 5			// 此时整个初始化完成

??前一节讲述的初始化顺序并不十分完整,初始化实际过程是:

  1. 在其他任何事务发生前,将分配给对象的储存空间初始化成二进制的零。
  2. 如前所述那样调用基类构造器,此时,调用覆盖后(多态)的方法,由于步骤1的关系,多态方法中用到的成员属性radius值为0;
  3. 按照声明的顺序调用成员的初始化方法。
  4. 调用导出类的构造器主体。

8.4 用继承进行设计

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

??更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时,组合不会强制我们的程序设计进入继承的层次结构。

class O{
	public void act(){}
}
class A extends O{
	public void act(){System.out.println("A...");}
}
class B extends O{
	public void act(){System.out.println("B...");}
}
class S{
	// 组合
	private O o = new A();
	public void change(){o = new B();}
	public void performPlay(){o.act();}
}

public class M{
	public static void main(String[] args){
		S s = new S();		// S 成员初始化是 A
		s.performPlay();	// S 调用默认是 A
		s.change();		// S 修改状态为 B
		s.performPlay();	// S 成员变成 B
	}
}

输出

A...
B...

??通过继承得到两个不同的类,用于表达act()方法的差异;而S通过运用组合是自己的状态发生变化,这种情况下,这种状态的改变也产生了应为的改变

8.5 向下转型和运行时类型识别

??Java中所有转型都会得到检查!

8.6 总结

??多态意味着“不同的形式”。在面向对象程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式:不同版本的动态绑定方法。