在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
向上转型
首先,对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用(继承中的概念)。而这种把对某个对象的引用视为对其基类型的引用的做法被称作为向上转型(因为继承树的画法中,基类是放置在上方的)。
看一个例子:
enum Note {
MIDDLE, SHARP, FLAT;
}
class Instrument {
public void play(Note n) {
System.out.println("Instrument.paly()");
}
}
class Wind extends Instrument {
//Redefine the interface method
public void play(Note n) {
System.out.println("Wind.paly() " + n);
}
}
public class Music {
public static void tune(Instrument i) {
//...
i.play(Note.MIDDLE);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);// Up casting
}
}
上面的这个例子中Music.tune()方法中接受了一个Instrument的引用,同时也接受任何导出自Instrument的类。在main方法中,当一个Wind引用传递到tune方法时,就会出现这种情况,而不需要任何类型的转换。
忘记对象类型
Music.java看起来似乎有些奇怪。为什么要故意忘记对象类型呢?在进行向上转型时,就会产生这种情况;并且如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎会更为直观。但是这样引发的一个重要的问题:如果这样做,就需要为系统内Instrument的美中类型都要编写一个tune的方法。
例如:
enum Note {
MIDDLE, SHARP, FLAT;
}
class Instrument {
public void play(Note n) {
System.out.println("Instrument.paly()");
}
}
class Wind extends Instrument {
//Redefine the interface method
public void play(Note n) {
System.out.println("Wind.paly() " + n);
}
}
class Brass extends Instrument {
//Redefine the interface method
public void play(Note n) {
System.out.println("Brass.paly() " + n);
}
}
public class Music {
public static void tune(Wind i) {
//...
i.play(Note.MIDDLE);
}
public static void tune(Brass i) {
//...
i.play(Note.MIDDLE);
}
//...
public static void main(String[] args) {
Wind flute1 = new Wind();
Brass flute2 = new Brass();
tune(flute1);
tune(flute2);
}
}
这样做并没有什么问题,但是有一个缺点,就是必须为添加的每一个新的Instrument类编写特定的方法。如果我们使用
enum Note {
MIDDLE, SHARP, FLAT;
}
class Instrument {
public void play(Note n) {
System.out.println("Instrument.paly()");
}
}
class Wind extends Instrument {
//Redefine the interface method
public void play(Note n) {
System.out.println("Wind.paly() " + n);
}
}
class Brass extends Instrument {
//Redefine the interface method
public void play(Note n) {
System.out.println("Brass.paly() " + n);
}
}
public class Music {
public static void tune(Instrument i) {
//...
i.play(Note.MIDDLE);
}
public static void main(String[] args) {
Wind flute = new Wind();
Brass flute2 = new Brass();
tune(flute);// Up casting
tune(flute2);
}
}
这样就不需要为每一个新的导出类重新编写重复的方法了。
转机
那么问题来了
public static void tune(Instrument i) {
//...
i.play(Note.MIDDLE);
}
它接受了一个Instrument引用。那么这种情况,编译器怎么才能知道
Instrument引用指向的是Wind对象呢。接下来探讨一下这个问题。
方法调用绑定
将一个方法的调用同一个方法的主体关联起来称为绑定(暂不理解)。
一旦知道Java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码的导出类都可以正确运行。或者说,发送消息给某个对象,让对象去判定应该做什么事。
看一个例子:
package polymorphism.shape;
/**
* Created by gmjiang on 17/4/7.
*/
public class Shape {
public void draw() {}
public void erase() {}
}
package polymorphism.shape;
/**
* Created by gmjiang on 17/4/7.
*/
public class Circle extends Shape {
public void draw() {
System.out.println("Circle.draw()");
}
public void erase() {
System.out.println("Circle.erase()");
}
}
package polymorphism.shape;
/**
* Created by gmjiang on 17/4/7.
*/
public class Square extends Shape {
public void draw() {
System.out.println("Square.draw()");
}
public void erase() {
System.out.println("Square.erase()");
}
}
package polymorphism.shape;
/**
* Created by gmjiang on 17/4/7.
*/
public class Triangle extends Shape {
public void draw() {
System.out.println("Triangle.draw()");
}
public void erase() {
System.out.println("Triangle.erase()");
}
}
package polymorphism.shape;
import com.sun.org.apache.regexp.internal.RE;
import java.util.*;
/**
* Created by gmjiang on 17/4/7.
*/
public class RandomShapeGenerator {
private Random rand = new Random(40);
public Shape next() {
switch (rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
package polymorphism;
import polymorphism.shape.*;
import java.util.Random;
/**
* Created by gmjiang on 17/4/7.
*/
public class Shapes {
private static RandomShapeGenerator gen =
new RandomShapeGenerator();
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] = gen.next();
}
//make polymorphic method calls
for(Shape shp : s) {
shp.draw();
}
}
}
output:
Square.draw()
Square.draw()
Triangle.draw()
Triangle.draw()
Triangle.draw()
Circle.draw()
Square.draw()
Circle.draw()
Circle.draw()
Shape基类为自它那里继承而来的导出类建立了一个公用的接口,多有的形状都可以描绘和擦除。导出类通过覆盖这些定义,为每一种特殊的类型的几何形状提供单独的行为。
向上转型是在
public class RandomShapeGenerator {
private Random rand = new Random(40);
public Shape next() {
switch (rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
每次调用next方法时,可以随机为Shape对象产生一个引用。在return语句中,每个return都返回的是某个特定形状的引用,并将其以Shape类型从next方法中发送出去。
可扩展性
大多数或者所有方法都会遵循tune模型(下面介绍),而且只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型,从而新添一些功能。那些操纵基类接口的方法不需要任何改动就可以应用与新类。
package polymorphism.music;
import org.omg.CORBA.PUBLIC_MEMBER;
/**
* Created by gmjiang on 17/4/7.
*/
enum Note {
MIDDLE, FLAT, HIGH
}
class Instrument {
void play(Note n) {
System.out.println("Instrument.play() " + n);
}
String what() {
return "Instrument";
}
void adjust() {
System.out.println("Adjusting Instrument");
}
}
class Wind extends Instrument {
void play(Note n) {
System.out.println("Wind.play() " + n);
}
String what() {
return "Wind";
}
void adjust() {
System.out.println("Adjusting Wind");
}
}
class Percussion extends Instrument {
void play(Note n) {
System.out.println("Percussion.play() " + n);
}
String what() {
return "Percussion";
}
void adjust() {
System.out.println("Adjusting Percussion");
}
}
class Stringed extends Instrument {
void play(Note n) {
System.out.println("Stringed.play() " + n);
}
String what() {
return "Stringed";
}
void adjust() {
System.out.println("Adjusting Stringed");
}
}
class Brass extends Wind {
void play(Note n) {
System.out.println("Brass.play() " + n);
}
void adjust() {
System.out.println("Adjusting Brass");
}
}
class Woodwind extends Wind {
void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
void adjust() {
System.out.println("Adjusting Woodwind");
}
}
public class Music {
//Doesn't care about type, so new types
//added to the system still work right:
public static void tune(Instrument i) {
//...
i.play(Note.FLAT);
}
public static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
//Upcasting during addition to the arrary:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}
output:
Wind.play() FLAT
Percussion.play() FLAT
Stringed.play() FLAT
Brass.play() FLAT
Woodwind.play() FLAT
(待理解)
缺陷:“覆盖”私有方法
package polymorphism.music;
import org.omg.CORBA.PUBLIC_MEMBER;
/**
* Created by gmjiang on 17/4/7.
*/
public class PrivateOverride {
private void f() {
System.out.println("private f()");
}
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}
output:
private f()
显然,我们期望的是输出public f(),但是由于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此,在这种情况,Derived类中的f方法就是一个全新的方法;既然基类中的f()方法在子类Derived中不可见,因此甚至也不能被重载。
结论就是:1、只有非private方法才能被覆盖;2、在导出类中,对于基类中的private方法,最好采用不同的名字。
缺陷:域与静态方法
一旦你了解多态机制,可能会开始认为所有事物都可以多态的发生。然而,只有普通的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问将在编译期进行解析
package polymorphism;
import org.omg.CORBA.PUBLIC_MEMBER;
/**
* Created by gmjiang on 17/4/7.
*/
class Super {
public int field = 0;
public int getField() {
return field;
}
}
class Sub extends Super {
public int field = 1;
public int getField() {
return field;
}
public int getSuperField() {
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); //upcast
System.out.println("sup.field = " + sup.field + ", sup.getField = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField = " + sub.getField() +
", sub.getSuperField() = " + sub.getSuperField());
}
}
output:
sup.field = 0, sup.getField = 1
sub.field = 1, sub.getField = 1, sub.getSuperField() = 0
当Sub对象转型为Super引用时,任何访问操作都将由编译器解析,因此不是多态的。在本例中,为Super.field和Sub.field,都分配了不同的存储空间。这样,Sub实际上包含两个称为field的域:他自己的一个,和他从Super继承得到的。然而,在引用Sub中的field时所产生的默认域并非Super版本的field域。因此,为了得到Super.field,必须显示地指明super.field。
尽管这看起来容易让人混淆,但是在实践中,并不会发生。因为我们通常将所有的域都设置称为private,因此不能直接访问它们,只能通过调用方法来访问。
如果某个方法是静态的,他的行为就不具有多态性。静态方法是与类,而并非与单个对象相关联的。