OOP FAQ

Java 更新於 Oct 9, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 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
// 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
// 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
// java
Animal a = new Dog();     // reference type Animal, actual type Dog
a.eat();                  // resolved at RUNTIME -> Dog's version if overridden

編譯期(靜態)多型 —— 方法多載:同名但參數列不同,在編譯期就決定。

java
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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) 快速重點檢查表

text
[ ] 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

參考資料