OOP FAQ
範圍 — Java 面試會用到的物件導向基本功:四大支柱、interface 與 abstract class 的取捨、組合優於繼承、SOLID 與日常原則,以及物件的幾份契約。 另見:
../../cheatsheet/ood_design.md— 類別建模/低階設計題(停車場、電梯、販賣機);java_design_pattern.md— 這些原則長出來的那些模式。
1) OOP 的四大支柱
| 支柱 | 一句話 | Java 的機制 |
|---|---|---|
| 封裝 Encapsulation | 把資料與行為包在一起,藏起內部狀態 | private 欄位 + getter/setter |
| 繼承 Inheritance | 重用並特化一個基底型別 | extends、implements |
| 多型 Polymorphism | 一個介面,多種具體行為 | 覆寫 + 動態分派 |
| 抽象 Abstraction | 公開做什麼,藏起怎麼做 | interface、abstract class |
1-1) 封裝
欄位保持 private,只開放受控的存取。這樣才能守住不變條件,也讓你改內部實作而不弄壞呼叫端。
// java
public class BankAccount {
private double balance; // hidden state
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("must be positive");
balance += amount; // invariant enforced here
}
public double getBalance() { return balance; } // read-only access
}
為什麼重要:呼叫端沒辦法直接把 balance 設成負數。所有的改動都得走過會驗證的方法。
1-2) 繼承(is-a)
子類別就是父類別的特化版本,並繼承它的成員。
// java
class Animal {
void eat() { System.out.println("eating"); }
}
class Dog extends Animal { // Dog IS-A Animal
void bark() { System.out.println("woof"); }
}
注意:繼承是強耦合。用過頭會長出僵硬又脆弱的階層。如果你只是想重用程式碼,請優先用組合(見第 3 節)。
1-3) 多型
執行期(動態)多型 —— 方法覆寫 + 動態分派:由實際的物件型別決定跑哪一個方法。
// java
Animal a = new Dog(); // reference type Animal, actual type Dog
a.eat(); // resolved at RUNTIME -> Dog's version if overridden
編譯期(靜態)多型 —— 方法多載:同名但參數列不同,在編譯期就決定。
// java
int add(int a, int b) { return a + b; }
double add(double a, double b){ return a + b; } // overloaded
| 覆寫 Overriding | 多載 Overloading | |
|---|---|---|
| 何時決定 | 執行期 | 編譯期 |
| 簽章 | 相同 | 參數不同 |
| 位置 | 子類別重新定義父類別的方法 | 通常在同一個類別裡 |
| 回傳型別 | 相同或共變 | 可以不同 |
1-4) 抽象
把實作藏在一份契約後面。呼叫端依賴的是抽象,不是具體類別。
// java
interface Shape { double area(); } // WHAT, not HOW
class Circle implements Shape {
private final double r;
Circle(double r){ this.r = r; }
public double area(){ return Math.PI * r * r; } // the HOW
}
2) Interface 與 Abstract Class 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
最常見的 OOP 面試題之一。
| 面向 | interface |
abstract class |
|---|---|---|
| 多重繼承 | 可以 —— 一個類別可以實作很多個 | 不行 —— 只能有一個父類別 |
| 狀態(欄位) | 只能有 public static final 常數 |
可以有實例欄位 |
| 建構子 | 沒有 | 有 |
| 方法本體 | default/static 方法(Java 8+);其餘是抽象的 |
抽象與具體方法混用 |
| 存取修飾詞 | 成員隱含為 public |
都可以(private、protected……) |
| 關係 | 「能做什麼」的能力(Comparable、Runnable) |
「是什麼」的共同基底,帶共用程式碼 |
經驗法則:
- 要定義一種「許多不相關的類別都可能具備」的能力,或需要型別上的多重繼承時,用 interface。
- 子類別共用狀態或實作、而且確實構成一個 is-a 家族時,用 abstract class。
// java
interface Drawable { void draw(); } // capability
abstract class Widget { // shared base + state
protected int x, y; // common state
Widget(int x, int y){ this.x = x; this.y = y; }
void moveTo(int x, int y){ this.x = x; this.y = y; } // shared behavior
abstract void render(); // subclass must define
}
class Button extends Widget implements Drawable {
Button(int x, int y){ super(x, y); }
void render(){ /* ... */ }
public void draw(){ /* ... */ }
}
3) 組合與繼承 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
「優先用組合,而不是繼承。」
- 繼承(is-a):
Car extends Vehicle。編譯期決定、僵硬、只能有一個父類別,而且會把父類別的內部暴露給子類別。 - 組合(has-a):
Car持有一個Engine。執行期有彈性、可抽換,類別也維持得小。
// java — composition: Car HAS-A Engine, delegates to it
interface Engine { void start(); }
class ElectricEngine implements Engine { public void start(){ /* ... */ } }
class Car {
private Engine engine; // composed part
Car(Engine engine){ this.engine = engine; } // injected -> swappable
void start(){ engine.start(); } // delegation
}
// Swap ElectricEngine -> PetrolEngine WITHOUT changing Car.
什麼時候用哪個:
- 只有在真正的 is-a 關係、而且子型別可以被替換(Liskov)時,才用繼承。
- 要重用行為、組合多種能力,或在執行期改變行為時,用組合。
4) SOLID 原則 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
| 原則 | 意思 | 它修掉的壞味道 |
|---|---|---|
| S — 單一責任 | 一個類別只有一個改變的理由 | 什麼都做的上帝類別 |
| O — 開放封閉 | 對擴充開放,對修改封閉 | 每加一個新型別就要去改那個大 switch |
| L — Liskov 替換 | 子型別可以替換掉它的基底型別 | Square extends Rectangle 把 setWidth 弄壞 |
| I — 介面隔離 | 偏好小而專注的介面 | 肥大的介面逼你實作一堆空的或不支援的方法 |
| D — 依賴反轉 | 依賴抽象,不要依賴具體實作 | 高層類別被寫死綁在某個具體資料庫上 |
// java — Open/Closed with polymorphism instead of a switch
interface Discount { double apply(double price); }
class NoDiscount implements Discount { public double apply(double p){ return p; } }
class PercentDiscount implements Discount {
private final double pct;
PercentDiscount(double pct){ this.pct = pct; }
public double apply(double p){ return p * (1 - pct); }
}
// Add a new discount -> new class. Existing code is untouched (closed to modification).
5) SOLID 之外:日常原則 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
| 原則 | 它說什麼 | 它預防什麼 |
|---|---|---|
| DRY — 不要重複自己 | 每一則知識只有一個權威的家 | 同一條規則在三個地方修好了,第四個地方漏掉 |
| KISS — 保持簡單 | 選那個無聊的解法 | 一個方法就夠了,卻做了一整套框架 |
| YAGNI — 你不會需要它 | 只做今天的需求 | 沒人用得到、但每個人都得讀的可設定性 |
| 迪米特法則 | 只跟直接的協作者說話 | order.getCustomer().getAddress().getCity() —— 一節火車,把你和三個類別的內部綁在一起 |
| 命令,不要詢問 | 給物件下命令,而不是把它的狀態挖出來替它決定 | 貧血模型,所有邏輯都塞在「service」類別裡 |
| 對介面寫程式 | 依賴能力,不要依賴實作 | 明明 List x = … 就夠了,卻寫 ArrayList x = … |
| 組合優於繼承 | 見 §3 | 脆弱的基底類別 |
DRY 有一個值得在面試裡講出來的界線:**重複比錯的抽象便宜。**兩段只是長得像、 但會因為不同理由而改變的程式碼,就該各過各的。
6) 物件的契約 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
除了 equals/hashCode(§7),還有兩份契約幾乎每個設計都會碰到:
Comparable 與 Comparator
Comparable<T> |
Comparator<T> |
|
|---|---|---|
| 位置 | 由類別自己實作 | 一個獨立的物件 |
| 方法 | int compareTo(T other) |
int compare(T a, T b) |
| 代表 | 該型別的自然順序(只有一種) | 任意多種替代排序 |
| 用在 | 有一個明顯的順序(String、Integer、版本號) |
要依不同欄位排序,或要排序一個不是你寫的類別 |
// java
class Version implements Comparable<Version> { // natural order
private final int build;
Version(int build) { this.build = build; }
@Override public int compareTo(Version o) { return Integer.compare(build, o.build); }
}
// alternative orders, composed and reusable
Comparator<Employee> byPay = Comparator.comparingLong(Employee::salary)
.reversed()
.thenComparing(Employee::name);
契約:compareTo 必須符合遞移性與反對稱性,而且應該和 equals 一致 ——
不一致時,TreeSet(用 compareTo == 0 判重複)和 HashSet(用 equals 判重複)
對「什麼算重複」的看法就會不一樣。也永遠不要寫成 a.value - b.value:
值很大時會溢位。
複製:clone 與複製建構子
Object.clone() 是淺複製,要求實作 Cloneable,而且繞過建構子。複製建構子
(new Order(other))或靜態工廠方法更清楚,也守得住不變條件。最好的則是不可變 ——
既然什麼都不會變,就沒有什麼要複製。
7) 面試常見問答
Q:== 和 .equals() 差在哪?
對物件來說 == 比較的是參考(是不是記憶體裡同一個物件),對 primitive 則是比較值。.equals() 比較的是類別自己定義的邏輯相等。覆寫 equals() 時一定要一併覆寫 hashCode()。
Q:為什麼覆寫 equals() 就要覆寫 hashCode()?
契約是:相等的物件必須有相同的雜湊碼。以雜湊為基礎的集合(HashMap、HashSet)先用 hashCode() 找 bucket,再在 bucket 裡用 equals() 比對。違反這個契約,物件就會從這些集合裡「憑空消失」。
// java
@Override public boolean equals(Object o){
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override public int hashCode(){ return Objects.hash(x, y); }
Q:Java 有多重繼承嗎?
類別沒有(只能單一 extends),為的是避開「菱形問題」。但一個類別可以 implements 多個介面。Java 8 之後介面可以有 default 方法;如果兩個介面提供了同名的 default,實作的類別就必須覆寫來解決衝突。
Q:方法隱藏(hiding)和覆寫(overriding)差在哪?
static 方法是被隱藏而不是被覆寫 —— 由參考的型別在編譯期決定。實例方法才是覆寫 —— 由實際的物件型別在執行期決定。
Q:final、finally、finalize?
final:關鍵字 —— 常數變數/不可覆寫的方法/不可繼承的類別。finally:try/catch之後一定會跑的區塊(收尾清理)。finalize():已棄用的 Object 方法,GC 以前會在回收物件前呼叫它;不要依賴它。
Q:什麼是抽象方法?
宣告時沒有本體的方法(abstract void foo();)。它所在的類別必須是 abstract,而具體的子類別必須實作它。
Q:什麼是動態分派/晚期繫結? JVM 依照物件的執行期型別挑選被覆寫的方法,而不是依照宣告的參考型別。這就是執行期多型背後的機制。
Q:static 成員和實例成員?
static 成員屬於類別(只有一份,透過類別存取)。實例成員屬於每個物件。static 方法不能直接存取實例狀態,也不能被覆寫(只能被隱藏)。
Q:super 關鍵字是做什麼的?
指向直接的父類別:呼叫父類別的建構子(super(...)),或呼叫子類別已覆寫/隱藏掉的父類別方法或欄位(super.method())。
Q:內聚(cohesion)與耦合(coupling)?
- 高內聚(好):一個類別的成員都緊扣同一個目的。
- 鬆耦合(好):類別之間依賴抽象,盡量不知道彼此的內部。 目標是高內聚 + 鬆耦合。
Q:什麼是不可變類別?為什麼要用?
建構完成之後狀態就不能再改(例如 String、Integer)。欄位設成 private final、不要提供 setter,可變的輸入要做防禦性複製。好處:執行緒安全、可以安全當 map 的 key、也更好推理。
// java
public final class Money {
private final long cents;
private final String currency;
public Money(long cents, String currency){ this.cents = cents; this.currency = currency; }
public long getCents(){ return cents; } // no setters
}
Q:聚合(aggregation)與組合(composition)差在哪?
兩者都是 has-a。組合是強擁有;部分不能活得比整體久(House 和它的 Room)。聚合是弱擁有;部分可以獨立存在(Team 和它的 Player)。
8) 快速重點檢查表
[ ] Can explain the 4 pillars with a one-line example each
[ ] Interface vs abstract class: multiple inheritance, state, when to use
[ ] Composition over inheritance: why + a delegation example
[ ] All 5 SOLID principles + the smell each fixes
[ ] DRY / KISS / YAGNI / Law of Demeter — and where DRY stops
[ ] equals()/hashCode() contract and why both matter
[ ] Comparable vs Comparator, and consistency with equals
[ ] Overriding vs overloading (runtime vs compile-time)
[ ] Dynamic dispatch = runtime polymorphism mechanism
[ ] Immutability: how to build one and why it helps
參考資料
../../cheatsheet/ood_design.md— 物件導向設計/低階設計的題型../../cheatsheet/design.md— 資料結構與系統類的 coding 設計題java_design_pattern.md— 這些原則長出來的 GoF 模式java_basic.md— 語言機制(static、final、Object的方法)java_generics.md— 型別系統裡的變異性、通配符與替換