> ## Documentation Index
> Fetch the complete documentation index at: https://meta.niceshare.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 里氏替換原則

> 里氏替換原則是指子類型必須能替換父類型，且不破壞程式已依賴的正確性。了解利斯科夫起源、Java Properties 案例與邊界。

<Info>
  **類別**: 原則<br />
  **類型**: 系統設計原則<br />
  **來源**: 芭芭拉·利斯科夫，OOPSLA 主題演講 *Data Abstraction and Hierarchy*，1987 年<br />
  **別名**: LSP；行為子類型；強行為子類型；Liskov Substitution Principle
</Info>

<Note>
  **快速回答** — **里氏替換原則**（Liskov Substitution Principle）是指：子類型只有在守住父類型的行為契約時，才能頂替父類型，讓呼叫方不必知道手裡到底是哪一個物件。芭芭拉·利斯科夫在 1987 年 OOPSLA 主題演講裡提出替換性質；她與珍妮特·溫在 1994 年把它形式化。實務檢驗很硬：如果呼叫父方法之前必須先看具體種類，這棵繼承樹已經在說謊。
</Note>

## 什麼是里氏替換原則？

里氏替換原則是一條設計規則：子類型必須能用在父類型被期望出現的任何地方，並且不破壞那些呼叫方已經依賴的正確性。

> 若對類型 S 的每個物件 o1，都存在類型 T 的物件 o2，使得所有按 T 來寫的程式 P 在把 o1 換成 o2 後行為不變，則 S 是 T 的子類型。

這不是編譯器檢查。編譯器對的是名字和簽章。這條原則對的是承諾：父類型允許呼叫方假定的事情，換上子類型之後仍應成立。

日常圖像是代課老師。課程表仍寫著「第二節數學」。能把這節課上下來的代課老師，才是可替換的。不肯點名、或必須原任坐在教室裡才能教的人，不是。學校不該因為椅子上換了人，就把一天重排一遍。

在程式碼裡，正方形繼承長方形是最常被撞上的傷。長方形承諾：改寬不必改高。正方形要保持邊相等，就得兩邊一起改。先設寬再讀高的程式會吃一驚。家譜寫著「是一種」。契約沒有。[開放封閉原則](/zh-hant/principles/open-closed)需要這條規則：如果舊客戶必須先檢查子類型是哪一種，你就插不進新的子類型。

### 里氏替換原則的三層理解

* **入門**：自稱能填一個角色，就必須做這個角色已經公開的那些工作。能塞進三號電池槽、電壓卻不同的電芯，形狀一樣，契約不一樣。
* **實務**：動手繼承之前，先列出父類型的承諾——呼叫方可傳入什麼、可期望回傳什麼、什麼事絕不發生。子類型可以更慷慨，不可以更挑剔。
* **進階**：行為子類型既是快照，也是歷史約束。可變物件不得引入父類型已經排除的狀態變化。所以能從中間戳一把的堆疊，即使也有 push 和 pop，仍不是堆疊。

## 起源

里氏替換原則起於類型層次的問題，不是貼在團隊牆上的口號。

**芭芭拉·利斯科夫**當時在 MIT 電腦科學實驗室，於 **OOPSLA '87**（奧蘭多，**1987 年 10 月 4–8 日**）做了主題演講 *Data Abstraction and Hierarchy*。講稿收入會議增補（ACM，doi 10.1145/62138.62141），後又刊於 *ACM SIGPLAN Notices* **23**(5): 17–34（**1988 年 5 月**）。她在 1970 年代已用 **CLU** 語言做資料抽象。在 OOPSLA 上，她主張：Smalltalk 式的*實作*繼承，並不等於呼叫方可以信任的類型層次。她讀到的一篇論文把堆疊和佇列當成彼此的子類型。這不可能成立：按後進先出寫的程式，拿到先進先出就會壞。

她與 **珍妮特·溫**（Jeannette Wing）在 *A Behavioral Notion of Subtyping*（*ACM Transactions on Programming Languages and Systems* **16**(6): 1811–1841，**1994 年 11 月**，doi 10.1145/197320.197383）裡給了這想法形式上的牙齒。他們的子類型要求是：凡能對類型 T 的物件證明的性質，對子類型 S 的物件也必須成立。這裡的子類型是*規格*之間的關係，不是類別檔案之間的關係。

**羅伯特·C·馬丁**在 **1996 年 3 月**的 *C++ Report* 工程筆記專欄 “The Liskov Substitution Principle” 裡，把這想法帶進日常物件導向實務。他的工作定義是：

> 使用基底類別指標或參照的函式，必須能夠在不知情的情況下使用衍生類別的物件。

馬丁 **2003 年**的《敏捷軟體開發：原則、模式與實務》（Prentice Hall）保留了這一檢驗。大約 **2004 年**，**Michael Feathers** 把馬丁的「前五條」排成 SOLID 口訣。里氏替換原則是其中的 「L」。利斯科夫因資料抽象、容錯與分散式計算獲得 **2008 年** ACM 圖靈獎；獎項比這一條規則更寬，但名字因此留在了工程口語裡。

## 核心要點

里氏替換原則真正值錢的時候，是一種新東西必須填進舊槽位。它不是禁止額外功能。它是一條關於哪些承諾不許收回的規則。

<Steps>
  <Step title="「是一種」是契約，不是長得像">
    共用幾個欄位、或圖個方便去繼承某個父類別，並不構成子類型。父類型的呼叫方有權得到公開行為，而不是你的繼承圖。企鵝繼承了 `fly()` 再拋例外，並不是「一種特殊的鳥」，而是父類型聲稱得太多。[關注點分離](/zh-hant/principles/separation-of-concerns)告訴你把工作拆開。這條原則告訴你，不要用假家譜再黏回去。
  </Step>

  <Step title="不要把門收窄，也不要把結果變弱">
    子類型可以放寬前置條件（接受更多輸入），加強後置條件（對輸出保證更多）。箭頭不能反轉。父類型說「任意正數金額」，子類型拒絕小於 100 的金額，就不可替換。父類型說「回傳非空清單」，子類型回傳空，就不可替換。多出來的方法可以。把舊方法做得更安靜、更挑剔，不行。
  </Step>

  <Step title="不變式和歷史，在替換之後仍須成立">
    父類型聲稱對每個合法物件都為真的事情，替換後仍須為真。屬性表本應只裝字串，卻能透過父方法塞進整數，不變式已經破了。歷史也要緊：若父類型從不允許把某個值縮小，子類型的 `setWidth` 順帶砍掉高度，就是在改寫呼叫方以為已經合上的過去。
  </Step>

  <Step title="呼叫方必須檢視具體種類，層次就已經失敗">
    一串針對執行時期類型的 `if`，或「別在這個特殊子類上呼叫」的註解，就是氣味。每來一個新表親，這些呼叫方都得改——這條原則和[開放封閉原則](/zh-hant/principles/open-closed)會一起失效。[單一職責原則](/zh-hant/principles/single-responsibility)常常壓在底下：一個類型扛了兩份工作，繼承被當成膠水。
  </Step>
</Steps>

## 應用場景

當一個槽位會被幾種東西填上、而舊客戶不該去讀警告標籤時，再用里氏替換原則。不要用它逼所有相似名詞都掛到同一個父類別上。

<CardGroup cols={2}>
  <Card title="在加上下一個表親之前，先劈開說謊的層次">
    正方形必須保持邊相等，它就不是可以隨便改尺寸的長方形。定期存款不能提領，它就不是父類型已經承諾了 `withdraw` 的帳戶。做更瘦的父類型——或者不要父類型——只給每種東西它真正能兌現的方法。助教的評分類型也是同一刀：「不能改分的測驗」不是指令碼其餘部分預設可改的「可評分項」。
  </Card>

  <Card title="用崗位本身、而不是用徽章，去核對待崗角色">
    不能簽採購單的代理經理，替換不了流程已經依賴的經理。要麼給副手同樣的簽字權，要麼改流程，讓簽字不再屬於這個槽位。職位名稱是繼承圖。工作流程才是契約。這裡的意外既是[最小驚訝原則](/zh-hant/principles/least-astonishment)的失敗，也是替換的失敗。
  </Card>

  <Card title="寫出一個替補真正能擔任的社團職務">
    學校或社區團體常常設一個「財務」，然後發現備份的人進不了銀行、交不了報表、也拿不到鑰匙。章程承諾的是一個角色。備份填上的是一個名字。先列出這個職務必須做的事——報帳、保管鑰匙、出席會議——再問誰能頂上。必須原任在場才能轉的值日表，不是值日表。
  </Card>

  <Card title="把「相容」的公共表格當成契約，而不是長得像">
    辦事窗口處理不了的替代證件、「視同」稅表、或無法按常規窗口辦理的臨時許可，都不可替換。市民用多跑的腿為錯配付錢。若公開表格寫著任何匹配附件都可以，每一份匹配附件就必須真的可以。若有些不行，就把限制印在父表格上；不要藏進一個仍用同一標題的子類型裡。
  </Card>
</CardGroup>

## 經典案例

里氏替換原則作為操作傷口的有數字窗口，是 **Java 的 `Properties` 類別**——不是聲稱一個庫類別就是類型論的全部。

從 **JDK 1.0**（**1996 年**）起，`java.util.Properties` 表示一份可從串流載入、也可存回串流的持久字串表。它被寫成 `Hashtable` 的子類別，而 `Hashtable` 接受任何非空鍵值。父類型的公開契約，比子類型的公開工作更寬。因為這份繼承，`put` 和 `putAll` 可以打在 `Properties` 物件上。官方 Java API 至今仍說「強烈不鼓勵」這樣用，因為呼叫方能塞進非字串的鍵或值。若再對這份「被污染」的物件呼叫 `store` 或 `list`，呼叫會失敗——典型是型別轉換錯誤。子類型並沒有收回父方法。它繼承了一扇自己守不住的門。

人們指著這件事已經幾十年。**2016 年 5 月 17 日**，OpenJDK 缺陷 **JDK-8157123** 以 “java.util.Properties class is violating the Liskov’s Substitution Principle (LSP)” 為題提交。建議的修法是組合：在 `Properties` 裡面持有一張表，而不是*成為*一張 `Hashtable`。缺陷以 **Won’t Fix** 關閉。公開的類型關係是對既有呼叫方的承諾；拿掉 extends 會是相容性斷裂。**JDK 9**（**2017 年**）重做了這個類別，條目不再存在繼承來的那張表裡。發行說明 **JDK-8175789** 記錄：值放在內部的並行對映中，並且 getter 不再同步，以減少死結。目前 OpenJDK 原始碼的註解仍寫著：`Properties` 不把值存在繼承來的 `Hashtable` 裡。這個類別**仍然 extends** `Hashtable`。截至 **2026 年**，這句層次上的謊話還留在公開 API 裡。

案例展示的是原則作為鎖定，而不是風格偏好。一旦你發布「這個是那種」，你可以改內臟，可以寫警告。你不能悄悄收回替換。邊界說明：若使用 `setProperty` 並保持字串，`Properties` 仍能工作。失敗的是*類型*聲稱，不是每一個呼叫點。給新設計的教訓，是 2016 年那條未能採納的繞路：把表組合進來，不要去繼承它。

## 邊界與失效場景

當父類型的契約已經是一隻雜物袋時，里氏替換原則會失效。Java 的 `List` 「可選操作」——`add` 在定長或不可改清單上可以拋例外——是函式庫層級的妥協。於是每個實作者都成了部分子類型。呼叫方必須讀腳註。這不是再加腳註的許可證；它警告父類型畫得太寬。在再堆一個會拋例外的表親之前，先把父類型削瘦，或把介面劈開。

它也會在給已發布的謊言做修補時失效。`Properties` 就是展品：正確設計是組合，而正確設計已經不可得。把一棵二十年的層次叫做「學習機會」，並不等於可以照抄。抄 2016 年的繞路，不要抄 1996 年的 extends。

常見誤用是一個上帝類型，一半方法寫成 `NotSupported`。不會飛的鳥、不能提領的帳戶、不能關機的裝置，都不是「特化的子類型」，而是過大父類型下面更挑剔的孩子。另一種誤用是行為已經分叉，卻為了重用程式碼去繼承。那種重用應放進共享助手，這是沒有假「是一種」的 [DRY](/zh-hant/principles/dry-principle)。子類型在父類型承諾安全的呼叫上拋出意外例外，也和[快速失敗](/zh-hant/principles/fail-fast)打架：程式是失敗了，卻失敗在別人以為安全的那一次呼叫裡。

## 常見誤區

這個中文名容易撞上「能編譯就能替換」「拋不支援就行」，以及把 SOLID 當成給類別做性格測驗。

<AccordionGroup>
  <Accordion title="子類能對著父類編譯，就滿足里氏替換原則">
    編譯器檢查名字、類型和可見性。它不檢查 `setWidth` 是否仍是呼叫方以為的意思，也不檢查屬性表是否仍只裝字串。行為子類型是關於已經能工作的程式的聲稱。綠燈建置是必要的，遠遠不夠。
  </Accordion>

  <Accordion title="子類型可以對自己不喜歡的方法拋「不支援」">
    若父類型公開說過這個方法屬於日常使用，拋例外就是收回承諾。標準函式庫裡的可選操作是已知汙點，不是該抄的模式。要麼這個方法不該在父類型上，要麼每個子類型都必須兌現。寫著「別在這裡呼叫」的註解是契約上的洞，不是設計。
  </Accordion>

  <Accordion title="里氏替換原則只屬於物件導向的類別">
    利斯科夫 1987 年的演講用物件，因為那是那場會議。同樣的形狀出現在角色、API、文件和代理人身分上。若槽位寫著「任何財務」「任何附件」「任何支付方式」，頂上的人就必須做這個槽位的工作。一個「相容」卻弄壞舊讀取方的 JSON 欄位，是同一種失敗，只是沒有 class 關鍵字。
  </Accordion>
</AccordionGroup>

## 相關概念

這些頁面挨著同一個問題：怎樣讓一種新東西填進舊槽位，又不讓已經依賴這個槽位的人吃一驚。

<CardGroup cols={3}>
  <Card title="開放封閉原則" href="/zh-hant/principles/open-closed">新行為作為新零件到來。這條原則檢驗新零件是否真能坐進舊槽位。</Card>
  <Card title="單一職責原則" href="/zh-hant/principles/single-responsibility">每個模組一個變更理由。假子類型常常出現在一個父類型扛了兩份工作的時候。</Card>
  <Card title="關注點分離" href="/zh-hant/principles/separation-of-concerns">把不同決策分開。更瘦的父類型往往是更乾淨的切口，而不是更深的樹。</Card>
  <Card title="最小驚訝原則" href="/zh-hant/principles/least-astonishment">能編譯、行為卻古怪的替換，天生就是一次驚訝。</Card>
  <Card title="快速失敗" href="/zh-hant/principles/fail-fast">在父類型本該安全的地方拋例外的子類型，失敗得太晚，而且失敗在別人的呼叫裡。</Card>
  <Card title="迪米特法則" href="/zh-hant/principles/law-of-demeter">只跟直接鄰居說話。去查看表親內部的呼叫方，已經繞開了父類型的契約。</Card>
</CardGroup>

## 一句話總結

<Tip>
  若新零件需要一張警告、寫著呼叫方不得把它當成父類型，它就不是子類型——把父類型削瘦，或把多出來的行為組合在旁邊。
</Tip>
