很多前輩,老師,面試官考校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章類型資訊将會詳細介紹。