1. 抽象的概念
簡而言之:抽象就是以簡馭繁的藝術。
在程式設計中,抽象是從一組物件中抽取共同的特徵與行為,同時忽略具體細節(例如某些東西在「引擎蓋下」究竟如何運作)。想像你在畫城市地圖:上面標示了道路、建築、河流 — 但不包含每扇窗簾的顏色這種細節。地圖就是城市的抽象。
在物件導向(OOP)中,抽象是建立只呈現對問題重要的屬性與行為的類別與介面,並隱藏不必要的細節。
生活中的例子
- 交通工具是一種抽象。不論是公車、腳踏車,還是太空船 — 共同點在於它能移動,並且有乘客與駕駛。
- 動物也是抽象。所有動物都能呼吸、進食、移動 — 至於具體怎麼做,取決於物種。
- 付款是銀行軟體中的抽象。你不一定在乎是刷卡、透過 PayPal,還是用比特幣 — 重點是它能被處理並得到結果.
為什麼抽象很重要?
- 減少為當前任務不重要的細節分心。
- 設計更容易擴充與維護的系統.
- 透過共同介面與物件互動,而不必關心具體實作。
2. Java 中的抽象
在 Java 中,抽象主要透過兩種工具實現:
- 抽象類別(abstract class)
- 介面(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: 過度迷信抽象.
不要為了抽象而抽象。如果系統中只有一種物件型別,而且不會擴充,抽象只會讓程式碼更複雜。
GO TO FULL VERSION