Java中的多态性

Posted 小竹_phper

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的多态性相关的知识,希望对你有一定的参考价值。

1、上溯造型

之前我们已经知道将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。但这样做也会遇到一个问题,如下例所示:
//Inheritance & upcasting

class Note {
	private int value;

	private Note(int val) {
		value = val;
	}

	public static final Note middleC = new Note(0), cSharp = new Note(1), cFlat = new Note(2);
} // Etc.

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

// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
	// Redefine interface method:
	public void play(Note n) {
		System.out.println("Wind.play()");
	}
}

public class Music {
	public static void tune(Instrument i) {
		// ...
		i.play(Note.middleC);
	}

	public static void main(String[] args) {
		Wind flute = new Wind();
		tune(flute); // Upcasting
	}
}

输出结果:
Wind.play()

其中,方法Music.tune()接收一个Instrument 句柄,同时也接收从Instrument 衍生出来的所有东西。当一个Wind 句柄传递给tune()的时候,就会出现这种情况。

2、为什么要上溯造型

如果让tune()简单地取得一个Wind 句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument 的每种类型写一个全新的tune()。假设按照前面的推论,加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器):
//Overloading instead of upcasting
class Note2 {
	private int value;

	private Note2(int val) {
		value = val;
	}

	public static final Note2 middleC = new Note2(0), cSharp = new Note2(1), cFlat = new Note2(2);
} // Etc.

class Instrument2 {
	public void play(Note2 n) {
		System.out.println("Instrument2.play()");
	}
}

class Wind2 extends Instrument2 {
	public void play(Note2 n) {
		System.out.println("Wind2.play()");
	}
}

class Stringed2 extends Instrument2 {
	public void play(Note2 n) {
		System.out.println("Stringed2.play()");
	}
}

class Brass2 extends Instrument2 {
	public void play(Note2 n) {
		System.out.println("Brass2.play()");
	}
}

public class Music2 {
	public static void tune(Wind2 i) {
		i.play(Note2.middleC);
	}

	public static void tune(Stringed2 i) {
		i.play(Note2.middleC);
	}

	public static void tune(Brass2 i) {
		i.play(Note2.middleC);
	}

	public static void main(String[] args) {
		Wind2 flute = new Wind2();
		Stringed2 violin = new Stringed2();
		Brass2 frenchHorn = new Brass2();
		tune(flute); // No upcasting
		tune(violin);
		tune(frenchHorn);
	}
}

这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2 类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument 添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行重载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。这正是“多形性”大显身手的地方。

3、进一步理解上塑造型

形状例子有一个基础类,名为Shape;另外还有大量衍生类型:Circle(圆形),Square(方形),Triangle(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。
下面这幅继承图向我们展示了它们的关系:


技术分享
上溯造型可用下面这个语句简单地表现出来:
Shape s = new Circle();
在这里,我们创建了Circle 对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle 属于Shape 的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。
当我们调用其中一个基础类方法时(已在衍生类里覆盖):s.draw();
同样地,大家也许认为会调用Shape 的draw(),因为这毕竟是一个Shape 句柄。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw() ,因为后期绑定已经介入(多形性)。
"后期绑定":绑定在运行期间进行,以对象的类型为基础。
下面这个例子从一个稍微不同的角度说明了问题:
//Polymorphism in Java
class Shape {
	void draw() {
	}

	void erase() {
	}
}

class Circle extends Shape {
	void draw() {
		System.out.println("Circle.draw()");
	}

	void erase() {
		System.out.println("Circle.erase()");
	}
}

class Square extends Shape {
	void draw() {
		System.out.println("Square.draw()");
	}

	void erase() {
		System.out.println("Square.erase()");
	}
}

class Triangle extends Shape {
	void draw() {
		System.out.println("Triangle.draw()");
	}

	void erase() {
		System.out.println("Triangle.erase()");
	}
}

public class Shapes {
	public static Shape randShape() {
		switch ((int) (Math.random() * 3)) {
		default: // To quiet the compiler
		case 0:
			return new Circle();
		case 1:
			return new Square();
		case 2:
			return new Triangle();
		}
	}

	public static void main(String[] args) {
		Shape[] s = new Shape[9];
		// Fill up the array with shapes:
		for (int i = 0; i < s.length; i++)
			s[i] = randShape();
		// Make polymorphic method calls:
		for (int i = 0; i < s.length; i++)
			s[i].draw();
	}
}

运行结果:
技术分享
在主类Shapes 里,包含了一个static 方法,名为randShape()。它的作用是在每次调用它时为某个随机选择的Shape 对象生成一个句柄。请注意上溯造型是在每个return 语句里发生的。这个语句取得指向一个Circle,Square 或者Triangle 的句柄,并将其作为返回类型Shape 发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的Shape 句柄。main()包含了Shape 句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用draw()的时候,与各类型有关的正确行为会魔术般地发生,产生了上面的运行结果。

4、扩展性

现在,让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改true()方法。在一个设计良好的OOP 程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变。
对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示意图:

技术分享
所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使tune()位于一个独立的文件里,而将新方法添加到Instrument 的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:
//An extensible program
import java.util.*;

class Instrument3 {
	public void play() {
		System.out.println("Instrument3.play()");
	}

	public String what() {
		return "Instrument3";
	}

	public void adjust() {
	}
}

class Wind3 extends Instrument3 {
	public void play() {
		System.out.println("Wind3.play()");
	}

	public String what() {
		return "Wind3";
	}

	public void adjust() {
	}
}

class Percussion3 extends Instrument3 {
	public void play() {
		System.out.println("Percussion3.play()");
	}

	public String what() {
		return "Percussion3";
	}

	public void adjust() {
	}
}

class Stringed3 extends Instrument3 {
	public void play() {
		System.out.println("Stringed3.play()");
	}

	public String what() {
		return "Stringed3";
	}

	public void adjust() {
	}
}

class Brass3 extends Wind3 {
	public void play() {
		System.out.println("Brass3.play()");
	}

	public void adjust() {
		System.out.println("Brass3.adjust()");
	}
}

class Woodwind3 extends Wind3 {
	public void play() {
		System.out.println("Woodwind3.play()");
	}

	public String what() {
		return "Woodwind3";
	}
}

public class Music3 {
	// Doesn't care about type, so new types
	// added to the system still work right:
	static void tune(Instrument3 i) {
		// ...
		i.play();
	}

	static void tuneAll(Instrument3[] e) {
		for (int i = 0; i < e.length; i++)
			tune(e[i]);
	}

	public static void main(String[] args) {
		Instrument3[] orchestra = new Instrument3[5];
		int i = 0;
		// Upcasting during addition to the array:
		orchestra[i++] = new Wind3();
		orchestra[i++] = new Percussion3();
		orchestra[i++] = new Stringed3();
		orchestra[i++] = new Brass3();
		orchestra[i++] = new Woodwind3();
		tuneAll(orchestra);
	}
}
在main()中,当我们将某样东西置入Instrument3 数组时,就会自动上溯造型到Instrument3。可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。







































以上是关于Java中的多态性的主要内容,如果未能解决你的问题,请参考以下文章

java语言中的多态概述

LockSupport.java 中的 FIFO 互斥代码片段

Java中的多态

Java中的多态

Java中的继承和多态

精选Java中的多态和继承