1. 多型:它是什麼,為什麼需要它
如果你以為多型是 Marvel 變種人的能力,那我要讓你失望了:在程式設計中一切平靜許多,但一樣充滿魔力。多型是指具有不同實作的物件,能夠對相同的方法呼叫做出不同的反應。
生活中的例子:
你有類別 Book 與 Magazine,兩者都繼承自抽象類別 LibraryItem。你希望對任何圖書館項目都能呼叫 printInfo() 方法,並輸出正確資訊——對書籍顯示作者與書名,對期刊顯示期數與日期。
程式碼範例:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("書籍: " + title + ",作者: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("期刊: " + title + ",期數: " + issueNumber);
}
}
現在可以建立由不同元素組成的陣列,並對每個元素呼叫 printInfo():
LibraryItem[] items = {
new Book("蒼蠅王", "威廉·戈爾丁"),
new Magazine("科學與生活", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// 輸出:
// 書籍:蒼蠅王,作者:威廉·戈爾丁
// 期刊:科學與生活,期數:5
多型就是這樣運作的!
2. 多型的典型錯誤
嘗試呼叫基底型別中不存在的方法
最常見的錯誤之一——透過基底型別的參考去呼叫只在子類別中宣告的方法。
LibraryItem item = new Book("哈利·波特", "J. K. 羅琳");
// item.getAuthor(); // 編譯錯誤!在 LibraryItem 中沒有 getAuthor() 方法
Java 依據變數的型別(LibraryItem)而非實際物件(Book)來編譯程式碼。因此若你需要呼叫書籍特有的方法,就必須進行轉型:
if (item instanceof Book) {
Book book = (Book) item;
// 現在可以呼叫 book.getAuthor()
}
未檢查就轉型
如果你自以為物件是 Book,但其實不是,就會在執行階段得到 ClassCastException。例如:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // 砰!ClassCastException
正確作法——一定要先檢查型別:
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("這不是書!");
}
沒有善用多型的優勢
有時開發者會把程式碼寫得與具體型別強烈耦合,而其實可以使用抽象。例如,若你寫成:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
這只對書籍有效。如果明天出現期刊、報紙、漫畫呢?最好使用 LibraryItem[] 陣列,並操作基底類別或介面的成員方法。
3. 抽象:它的用途與避免踩雷
抽象類別與介面
抽象是擷取重點並隱藏細節的藝術。Java 提供抽象類別與介面來達成這件事。
- 抽象類別——無法直接建立實例,只能被繼承。
- 介面——一種契約:描述類別應該會什麼,而不是如何實作。
錯誤 1:建立沒有抽象方法的抽象類別
如果你的抽象類別一個抽象方法都沒有,請想一想——它真的需要是抽象的嗎?也許做成一般類別更簡單?
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// 如果沒有抽象方法,最好做成普通類別
錯誤 2:子類未實作必要方法
若類別繼承抽象類別或實作介面,它必須實作所有抽象方法。忘了實作——編譯器會提醒,但也常見「為了交差」而寫的空方法。這對於程式碼維護非常不利。
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// 空的!不好!
}
}
錯誤 3:過深或混亂的抽象層級
當類別彼此繼承達到五到十層時,理解起來會非常困難。最好維持「扁平」的層級結構,讓一切清楚易懂。
反例:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
很複雜,對吧?最好控制在兩到三層。
4. 實作:在教學應用中運用多型與抽象
讓我們擴充你的圖書館教學應用。先前你只有書籍。現在加入期刊,並為印刷品實作共通介面。
宣告抽象類別:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
加入子類別:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("書籍: " + title + ",作者: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("期刊: " + title + ",期數: " + issueNumber);
}
}
使用多型:
LibraryItem[] items = {
new Book("無瑕的程式碼", "羅伯特·馬丁"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
為電子出版品新增介面
假設有些出版品可以線上閱讀。我們引入一個介面:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("開啟電子書位址: " + url);
}
}
現在可以透過介面來操作電子書:
ReadableOnline ebook = new EBook("Java 入門", "巴里·伯德", "https://example.com/java");
ebook.openOnline();
5. 如何避免多型與抽象的問題:best practices
- 使用介面與抽象類別描述行為,而非狀態。
例如,介面 Printable 很適合描述「可列印」這種能力;但在介面中放一個欄位 String title 就是個壞主意。 - 在轉型前使用 instanceof 檢查型別。
特別是當物件可能有不同型別時。這能避免 ClassCastException。 - 追求「扁平」且易懂的層級結構。
繼承樹越簡單——越容易維護與擴充程式碼。 - 避免做出「沒有意義」的抽象。
如果類別不包含抽象方法,也不打算被繼承——就不要把它做成抽象的。 - 覆寫方法時務必使用註解 @Override。
這可幫助編譯器捕捉簽章錯誤。
6. 處理多型與抽象時的典型錯誤
錯誤 1:未檢查就轉型
有時會想「抄近路」不檢查就轉型。這也許會成功,但也可能導致程式突然崩潰。請永遠使用 instanceof:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
錯誤 2:透過基底型別引用呼叫子類方法
LibraryItem item = new Book("Java", "作者");
item.getAuthor(); // 編譯錯誤:在 LibraryItem 中沒有這個方法!
解法——要嘛轉型,要嘛把需要的方法加到基底類別(若這麼做合乎邏輯)。
錯誤 3:未完整實作介面或抽象類別
若忘了實作介面的所有方法——編譯器不會讓你通過建置。但若只寫「空殼」方法,什麼也不做,則會導致難以預期的行為。
錯誤 4:繼承層級過深
若你的繼承層級超過三層——想一想是否能簡化架構。
錯誤 5:違反單一職責原則
如果一個抽象承擔太多責任,它就會變得難以維護。最好拆分為多個介面或類別。
GO TO FULL VERSION