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()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。

时间: 2024-10-26 08:14:16

Java中的多态性的相关文章

【Java_基础】java中的多态性

方法的重载.重写和动态链接构成了java的多态性. 1.方法的重载 同一个类中多个同名但形参有所差异的方法,在调用时会根据参数的不同做出选择. 2.方法的重写 子类中重新定义了父类的方法,有关方法重写的规则请参考文章:Java中方法重写的注意事项. 3.动态链接 动态链接出现在父类引用指向子类对象的场景. 因为子类中有一个隐藏的引用super指向父类实例,当出现父类引用指向子类对象时,子类对象就会将其隐藏的父类实例返回给该父类引用.因此,父类引用还是指向了父类实例,而不是像表面看到的那样指向了子

浅谈java中的多态性

讲到多态,就必须牵扯到继承和接口.至于多态强大的功能,目前水平有限,暂时还没有很明显地体会到. 我们先看 多态+继承 package test; public class Test { public static void main(String[] args) { A test = new B(); test.testA(100); } } class A { int i = 0; void testA(int i) { this.i = i; System.out.println("i的值为

Java中多态性的实现

class A ...{ public String show(D obj)...{ return ("A and D"); } public String show(A obj)...{ return ("A and A"); } } class B extends A...{ public String show(B obj)...{ return ("B and B"); } public String show(A obj)...{ re

java中的instanceof

instanceof是Java.php的一个二元操作符(运算符),和==,>,<是同一类东西.由于它是由字母组成的,所以也是Java的保留关键字.它的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据.可以用来判断继承中的子类的实例是否为父类的实现.相当于c#中的is操作符.java中的instanceof运算符是用来在运行时指出对象是否是特定类的一个实例.instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例. instanceof

Java中堆内存和栈内存详解

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用. 堆内存用于存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中

Java中的继承、封装、多态的理解

Java中的继承.封装.多态 继承的理解: 1.继承是面向对象的三大特征之一,也是实现代码复用的重要手段.Java的继承具有单继承的特点,每个子类只有一个直接父类. 2.Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类称为父类(有的也称其为基类.超类),父类和子类的关系,是一种一般和特殊的关系.就像是水果和苹果的关系,苹果继承了水果,苹果是水果的子类,水果是苹果的父类,则苹果是一种特殊的水果. 3.Java使用extends作为继承的关键字,extends关键字在

Java基础__慕课网学习(22):Java中的instanceof关键字

instanceof是Java的一个二元操作符,和==,>,<是同一类东东.由于它是由字母组成的,所以也是Java的保留关键字.它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据.举个例子: String s = "I AM an Object!"; boolean isObject = s instanceof Object; 我们声明了一个String对象引用,指向一个String对象,然后用instancof来测试它所指向的对象是否是Obj

java中重载与重写的区别

首先我们来讲讲:重载(Overloading) (1) 方法重载是让类以统一的方式处理不同类型数据的一种手段.多个同名函数同时存在,具有不同的参数个数/类型. 重载Overloading是一个类中多态性的一种表现. (2) Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义. 调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这就是多态性. (3) 重载的时候,方法名要一样,但是参数类型和个数不一样,返回值类型可以相同也可以不

黑马程序员【Java中的面向对象】

Java中的面向对象 在软件开发的学习中, 我最先接触的开发语言就是java,但都是简单的函数和循环数组的应用.说道面向对象,第一次看到这个词的时候还是在C#的学习过程中,我记得当时PPT上霸气的解释,什么是对象?万物皆对象!够霸气吧,当时的面向对象思想对我来说还是挺崩溃的,什么继承多态啊!经过了无数的联系项目的书写,终于对面向对象有了一定的理解,现在刚好在java的学习中再度重温面向对象,那么就将我眼中的面向对象写出来分享给大家. 说到面向对象,我们就不得不提一下他的三大特性,继承封装和多态.