1. 建立階層:從抽象到細節
實作抽象與階層是一種將程式碼從通用規則走向具體細節的結構化方式。先描述所有物件都應具備的能力(抽象),再說明每個具體類別如何實作。
在程式設計中,就像在生活中,一切都從提問開始。例如:「圓形與長方形有什麼共同點?」答案:它們都是 圖形。圖形又有什麼共通點?通常有面積,並且可以被繪製。
在 Java 中,這可以用 abstract 類別來表達:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
在這裡我們表示:
- 任何圖形都必須能計算自己的面積(area())。
- 任何圖形都必須能被繪製(draw())。
- 至於具體怎麼做——暫時不是我們關心的。
現在建立具體的圖形:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("繪製半徑為 " + radius + " 的圓");
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("繪製長方形 " + width + "x" + height);
}
}
我們做了什麼?
- 把共通部分抽到抽象類別。
- 在子類中細化行為。
示意圖:
. Shape
/ \
Circle Rectangle
表格:在哪裡實作了什麼
| 類別 | area() | draw() | 自有欄位 |
|---|---|---|---|
| Shape | |
|
- |
| Circle | 已實作 | 已實作 | |
| Rectangle | 已實作 | 已實作 | |
2. 為什麼這樣方便?(以及它為什麼奏效)
用統一介面處理不同物件
假設你有一個圖形集合:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
你可以不必考慮型別,以同樣方式遍歷它們:
for (Shape shape : shapes) {
shape.draw();
System.out.println("面積:" + shape.area());
}
讓 JVM 自己判斷哪個是圓、哪個是長方形!這就是多型。我們之前提過,接下來的單元還會更詳細說明。
擴充容易
想要新增三角形嗎?只要撰寫(新增型別——不改舊程式碼):Triangle extends Shape。
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("繪製三角形:底邊 " + base + ",高 " + height);
}
}
其餘程式碼(例如遍歷圖形清單)都不必修改.
避免重複程式碼
如果所有圖形都有共同屬性(例如顏色),把它提取到抽象類別會很方便:
public abstract class Shape {
private String color = "黑色";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
現在任何子類——無論是圓還是三角形——都會「繼承」這個顏色。
3. 實作練習:開發迷你圖形編輯器
讓我們把一切組合起來。想像你正在做一個簡單的圖形編輯器。
抽象類別 Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
具體圖形
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("繪製長度為 " + length + "、顏色為 " + getColor() + " 的線條");
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("線條的新長度:" + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("繪製橢圓,軸長為 " + a + " 與 " + b + ",顏色為 " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("橢圓的新軸長:" + a + "," + b);
}
}
想新增一個工具,例如 Polygon?只要建立新類別即可——整個編輯器都是透過抽象的 Figure 運作。
在程式中的使用
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
輸出:
繪製長度為 10.0、顏色為 red 的線條
線條的新長度:15.0
繪製橢圓,軸長為 5.0 與 3.0,顏色為 red
橢圓的新軸長:7.5,4.5
階層視覺化
. Figure
/ \
Line Ellipse
4. 如何避免重複:共用欄位與方法
有時所有子類不僅有共用方法,還有共用欄位(例如中心座標)。抽象類別是放置它們的理想位置:
public abstract class Figure {
private double x, y; // 中心座標
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("圖形已移動到點 (" + x + ", " + y + ")");
}
public abstract void draw();
}
現在任何 Line 或 Ellipse 都可以移動,而無須重新實作這個方法。
5. 另一個例子:支付系統
抽象不只是圖形!想像你在撰寫一套付款處理系統。
抽象類別 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 payment : payments) {
payment.process();
}
輸出:
處理信用卡付款
處理 PayPal 付款
6. 此種做法的優點
- 統一介面:可以用相同方式處理不同物件。
- 可擴充性:新增新種類的物件不需重寫舊程式碼。
- 最小化重複:共通部分被提取到基礎抽象類別。
- 彈性:可以使用抽象型別的集合,而不用關心細節。
7. 生活中的例子:運輸
抽象不只出現在教科書中。例如,你在設計交通運輸管理系統:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
具體的交通工具實作細節:
public class Car extends Transport {
@Override
public void move() {
System.out.println("汽車在道路上行駛");
}
@Override
public void fuelUp() {
System.out.println("加注汽油");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("腳踏車踩動踏板");
}
@Override
public void fuelUp() {
System.out.println("腳踏車不需要燃料,只需要給騎士三明治!");
}
}
8. 實用流程圖:如何建立抽象的階層
[抽象類別]
|
[具體子類別]
|
[更具體的子類別](如有需要)
- 共通的都放在上面!
- 獨特的都放在下面!
9. 實作抽象與階層時的常見錯誤
錯誤 1:在子類中重複程式碼。
若你發現每個子類都在寫相同的欄位或方法,這就是該把它們抽到抽象類別的訊號。如果能減少重複,不要害怕讓抽象「更寬」一些。
錯誤 2:違反「由泛到專」的原則。
有時初學者從細節開始建立階層,忽略了共通部分,結果出現像 RedCircleWithShadow 這樣難以融入結構的奇怪類別。務必先抽出抽象,再處理細節。
錯誤 3:階層過深。
如果繼承鏈超過 3–4 層,就該思考是否應以組合或介面替代繼承?
錯誤 4:強迫實作不相關的方法。
如果抽象類別包含太多對某些子類不適用的抽象方法,或許就該重新檢視結構。例如,並非所有交通工具都需要方法 fuelUp()(腳踏車就不需要)。
錯誤 5:混淆抽象類別與介面。
抽象類別——當需要共用狀態和/或部分實作時使用。介面——當只需「承諾」方法的存在,而不儲存資料也不提供實作時使用。非必要請勿混用。
GO TO FULL VERSION