CodeGym /課程 /JAVA 25 SELF /OOP 中的抽象:為什麼需要與如何應用

OOP 中的抽象:為什麼需要與如何應用

JAVA 25 SELF
等級 19 , 課堂 0
開放

1. 抽象的概念

簡而言之:抽象就是以簡馭繁的藝術。

在程式設計中,抽象是從一組物件中抽取共同的特徵與行為,同時忽略具體細節(例如某些東西在「引擎蓋下」究竟如何運作)。想像你在畫城市地圖:上面標示了道路、建築、河流 — 但不包含每扇窗簾的顏色這種細節。地圖就是城市的抽象。

在物件導向(OOP)中,抽象是建立只呈現對問題重要的屬性與行為的類別與介面,並隱藏不必要的細節。

生活中的例子

  • 交通工具是一種抽象。不論是公車、腳踏車,還是太空船 — 共同點在於它能移動,並且有乘客與駕駛。
  • 動物也是抽象。所有動物都能呼吸、進食、移動 — 至於具體怎麼做,取決於物種。
  • 付款是銀行軟體中的抽象。你不一定在乎是刷卡、透過 PayPal,還是用比特幣 — 重點是它能被處理並得到結果.

為什麼抽象很重要?

  • 減少為當前任務不重要的細節分心。
  • 設計更容易擴充與維護的系統.
  • 透過共同介面與物件互動,而不必關心具體實作。

2. Java 中的抽象

在 Java 中,抽象主要透過兩種工具實現:

  1. 抽象類別abstract class
  2. 介面interface

本講次我們先聚焦於抽象類別。(很快就會談到介面!)

抽象類別是一種不打算直接實例化的類別。它為其他類別提供共同基底(樣板)。在抽象類別中可以定義已實作的方法(有方法本體)以及抽象方法(沒有方法本體) — 後者必須由子類別實作。

抽象方法是沒有實作的方法,也就是沒有方法本體。它的含義是:「此類別的所有子類都必須以自己的方式實作這個方法」。

範例:抽象「圖形」

public abstract class Shape {
    public abstract void draw(); // 抽象方法 — 沒有方法本體!
}

這裡我們宣告:「所有圖形都能被繪製,但我不知道具體怎麼畫 — 讓每個子類自己決定。」

為什麼不一定需要知道實作細節?

使用抽象時,你透過它的「介面」 — 也就是它必須支援的一組方法 — 與物件互動。至於方法內部怎麼做,並不重要。

例如,你呼叫物件 payment.process() 來處理付款。它的實作方式不重要 — 能運作就好。這帶來以下好處:

  • 可以在不修改使用端程式碼的情況下替換不同實作。
  • 簡化測試(可以用「stub(樁件)」替換實作)。
  • 讓程式碼更具彈性,且更能承受變更。

3. 抽象的優點

簡化設計與維護
抽象讓你不必在意多餘的細節。你不需要知道汽車引擎如何運作就能駕駛 — 只要方向盤、踏板,以及「往前開」的指示即可。在程式碼中亦然:當你透過抽象類別或介面工作時,你只會看到你需要的部分。

容易擴充系統
當系統建立在抽象之上,就能輕鬆加入新的物件型別。例如,有了抽象類別 Shape,你可以新增一種圖形 — Triangle,而不需修改既有程式碼。

降低元件耦合
若程式的各部分只透過抽象互動,它們就能彼此獨立變更。這就像插座與插頭:只要標準一致,就能接上各種電器。

4. 實作範例

我們來看看實務上抽象如何呈現。這些範例可以放進你的學習專案,方便你動手玩。

範例 1:類別「Shape」(圖形)

public abstract class Shape {
    public abstract void draw();
}

接著建立幾個具體的圖形:

public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("繪製圓形");
    }
}

public class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("繪製長方形");
    }
}

使用抽象:

Shape s1 = new Circle();
Shape s2 = new Rectangle();

s1.draw(); // 繪製圓形
s2.draw(); // 繪製長方形

這裡我們以變數型別 Shape 進行操作 — 內部具體是哪種圖形並不重要。這就是抽象的威力!

範例 2:類別「Payment」

public abstract class Payment {
    public abstract void process();
}

具體實作:

public class CreditCardPayment extends Payment {
    @Override
    public void process() {
        System.out.println("處理信用卡付款");
    }
}

public class PaypalPayment extends Payment {
    @Override
    public void process() {
        System.out.println("透過 PayPal 處理付款");
    }
}

使用方式:

Payment[] payments = {
    new CreditCardPayment(),
    new PaypalPayment()
};

for (Payment p : payments) {
    p.process();
}

結果是每筆付款都以自己的方式被處理,而呼叫它們的程式碼不需要為此操心。

5. 抽象與實作細節: 如何不被混淆

抽象關注「做什麼」,而非「怎麼做」

當你設計抽象時,你在回答:「物件應該會做什麼?」
而實作細節則是「它如何做到?」。

例如:

  • 抽象:「任何圖形都應該能被繪製(draw())。」
  • 實作細節:「圓形用圓來畫,長方形用四條線來畫。」

生活類比

想想電視遙控器。你不在乎它如何傳遞訊號,— 重點是有「電源」、「換台」和「音量加大」等按鍵。這就是抽象 — 一組你可以操作的按鍵。而設計遙控器的工程師才需要考慮實作細節。

6. 如何在程式中抽出適當的抽象

步驟 1. 找出共通點

檢視你的領域物件:它們有哪些共同點?例如,所有交通工具都能行駛,所有動物都能進食,所有付款都可以被處理。

步驟 2. 定義抽象類別

建立只包含共同行為的抽象類別。

public abstract class Transport {
    public abstract void move();
}

步驟 3. 在子類別中實作細節

public class Car extends Transport {
    @Override
    public void move() {
        System.out.println("汽車在道路上行駛");
    }
}

public class Bicycle extends Transport {
    @Override
    public void move() {
        System.out.println("腳踏車踩踏前進");
    }
}

步驟 4. 在程式碼中使用抽象

Transport[] transports = {
    new Car(),
    new Bicycle()
};

for (Transport t : transports) {
    t.move();
}

你的程式碼是與抽象互動 — 而不依賴具體實作細節。

7. 抽象 vs. 細節: 取得平衡

新手常見錯誤 — 把抽象做得過於細碎,或反過來過於寬泛。

  • 如果抽象過於寬泛(例如只有一個「doSomething」方法的類別「Object」),它幾乎沒有任何幫助。
  • 如果抽象過於細節化(例如「紅色外框、半徑為 5 的圓」),它就失去意義 — 倒不如直接寫具體類別。

黃金法則: 抽象只應涵蓋對你的任務而言真正共通且重要的部分。

8. 使用抽象時的常見錯誤

錯誤 №1: 嘗試建立抽象類別的物件.
抽象類別不適合直接建立物件。如果你嘗試寫 Shape s = new Shape();,編譯器會報錯:Cannot instantiate the type Shape。這就像想在商店裡購買一個「交通工具」本身。行不通:你必須選擇具體的腳踏車、汽車或公車。

錯誤 №2: 忘記在子類別中實作抽象方法.
若一個類別繼承了抽象類別,卻沒有實作其所有抽象方法,它本身也會成為抽象類別 — 因此無法被實例化。請確認你已實作所有必要的方法。

錯誤 №3: 把抽象與實作細節混在一起.
如果你開始把只適用於某個子類的細節放進抽象類別,這就是糟糕的抽象。例如,若在抽象類別 Payment 中出現了欄位 cardNumber,但不是所有付款都走信用卡。

錯誤 №4: 過度迷信抽象.
不要為了抽象而抽象。如果系統中只有一種物件型別,而且不會擴充,抽象只會讓程式碼更複雜。

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