1. 什麼是類別階層?
在程式設計(以及其他領域)中,階層是一種樹狀結構:樹頂是較「共通」的實體(基底類別),往下則是更具體的實體(子類別)。在 Java 中,類別階層是藉由繼承建立的:每個子類別本身也可以成為其他子類別的父類別,依此類推。
類比:
- 動物(Animal)是通用的類別。
- 哺乳類(Mammal)是動物的一種特例。
- 狗(Dog)是哺乳類的一種特例。
- 而具體的「Sharik」則是 Dog 類別的一個物件。
在程式碼中如下:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
類別階層讓我們能在「上層」描述共通屬性與行為,在「下層」保留細節。這使程式碼更有邏輯,並可避免重複。
階層示意圖
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. 如何構建階層:邏輯與實務
辨識共通與特定
關鍵準則:基底類別應包含所有後代都具備的特徵;而獨特的部分應下放到子類別。
範例:
- 所有動物都會呼吸與進食——因此方法 breathe() 與 eat() 應該在類別 Animal 中。
- 只有鳥會飛——因此方法 fly() 應該在類別 Bird,而不是在 Animal。
- 只有狗會叫——方法 bark() 應該在類別 Dog。
範例:動物
// 基底類別
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " 在吃東西。");
}
void makeSound() {
System.out.println(name + " 發出聲音。");
}
}
// 子類別:哺乳類
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " 用母乳餵幼崽。");
}
}
// 子類別:狗
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " 在叫:汪汪!");
}
void wagTail() {
System.out.println(name + " 搖尾巴。");
}
}
// 子類別:貓
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " 在喵叫:喵!");
}
void purr() {
System.out.println(name + " 發出呼嚕聲。");
}
}
// 子類別:鳥
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " 在飛。");
}
@Override
void makeSound() {
System.out.println(name + " 在啁啾:啾啾!");
}
}
在 main 中的呼叫:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Sharik");
Cat murka = new Cat("Murka");
Bird sparrow = new Bird("Vorobey");
sharik.eat(); // Sharik 在吃東西。
sharik.makeSound(); // Sharik 在叫:汪汪!
sharik.feedMilk(); // Sharik 用母乳餵幼崽。
sharik.wagTail(); // Sharik 搖尾巴。
murka.eat(); // Murka 在吃東西。
murka.makeSound(); // Murka 在喵叫:喵!
murka.feedMilk(); // Murka 用母乳餵幼崽。
murka.purr(); // Murka 發出呼嚕聲。
sparrow.eat(); // Vorobey 在吃東西。
sparrow.makeSound(); // Vorobey 在啁啾:啾啾!
sparrow.fly(); // Vorobey 在飛。
}
}
視覺化:類別樹
| 類別 | 父類別 | 特點 |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. 更多生活中的例子
幾何圖形
類別階層非常適合用來建模幾何圖形。
// 基底類別
class Shape {
void draw() {
System.out.println("繪製圖形。");
}
}
// 圓形
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("繪製半徑為 " + radius + " 的圓");
}
}
// 長方形
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("繪製長方形 " + width + "x" + height);
}
}
使用方式:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // 繪製半徑為 5.0 的圓
s2.draw(); // 繪製長方形 3.0x4.0
}
}
交通工具
class Vehicle {
void move() {
System.out.println("交通工具正在移動。");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("汽車在道路上行駛。");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("自行車騎士在踩踏板。");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // 汽車在道路上行駛。
v2.move(); // 自行車騎士在踩踏板。
使用者
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " 已登入系統。"); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " 刪除了使用者 " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " 完成了一筆購買。"); }
}
4. 實用細節
不要濫用繼承。 繼承是用於「is-a」關係的工具。如果你要表達「貓是動物」,請使用繼承;如果是「貓擁有尾巴」,請使用組合(has-a)。
錯誤示範:
class Engine { /* ... */ }
class Car extends Engine { /* 車就是引擎嗎?不是,這是組合! */ }
正確示範:
class Car {
Engine engine; // 汽車包含引擎
}
不要建立過深的階層。 層級越多,維護與理解就越困難。多數情況下 2–3 層已足夠。
也不要讓階層過於「扁平」。 如果有 20 個類別都直接繼承同一個基底類別,可能該重新檢視架構。
為了「方便」而繼承。 有時會想為了「偷」幾個方法或欄位而不顧語意。這會走向混亂!如果 class A 與 class B 在語意上無關,就不要讓 class B extends A 只為了那幾個方法。
違反 Liskov 置換原則。 若子類別無法在不出現意外的情況下被當作父類別替換使用——代表階層設計不正確。
程式碼重複。 如果你發現自己在想「咦,這個方法在好幾個類別都在複製」,也許該把它抽到基底類別。
5. 建立類別階層時的常見錯誤
錯誤 №1:缺乏「is-a」語意的繼承。
如果你僅為了取得方法或欄位而使用繼承,而不是因為子類別確實「是」父類別的一種,你的程式碼很快會變得混亂。例如,「汽車繼承引擎」不是 is-a,而是 has-a。
錯誤 №2:忽略子類別的獨特性。
若所有子類別看起來都一樣、沒有新增任何功能,可能根本不需要階層——使用單一類別即可。
錯誤 №3:在子類別中重複程式碼。
若你在多個子類別中複製相同實作,應將其上移到父類別。
錯誤 №4:階層過於複雜或過深。
多層級的階層很難維護與測試;越簡單越好!
錯誤 №5:覆寫方法時未使用 @Override 標註。
沒有標註很容易在方法簽章上出錯,導致方法沒有被正確覆寫而是被視為新的方法。請務必使用 @Override!
GO TO FULL VERSION