Thinking in Java(7-1) 多型:向上轉型與方法綁定
在物件導向程式設計(OOP)中,多型(Polymorphism)是繼資料抽象和繼承之後的第三個基本特性。
多型透過將「做什麼」與「怎麼做」分離,從另一個角度將介面與實作分離。它不僅能夠改善程式碼的組織結構和可讀性,還能建立可擴充的程式。多型的方法調用允許一種類型表現出與其他相似類型之間的區別,只要它們是從同一個基礎類別衍生而來。
再論向上轉型
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
// Wind objects are instruments
// because they have the same interface:
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
*/
在上述程式中,從 Wind
向上轉型到 Instrument
可能會「縮小」介面,但不會比 Instrument
的完整介面更窄。進行向上轉型時,會刻意「忘記」物件的具體類別。如果讓 tune()
方法接受一個 Wind
類別的參數,似乎更直觀,但這樣會帶來一個重要問題:需要為系統中每一種 Instrument
的子類別都編寫一個新的 tune()
方法。
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
*/
雖然這種作法可行,但需要撰寫更多的程式碼,並且意味著日後若要添加新的 tune()
方法或從 Instrument
衍生新的類別,仍需要進行大量工作。如果忘記重載某個方法,編譯器也不會報錯。若讓方法只接受基礎類別,而不關心衍生類別,這正是多型所允許的。
動態方法綁定的轉機
tune()
方法接受一個 Instrument
的引用,但編譯器如何知道這個 Instrument
是 Wind
、Brass
還是其他類別呢?實際上,編譯器無法得知。
方法調用綁定
將一個方法調用與方法主體關聯起來,稱為綁定(binding)。
- 前期綁定:在程式執行前進行綁定,由編譯器和連結器實現。
- 後期綁定:在執行期間根據物件的類型進行綁定,又稱為動態綁定或執行期綁定。
在 Java 中,除了被 static
和 final
修飾的方法(private
方法也被隱式地視為 final
),其他方法都是後期綁定的。
產生正確的行為
了解 Java 的方法都是透過動態綁定實現多型後,我們可以撰寫只與基礎類別互動的程式碼,並且這些程式碼對所有衍生類別都能正確運行。換句話說,我們發送訊息給某個物件,讓物件自行判斷應該執行什麼操作。
向上轉型的語句可以非常簡潔:
Shape s = new Circle();
雖然看起來將一個引用賦值給不同類型的變數是錯誤的,但編譯器可以接受這種語句,因為透過繼承,Circle
就是一種 Shape
。如果調用 s.draw()
,直覺上可能認為會調用 Shape
的 draw()
方法,但由於後期綁定,實際上會正確地調用 Circle.draw()
。
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 Square extends Shape {
public void draw() { print("Square.draw()"); }
public void erase() { print("Square.erase()"); }
}
public class Triangle extends Shape {
public void draw() { print("Triangle.draw()"); }
public void erase() { print("Triangle.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
隨機產生不同類型的 Shape
,注意向上轉型在 return
語句中發生。從輸出結果可以看到,調用 draw()
方法時,會正確地執行衍生類別中的 draw()
。RandomShapeGenerator
是一種物件的「工廠」(factory),負責產生物件。
多型帶來的可擴充性
多型使得程式具有良好的可擴充性,我們可以在不修改現有代碼的情況下,添加新的子類別。
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
*/
透過向上轉型後的物件,tune()
方法不需關心其周圍程式碼的變化,仍能正常運行。我們所做的任何修改都不會對不應該受到影響的部分產生破壞。換句話說,多型是「將變化的事物與不變的事物分離」的重要技術。
覆蓋私有方法的陷阱
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()
方法是全新的方法。基礎類別的 f()
方法在子類別中不可見,無法被覆寫。非 private
方法才能被覆寫,雖然編譯器不會報錯,但執行結果並不符合預期。
欄位與靜態方法的多型限制
只有普通的方法調用是多型的,如果直接訪問某個欄位,這個訪問會在編譯期就進行解析,並非多型。
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.field
,必須明確地使用 super.field
。
同樣地,靜態方法也不具備多型性:
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()
*/
如果方法是靜態的,就不會具有多型性。以上程式中,staticGet()
方法的調用結果取決於引用的編譯期類型,而非執行期類型。
總結
多型是物件導向程式設計中強大的特性,它允許我們撰寫更具彈性和可擴充性的程式碼。透過向上轉型和動態方法綁定,我們可以讓基礎類別的引用操作衍生類別的物件,而不必關心物件的具體類型。然而,需要注意的是,私有方法、欄位和靜態方法不具備多型性,在使用時需特別小心。
Comments ()