第八章 多态
在面向对象的程序设计语言中,多态是继抽象和技能之后的第三种基本特征。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。
多态的作用是用来消除类型之间的耦合关系。
8.1 再论向上转型
将某个对象的引用视为对其基类对象的做法被称作向上转型。但是这样做也有问题。看如下的例子:
public enum Note {
MIDDLE_C, C_SHARP, B_FlAT;
}
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
class Wind extends Insrument {
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);
}
}
output: wind.play() MIDDLE_C
Music.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument类。当tune()方法中接受到一个wind引用时,会自动向上转型,这样做时允许的,因为instrument中的接口再wind中都存在。从wind向上转型会缩小接口,但是不会小于Instrument的接口。
8.1.1 忘记对象类型
如果让tune()方法直接接受一个wind引用作为自己的参数,表面上更加直观,但是每加入一种新的乐器(也就是Instrument的子类),相应的tune方法就需要重载一次。但是如果利用了多态,将instrument的多个导出类引用看作时instrument的引用,就会轻松很多。
8.2 转机
在运行上面的程序之后,传入instrument的导出类引用给tune方法,它也能识别到底是哪个导出类,很多人都会奇怪,编译器怎么知道的呢?其实编译器根本就不知道。。。。
8.2.1 方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。绑定分为前期绑定和后期绑定。
- 前期绑定:面向过程中不需要选择就默认绑定的方式。
- 后期绑定:运行时根据对象的类型进行绑定。也称作动态绑定或者运行时绑定。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定的。
8.2.2 产生正确的行为
Java中所有方法都是通过动态绑定实现多态,我们可以只编写与基类打交道的程序代码了,并且这些代码都可以对所有导出类正确的运行。例如:
class Shape {
public void draw() {}
public void erase() {}
}
class Circle extends Shape {
public void draw() {
System.out.println("Circle.draw()");
}
public void erase() {
System.out.println("Circle.erase()");
}
}
class Square extends Shape {
public void draw() {
System.out.println("Square.draw()");
}
public void erase() {
System.out.println("Square.erase()");
}
}
class Triangle extends Shape {
public void draw() {
System.out.println("Triangle.draw()");
}
public void erase() {
System.out.println("Triangle.erase()");
}
}
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 Test {
public static void main(String[] args) {
RandomShapeGenerator rsg = new RandomShapeGenerator();
Shape[] s = new Shape[9];
for(int i = 0; i < s.length; i++) {
s[i] = rsg.next();
}
for(Shape shp : s)
shp.draw();
}
}
输出:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
上面的例子中,向上转型发生在return中,每次返回Circle、Square、Triangle三个其中的一个引用,但是我们通过next()方法获得的总是一个通用的Shape引用,再来利用动态绑定实现多态。
8.2.3 可扩展性
针对上面的两种方法(draw和erase),任何Shape的导出类,覆盖了这两个接口,利用动态绑定,我们都能够很好的对其进行使用,不用管每个导出类是如何实现的。
8.2.4 缺陷:对私有方法的覆盖
例如:
public class PrivateOverride {
private void f() {
System.out.println("private f()");
}
public static void main(String[] args) {
Private Override po = new Deived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}
像上面那样做,我们期待的结果是”public f()”, 但是结果为”private f()”,因为private属于final类方法,其不可以被覆盖,而且属于前期绑定,而非动态绑定,因此无法实现动态绑定。
8.2.5 缺陷:域与静态方法
一旦了解了多态机制,可能会认为所有事物都可以多态的发生,然而只有普通方法才表现出多态。如果直接访问某个域,将不表现为多态。例如:
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 JavaTest{
public static void main(String[] args) {
Super sup = new Sub(); //向上转型
//这里在直接访问域的时候,并没有出现理想中的多态
System.out.println("sup.field: " + sup.field + "\nsup.getField: " + sup.getField() + "\n");
Sub sub = new Sub();
System.out.println("sub.field: " + sub.field + "\nsub.getField: " + sub.getField() + "\nsub.getSuperField():" + sub.getSuperField());
}
}
输出为:
sup.field: 0
sup.getField: 1
sub.field: 1
sub.getField: 1
sub.getSuperField():0
当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,由于多态采用的是动态绑定,而不是靠编译器,所以无法完成多态。
在上面的例子中,一个sub对象中有两个叫做field的域(Super.field和Sub.field),然而在引用sub中的field时所产生的默认域并非Super版本的field,因此必须显示的指明super.field。
8.3 构造器和多态
通常构造器不同于其他的方法,涉及到多态的时候也是如此。尽管构造器并不具有多态性(实际上构造器是static方法,只不过是隐式的static声明)。
8.3.1 构造器的调用顺序
在之前的[学习笔记](“http://blog.csdn.net/jing_unique_da/article/details/45530563“)中也提到过了初始化的顺序,调用的顺序是:
- 调用基类的构造器。这个步骤一直递归下去,直至递归到根类,再开始从根向导出类开始初始化。
- 按声明顺序调用成员的初始化方法。
- 调用导出类构造器的主体。
8.3.3 构造器内部的多态方法的行为
现在来分析在一个构造器内部调用正在构造的对象的某个动态绑定方法时,会发生什么事情。例如:
class Super {
public int field;
public Super() {
System.out.println("Super() Before");
getField();
System.out.println("Super() after");
}
public int getField() {
System.out.println("Super() " + field);
return field;
}
}
class Sub extends Super {
public int field = 1;
public Sub(int i) {
field = i;
System.out.println("Sub() " + field);
}
public int getField() {
System.out.println("Sub() " + field);
return field;
}
}
public class JavaTest{
public static void main(String[] args) {
new Sub(5);
}
}
输出为:
Super() Before
Sub() 0
Super() after
Sub() 5
这里的getField()方法被覆盖了,但是在Super类的构造器中调用的getField方法并不是Super的,而是覆盖之后的方法,而且结果也不是5,而是0。原因在于前面讲述的构造顺序不完整。初始化的实际过程是:
- 在其他任何事情发生之前,先将分配给对象的存储空间初始化成二进制的零。
- 递归调用基类的构造器。
- 按照声明的顺序调用成员的初始化方法。
- 调用导出类的构造器主体。
8.4 协变返回类型
Java SE5中添加了协变返回类型,也就是在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型,话有些绕口,直接上例子:
class Grain {
public String toString() {
return "Grain";
}
}
class Wheat extends Grain {
public String toString() {
return "Wheat";
}
}
class Mill {
Grain process() {
return new Grain();
}
}
class WheatMill extends Mill {
Wheat process() {
return new Wheat();
}
}
public class JavaTest{
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}
结果:
Grain
Wheat