CodeGym /課程 /JAVA 25 SELF /多型與抽象的問題與陷阱

多型與抽象的問題與陷阱

JAVA 25 SELF
等級 23 , 課堂 3
開放

1. 多型:它是什麼,為什麼需要它

如果你以為多型是 Marvel 變種人的能力,那我要讓你失望了:在程式設計中一切平靜許多,但一樣充滿魔力。多型是指具有不同實作的物件,能夠對相同的方法呼叫做出不同的反應。

生活中的例子:
你有類別 BookMagazine,兩者都繼承自抽象類別 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:違反單一職責原則

如果一個抽象承擔太多責任,它就會變得難以維護。最好拆分為多個介面或類別。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION