概述
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时,还是在需要添加新功能时都可以“生长”的程序。
- 多态的作用是消除类型之间的耦合关系.
- 多态方法允许一种类型表现出与其他相似类型之间的区别,只要它们是从同一基类导出.
8.1在论向上转型
对象既可以作为它自己本身的类型使用,也可以作为其基类型来使用.这种把某个对象的引用视为对其基类型的引用的做法被称为向上转型.
例如:
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
} ///:~
乐器基类:
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
///:~
Wind是一种Instrument:
public class Wind extends Instrument {
// Redefine interface method:
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); // Upcasting
}
} /* Output:
Wind.play() MIDDLE_C
*///:~
- 在上述代码中,
Music.tune(...)
方法接受一个Instrument
类型的对象,同时也接受任何继承自Instrument
的类. - 在
main()
方法中传入一个Wind
到tune(..)
时,不需要任何类型转换,这是因为,Wind
从Instrument
继承而来,所以Instrument
中的接口必定存在于Wind
中;从Wind
向上转型到Instrument
可能会”缩小”接口,但是不是比Instrument
的全部接口更窄.
8.1.1忘记对象类型
那么问题就来了,为什么所有人都故意“忘记”对象的类型呢?
似乎,直接让tune()
方法去接收一个Wind
类型更加的直观?
这样做,会导致一个十分严重的问题:需要为系统内Instrument
的每种类型都编写一个新的tune(...)
方法.
假设按照上述逻辑,加入Stringed(弦乐),Brass(管乐)
这两种Instrument
:
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
class Brass extends Instrument {
public void play(Note n) {
print("Brass.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // No upcasting
tune(violin);
tune(frenchHorn);
}
} /* Output:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
*///:~
- 这样做是行得通的.但是有一个缺点:必须为每一个新
Instrument
类编写特定类型的方法.这意味着,在开始时,就需要更多的编程,这也意味着,如果以后想添加类似tune(...)
的方法,或者添加自Instrument
的导出类,任需要大量的工作. - 当我们写一个简单的方法,仅仅接收基类作为参数,而不是特殊的子类,这正是多态所允许的.
8.2转机
问题:
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
让wind
发声,可以调用tune(wind)
该方法接收一个Instrument
类型的引用,那么在这种情况下,编译器怎么才能知道,这个Instrument
引用指向的是Wind
对象而不是Brass
对象呢?
首先我们要清楚几个概念:
- 将一个方法调用和一个方法主体关联起来的过程叫做绑定;
- 在程序执行之前进行绑定,叫做前期绑定;
- 在运行时根据对象的类型进行绑定,叫做后期绑定(动态绑定,运行时绑定);
- Java中出了
static
和final
方法(private
方法属于final
方法)之外,其他所有方法都是后期绑定,并且自动发生;
答案就是:运行时绑定;编译器一直不知道对象的类型,但是在运行时会根据对象的类型动态的绑定,将方法的调用和对应的方法主体绑定起来.
举一个经典的例子:几何形状.
public class Shape {
public void draw() {}
public void erase() {}
} ///:~
public class Circle extends Shape {
public void draw() { print("Circle.draw()"); }
public void erase() { print("Circle.erase()"); }
} ///:~
public class Triangle extends Shape {
public void draw() { print("Triangle.draw()"); }
public void erase() { print("Triangle.erase()"); }
} ///:~
public class Square extends Shape {
public void draw() { print("Square.draw()"); }
public void erase() { print("Square.erase()"); }
} ///:~
public class RandomShapeGenerator {
private Random rand = new Random(47);
public Shape next() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
} ///:~
----------------------
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:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
*///:~
RandomShapeGenerator
是一种工厂,当我们每次调用next()
方法时,它可以随机的为Shape
对象产生一个引用.- 向上转型发生在
return
语句中.每一个return
语句取得一个指向某个Circle,Square,Triangle
的引用,并且从next()
方法中返回; - 无论在什么时候调用
next()
方法,我们事先都不能预测具体是什么类型. - 由输出信息可以得出,对数组中每个元素调用
draw()
方法时,与类型相关的特定行为会奇迹般的发生.
可以得出:在编译时,编译器不需要获得任何特殊信息就能进行调用.对draw()
方法的所有调用都是通过动态绑定进行的.
8.2.1可扩展性
在Instrument
的基础上,进行扩展,直接上代码:
class Instrument {
void play(Note n) { print("Instrument.play() " + n); }
String what() { return "Instrument"; }
void adjust() { print("Adjusting Instrument"); }
}
class Wind extends Instrument {
void play(Note n) { print("Wind.play() " + n); }
String what() { return "Wind"; }
void adjust() { print("Adjusting Wind"); }
}
class Percussion extends Instrument {
void play(Note n) { print("Percussion.play() " + n); }
String what() { return "Percussion"; }
void adjust() { print("Adjusting Percussion"); }
}
class Stringed extends Instrument {
void play(Note n) { print("Stringed.play() " + n); }
String what() { return "Stringed"; }
void adjust() { print("Adjusting Stringed"); }
}
class Brass extends Wind {
void play(Note n) { print("Brass.play() " + n); }
void adjust() { print("Adjusting Brass"); }
}
class Woodwind extends Wind {
void play(Note n) { print("Woodwind.play() " + n); }
String what() { return "Woodwind"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*///:~
- 增添新方法
what()
返回一个String类型的引用.添加一个adjust()
方法供每种乐器调音. - 在
main()
中我们只需要将某 - 种引用置入
orchestra
数组中,就会自动向上转型到Instrument
; tune()
方法忽略周围代码的变化,依旧正常运行,这就是我们期望多态所具有的的特性.- 换句话说,多态是一项让程序猿”将改变的事物与为改变的事物分离开来”的重要技术;
8.2.3缺陷
1.”覆盖”私有方法————————————————–
public class PrivateOverride {
private void f() { print("private f()"); }//注意访问控制符
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() { print("public f()"); }
} /* Output:
private f()
*///:~
我们期望输出的是public f()
,但是由于private
的方法默认是final
的,并且对子类是屏蔽的.因此在这种情况下,Derived
类中的f()
方法就是一个全新的方法
结论:只有非private
方法才可以被覆盖,但是还需要注意覆盖private
方法的现象,这时候,编译器不会报错,但是也不会按照我们所期望的来执行.
2.域————————————————–
谨记:只有普通的方法调用可以使多态的.
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
的filed
时产生的默认的域是其本身的. - 若要得到
Super.field
,必须显示指明super.field
总结:成员变量的访问操作是在编译期进行的,成员方法的操作是在运行时进行的.
3.静态方法————————————————–
class StaticSuper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSuper {
public static String staticGet() {
return "Derived staticGet()";
}
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSuper sup = new StaticSub(); // Upcast
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
} /* Output:
Base staticGet()
Derived dynamicGet()
*///:~
总结:静态方法是与类相关联,而非与单个对象相关联