天天看点

第8章 多态 —— 《Thinking in Java》学习笔记

很多前辈,老师,面试官考校Java菜鸟的时候时常会说:“面向对象的三大特性是什么?” ‘封装,继承,多态“,看过Java的各种教科书扉页的人相信都能轻松答出来吧。

但是所谓三特性是为何出现?为何如此重要?

 ----------------------------------------------------------------

多态通过分离做什么、怎么做,从另一个角度将接口和实现分离开来。可拓展性?—即消除类型之间的耦合关系。归根结底,还是为了代码的复用性。(?)

一、再论对象上转型

根据复用类的章节我们知道,对于基类的每一个派生类的调用方法,如果重新根据类型再度定义的话,不仅花费精力,而且是愚蠢的。

如果我们以基类类型进行定义方法参数(或者返回值之类的),那么我们就可以根据通过继承或实现基类从而使用那个方法而不需要再度定义

import static net.mindview.util.Print.*;

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
*///:~
           

这样编写无疑是愚蠢的。但是多态让事情出现了转机

public static void tuneInstrument i) {
    i.play(Note.MIDDLE_C);
  }
           

但是编译器是如何知道我们使用的是Wind对象(某个特定的 Instrument i=  new Wind()),而不是其他的派生类对象呢

这涉及到一个编译的名词,俗称绑定。

二、方法调用绑定

将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定,那么叫做前期绑定。例如C语言。多态的程序之所以迷惑,那就是因为我们脑中想象的就是一个前期绑定逻辑。因为当编译器知道只有Instrument引用时,他不知道调用哪个方法才对。

解决的方法就是后期(动态)绑定。Java除了static和final方法之外,都是使用动态绑定的方法,他可以让编译器在运行时判断对象的类型,从而调用合适的方法。也就是说:

编译器一直不知道对象的类型,但方法调用机制能找到正确的方法体。

一旦我们知道了动态绑定之后,我们就可以写出只和基类打交道的代码了。也就是[发送消息给某个对象,让该对象来判断做什么事]的程序了。

下面是经典的图形类 Shape -----> Circle - Square - Triangle

package org.hope6537.thinking_in_java.test;

import java.util.Random;

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 Shapes {
	private static RandomShapeGenerator gen = new RandomShapeGenerator();

	public static void main(String[] args) {
		Shape[] s = new Shape[9];
		for (int i = 0; i < s.length; i++)
			s[i] = gen.next();
		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()
 */
           

如上所示,编译器不需要获得任何特殊信息就能进行正确的调用。对draw的所有调用都是在动态绑定上进行的。

同时这样的设计还有助于增加可拓展性。

在大多数设计良好的OOP程序中。大多数或者所有的方法都会遵循模型方法,而且只和基类接口通信,这样的程序我们如果要拓展,我们仅仅就是可以在新类里继承或实现基类,并且实现或重写模型方法。如下面程序所示

class Instrument {
  void play(Object n) { System.out.println("Instrument.play() " + n); }
  String what() { return "Instrument"; }
  void adjust() { System.out.println("Adjusting Instrument"); }
}

class Wind extends Instrument {
  void play(Object n) { System.out.println("Wind.play() " + n); }
  String what() { return "Wind"; }
  void adjust() { System.out.println("Adjusting Wind"); }
}	

class Percussion extends Instrument {
  void play(Object n) { System.out.println("Percussion.play() " + n); }
  String what() { return "Percussion"; }
  void adjust() { System.out.println("Adjusting Percussion"); }
}

class Stringed extends Instrument {
  void play(Object n) { System.out.println("Stringed.play() " + n); }
  String what() { return "Stringed"; }
  void adjust() { System.out.println("Adjusting Stringed"); }
}

class Brass extends Wind {
  void play(Object n) { System.out.println("Brass.play() " + n); }
  void adjust() { System.out.println("Adjusting Brass"); }
}

class Woodwind extends Wind {
  void play(Object n) { System.out.println("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("Play!");
  }
  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);
  }
} 
           

模型方法(tune)完全忽略它周围代码所发生的全部变化,依旧正常运行,这正是我们期望多态所具有的特性。

将改变的事物和未变的事物分离开来 - Bruce

三、多态的一些缺陷

1、“覆盖”私有方法 —— 如果在派生类中声明一个方法f() 和基类的private f 方法重名。那么基类的f方法对于派生类的调用来说将不可见,而是默认调用“覆盖"了的f方法,但是实际上这并不是覆盖,甚至它不能被重载。

2、域与静态方法 —— 如果一个方法是静态的,那么他的行为就不具有多态性

四、构造器和多态

还是要重申复杂对象调用构造器的顺序:

1)调用基类构造器,这个步骤会不断的反复递归下去,首先是构造这个层次结构的根,然后是下一”层“ 导出类,直到最上层(就是我们可见层)

2)按照声明顺序调用成员的初始化方法。

3)调用导出类构造器的(主体)?

以下面的程序举例

import static net.mindview.util.Print.*;

class Meal {
  Meal() { print("Meal()"); }
}

class Bread {
  Bread() { print("Bread()"); }
}

class Cheese {
  Cheese() { print("Cheese()"); }
}

class Lettuce {
  Lettuce() { print("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { print("Lunch()"); }
}

class PortableLunch extends Lunch {
  PortableLunch() { print("PortableLunch()");}
}

public class Sandwich extends PortableLunch {
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() { print("Sandwich()"); }
  public static void main(String[] args) {
    new Sandwich();
  }
} /* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*///:~
           

首先是SandWich对象被构造,然后层层向上寻找基类 ---- ProtableLunch --- Lunch -- Meal 找到基类后回返

然后按照顺序调用Bread Cheese Lettuce 最后回到构造器的主体,打印出SandWich();

五、构造器内部的多态方法的行为

问题:如果一个构造器内部调用正在构造的对象的某个动态绑定方法,那会发生什么?

这的确是一个令人思考的问题,因为在方法的内部,动态绑定是后期绑定,从而是在构造期间,因此对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。

设想一下,如果要调用构造器内部的一个动态绑定方法,就要(也许)用到那个方法被(派生类)覆盖后的定义,然而被覆盖的方法在对象被【完全构造】之前就会被调用!

这可能会出现一些错误,因为我们知道构造器仅仅是构建对象过程中的一个步骤,如果导出(被调用方法所使用的)部分在构造器当前被调用的时刻依旧是没被初始化的?

这肯定会出错,轻者空指针,重则逻辑错误。

Bruce给出了一个例子

import static net.mindview.util.Print.*;

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
}	

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
}	

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
           

Glyph方法将会被覆盖,这个覆盖是在RoundGlyph中发生的。但是基类构造器会调用这个方法,结果调用了派生类的方法,初始化的值的确令人头疼。

所以我们有必要知道下初始化的其他流程

1)在其他任何事物发生之前,将分配对象的存储空间初始化成二进制的零

2)如前所述的那样构造基类构造器,此时调用被覆盖后的draw方法。

3)按照声明的顺序调用成员的初始化方法。

4)调用导出的构造类主体。

因此,编写构造器有一条有效的准则:"用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用它其他方法,唯一安全的就是基类中的final方法。

六、返回协变类型(用途未知)

他表示在导出类中被覆盖的方法可以返回基类方法类型中的某种导出类型。

七、继承-组合设计。

看起来多态是如此的强大,但是我们建立程序的首选还是以组合为主。而且组合更加灵活,他可以动态选择类型。

一条通用的准则就是:“用继承表达行为间的差异,用字段表达状态上的变化”

八、纯继承和拓展

Is-a和Is-Like-a的关系

对于拓展继承来说,如果我们使用的通信接口为基类接口,那么该派生类的拓展方法将无法被访问。在这种情况下,我们急于要用到【向下转型】。

九、向下转型和运行时类型识别

在Java中,所有的转型都会得到检查,如果检查没通过,就会返回一个ClassCastException 这种行为被称作RTTI 运行时类型识别。 第14章类型信息将会详细介绍。