[Learn] Java OOP:interface、繼承與 polymorphism
/ 19 min read
Updated:Table of Contents
上一篇先整理了 Java 的 class、method、constructor 與 access modifier
這一篇繼續往 OOP 前進,目標是看懂並寫出以下內容
- 用
interface定義共同契約 - 用 abstract class 保存共用狀態與流程
- 用
extends繼承 class - 用
implements實作 interface - 用
@Override改寫繼承而來的 method - 用 polymorphism 讓同一段程式操作不同實作
為什麼需要 OOP
假設系統一開始只會寄送 Email
public class NotificationService { private final EmailNotifier notifier = new EmailNotifier("School System");
public void sendMessage(String recipient, String message) { notifier.send(recipient, message); }}這段程式可以執行,但 NotificationService 直接依賴 EmailNotifier
未來加入 SMS 或 console 輸出時,service 可能開始出現判斷
if (type.equals("email")) { emailNotifier.send(recipient, message);} else if (type.equals("sms")) { smsNotifier.send(recipient, message);}每增加一種通知方式,就要回來修改這段流程
OOP 的其中一個用途,是先找出穩定的共同契約,再讓不同 class 各自實作細節
Notifier├── AbstractNotifier│ ├── EmailNotifier│ └── SmsNotifier└── ConsoleNotifier呼叫端只需要知道 Notifier,不需要為每一種實作撰寫不同流程
建立範例專案
這次會建立以下檔案
src/└── com/ └── example/ ├── app/ │ └── Main.java └── notification/ ├── Notifier.java ├── AbstractNotifier.java ├── EmailNotifier.java ├── SmsNotifier.java └── ConsoleNotifier.java每一個 public top-level class 或 interface 都放在同名檔案中
interface 定義共同契約
先建立 Notifier.java
package com.example.notification;
public interface Notifier { String channel();
void send(String recipient, String message);}interface 描述實作者必須提供哪些行為,但不決定每一種通知要如何寄送
這裡的契約包含兩個 method
channel()回傳通知管道名稱send(...)寄送訊息
interface 不能直接使用 new Notifier() 建立 instance
// Notifier notifier = new Notifier(); // 無法編譯但 interface 可以作為 variable、parameter 或回傳值的型別
Notifier notifier;沒有 method body 的 interface method 預設就是 public abstract
所以下面兩種宣告在這裡表達相同意思
void send(String recipient, String message);
public abstract void send(String recipient, String message);實作 interface method 時不能降低可見範圍,因此具體 class 中要寫 public
現代 Java 的 interface 也能有 default、static 與 private method,所以它不只是只能放 abstract method 的容器
初學時可以先把 interface 理解成呼叫端與實作者共同遵守的契約
abstract class 保存共用狀態與流程
Email 與 SMS 都需要寄件者名稱、參數驗證與操作紀錄
這些共用內容可以放進 AbstractNotifier.java
package com.example.notification;
public abstract class AbstractNotifier implements Notifier { private final String senderName;
protected AbstractNotifier(String senderName) { if (senderName == null || senderName.isBlank()) { throw new IllegalArgumentException("senderName 不可為空"); }
this.senderName = senderName; }
@Override public final void send(String recipient, String message) { validate(recipient, message); System.out.println("[" + channel() + "] 寄件者:" + senderName); deliver(recipient, message); }
protected abstract void deliver(String recipient, String message);
private void validate(String recipient, String message) { if (recipient == null || recipient.isBlank()) { throw new IllegalArgumentException("recipient 不可為空"); }
if (message == null || message.isBlank()) { throw new IllegalArgumentException("message 不可為空"); } }}abstract class 不能直接建立 instance
// AbstractNotifier notifier = new AbstractNotifier("School"); // 無法編譯但它可以擁有一般 class 會有的內容
- field
- constructor
- 有實作內容的 concrete method
- 沒有 method body 的 abstract method
這裡的 send 是共用流程
驗證輸入 ↓輸出共同紀錄 ↓呼叫各通知管道自己的 deliverdeliver 只規定 method signature,實際寄送方式留給 subclass 決定
protected abstract void deliver(String recipient, String message);abstract class 可以不實作 interface 的全部 abstract method
因為它本身不能被建立,尚未完成的責任可以繼續交給 concrete subclass
extends 繼承 class
建立 EmailNotifier.java
package com.example.notification;
public final class EmailNotifier extends AbstractNotifier { public EmailNotifier(String senderName) { super(senderName); }
@Override public String channel() { return "Email"; }
@Override protected void deliver(String recipient, String message) { System.out.println("寄送 Email 給 " + recipient + ":" + message); }}再建立 SmsNotifier.java
package com.example.notification;
public final class SmsNotifier extends AbstractNotifier { public SmsNotifier(String senderName) { super(senderName); }
@Override public String channel() { return "SMS"; }
@Override protected void deliver(String recipient, String message) { System.out.println("寄送 SMS 給 " + recipient + ":" + message); }}class 使用 extends 繼承另一個 class
public final class EmailNotifier extends AbstractNotifier {}subclass 會繼承 superclass 可用的 member,並能加入或 override 自己需要的行為
Java class 只能直接 extends 一個 superclass
// Java 不允許 class 同時 extends 兩個 class// class EmailNotifier extends AbstractNotifier, Logger {// }constructor 不會被繼承
EmailNotifier constructor 使用 super(senderName) 呼叫 AbstractNotifier constructor,完成 superclass 部分的初始化
public EmailNotifier(String senderName) { super(senderName);}如果 constructor 要明確呼叫 super(...),它必須是 constructor body 的第一個 statement
implements 實作 interface
AbstractNotifier 使用 implements Notifier 表示它遵守 Notifier 契約
public abstract class AbstractNotifier implements Notifier {}concrete class 也可以不經過 abstract class,直接實作 interface
建立 ConsoleNotifier.java
package com.example.notification;
public final class ConsoleNotifier implements Notifier { @Override public String channel() { return "Console"; }
@Override public void send(String recipient, String message) { System.out.println("[Console] " + recipient + ":" + message); }}concrete class 必須實作 interface 要求的全部 abstract method,否則 compiler 會要求把 class 也宣告成 abstract
一個 class 雖然只能直接 extends 一個 class,但可以 implements 多個 interface
public class ReportService implements Printable, Exportable, Auditable {}同時使用時,語法順序固定是先 extends 再 implements
public class SpecialNotifier extends AbstractNotifier implements Auditable {}interface 也能使用 extends 繼承一個或多個 interface
public interface ReliableNotifier extends Notifier, Retryable {}override 改寫繼承而來的 method
Email 與 SMS 都 override 了 channel 與 deliver
@Overridepublic String channel() { return "Email";}override 是 subclass 或實作 class 為繼承而來的 instance method 提供新實作
常見規則如下
- method 名稱與 parameter list 必須相同
- 回傳型別必須相容
- 不能縮小 access level
- superclass 中的
finalmethod 不能 override privatemethod 不會被 subclass 繼承,所以不是 overridestaticmethod 屬於 class,發生的是 method hiding 而不是 override
@Override annotation 不是讓 override 發生的關鍵字,但建議固定加上
如果 method 名稱或 parameter 寫錯,compiler 會直接提醒這不是有效的 override
@Overridepublic void sends(String recipient, String message) { // compiler 會指出沒有可 override 的 method}override 不要和 overload 混在一起
| 比較 | override | overload |
|---|---|---|
| 發生位置 | 有繼承或實作關係的型別 | 同一個或有繼承關係的型別 |
| method 名稱 | 相同 | 相同 |
| parameter list | 相同 | 不同 |
| 選擇時機 | instance method 通常在 runtime 決定 | compile time 決定 |
| 目的 | 替換繼承而來的行為 | 提供不同參數版本 |
AbstractNotifier.send 同時使用 @Override 與 final
@Overridepublic final void send(String recipient, String message) { validate(recipient, message); System.out.println("[" + channel() + "] 寄件者:" + senderName); deliver(recipient, message);}它 override 了 interface 的 send 契約,又禁止 subclass 再次 override 共同流程
subclass 只能 override channel 與 deliver,因此不會跳過驗證
這種由 superclass 固定流程、subclass 補上特定步驟的方式,常稱為 Template Method
polymorphism 用同一型別操作不同物件
建立 Main.java
package com.example.app;
import com.example.notification.ConsoleNotifier;import com.example.notification.EmailNotifier;import com.example.notification.Notifier;import com.example.notification.SmsNotifier;import java.util.List;
public class Main { public static void main(String[] args) { List<Notifier> notifiers = List.of( new EmailNotifier("School System"), new SmsNotifier("School System"), new ConsoleNotifier());
for (Notifier notifier : notifiers) { notifier.send("Amy", "成績已更新"); } }}編譯並執行全部檔案
javac -d out \ src/com/example/notification/*.java \ src/com/example/app/Main.java
java -cp out com.example.app.Main執行結果如下
[Email] 寄件者:School System寄送 Email 給 Amy:成績已更新[SMS] 寄件者:School System寄送 SMS 給 Amy:成績已更新[Console] Amy:成績已更新List<Notifier> 裡的每個元素都使用相同 reference type
實際物件卻可以是 EmailNotifier、SmsNotifier 或 ConsoleNotifier
Notifier email = new EmailNotifier("School System");Notifier sms = new SmsNotifier("School System");左邊的 Notifier 決定 compile time 可以呼叫哪些 member
右邊實際建立的 object type 則決定 runtime 執行哪一個 overridden instance method
Notifier reference ↓ send(...)實際物件是 EmailNotifier ↓執行 AbstractNotifier.send(...) ↓ channel() 與 deliver(...)動態分派到 EmailNotifier 的實作這就是 polymorphism 的核心
同一個 method call 可以因為實際物件不同而表現出不同行為
加入新的 PushNotifier 時,只要它 implements Notifier,原本的迴圈不需要增加 if 或 switch
reference type 與 object type
下面這行同時包含兩種型別
Notifier notifier = new EmailNotifier("School System");- reference type 是
Notifier - object type 是
EmailNotifier
因為 reference type 是 Notifier,只能直接呼叫 Notifier 宣告的 member
notifier.channel();notifier.send("Amy", "成績已更新");即使 EmailNotifier 還有自己專屬的 public method,也不能透過這個 reference 直接呼叫
這項限制讓使用者只依賴共同契約,而不是某個實作的細節
把 subclass object 指派給 superclass 或 interface reference 稱為 upcasting,通常不需要明確轉型
Notifier notifier = new EmailNotifier("School System");若確定需要專屬 method,可以直接使用具體型別:
EmailNotifier notifier = new EmailNotifier();notifier.verifyEmailAddress();反方向的 downcasting 則要非常小心
if (notifier instanceof EmailNotifier emailNotifier) { // 確認型別後才使用 EmailNotifier 專屬行為}如果程式經常需要 downcasting,可能代表 interface 缺少真正需要的行為,或抽象層次選得不合適
interface 與 abstract class 如何選
兩者都能包含 abstract method,但用途不完全相同
| 比較 | interface | abstract class |
|---|---|---|
| 主要用途 | 定義能力或契約 | 共用狀態、建構流程與部分實作 |
| 建立 instance | 不可以 | 不可以 |
| instance field | 不可以 | 可以 |
| constructor | 不可以 | 可以 |
| 一般實作 | 可用 default、static、private method | 可以有一般 instance method |
| 多重關係 | class 可實作多個 interface | class 只能直接繼承一個 superclass |
適合使用 interface 的情況
- 多種沒有共同 superclass 的 class 需要提供相同行為
- 呼叫端只需要知道能力,不需要知道實作細節
- 希望保留未來替換實作的空間
適合使用 abstract class 的情況
- 多個 subclass 真的具有同一種概念上的父子關係
- 需要共用 field 或 constructor
- 需要固定部分演算法,再讓 subclass 完成其中步驟
兩者也可以像這篇的範例一起使用
呼叫端依賴 Notifier interface ↓AbstractNotifier 提供可重用的共同流程 ↓EmailNotifier 與 SmsNotifier 實作差異interface 是對外契約,abstract class 是實作者可以選擇使用的共用基底
常見錯誤
忘記在實作 method 加上 public
interface 的 abstract method 預設是 public
下面的實作縮小了可見範圍,因此無法編譯
class ConsoleNotifier implements Notifier { @Override public String channel() { return "Console"; }
@Override // 錯誤,這裡是 package-private void send(String recipient, String message) { }}把 override 寫成 overload
多寫一個 parameter 後,這是新的 overload,不會取代原本 method
public void send(String recipient, String message, boolean urgent) {}固定加上 @Override 可以更早發現這類錯誤
為了共用幾行程式就使用繼承
繼承表達的是 is-a 關係
EmailNotifier is a Notifier 合理,因為任何需要 Notifier 的地方都能使用它
如果 subclass 無法遵守 superclass 的行為契約,通常應該改用 composition,而不是勉強 extends
合理的 is-a 關係
void notifyUser(Notifier notifier) { notifier.send("Amy", "成績已更新");}以下物件都能放進去:
notifyUser(new EmailNotifier("School"));notifyUser(new SmsNotifier("School"));因為:
EmailNotifier 是一種 NotifierSmsNotifier 是一種 Notifier而且它們都能遵守 Notifier 的契約:呼叫 send() 就會送出通知
不合理的繼承
假設 ReportService 想重用 Email 寄送程式碼,於是這樣寫:
class ReportService extends EmailNotifier {}語法上可能做得到,但語意變成:
ReportService 是一種 EmailNotifier這通常不合理
報表服務不是 Email 通知器,它只是「需要使用」通知器
而且因為繼承具有可替代性,Java 會允許:
EmailNotifier notifier = new ReportService();如果 ReportService 無法正確表現得像 EmailNotifier,這個繼承關係就有問題
改用 composition
Composition 表達的是 has-a/使用某個物件,也就是把需要的物件放進 field:
class ReportService { private final Notifier notifier;
public ReportService(Notifier notifier) { this.notifier = notifier; }
public void sendReport(String recipient) { // 產生報表…… notifier.send(recipient, "報表已完成"); }}使用時可以自由選擇通知方式:
ReportService emailReportService = new ReportService(new EmailNotifier("School"));
ReportService smsReportService = new ReportService(new SmsNotifier("School"));兩者差別可以記成:
繼承(extends / implements):is-aEmailNotifier is a Notifier
組合(field):has-a / uses-aReportService has a Notifier所以判斷方式是:如果子類別不能在所有需要父型別的地方合理替代父型別,就不應該只為了重用幾行程式而繼承;應考慮 composition,或把共用程式抽成獨立 helper
在 constructor 呼叫可被 override 的 method
Java 會依照實際物件型別動態分派 instance method,即使 superclass constructor 還在執行也一樣
此時 subclass field 可能尚未完成初始化,容易得到預期外的預設值
因此 constructor 應避免呼叫可被 subclass override 的 method
解釋
建立 subclass 物件時,superclass constructor 會先執行,subclass 的 field 才會初始化;但 method 呼叫已經會根據實際物件型別進行 override 分派
例如:
abstract class AbstractNotifier { protected AbstractNotifier() { System.out.println(channel()); }
protected abstract String channel();}
class EmailNotifier extends AbstractNotifier { private String channelName = "Email";
@Override protected String channel() { return channelName; }}執行:
new EmailNotifier();順序大致是:
1. 配置 EmailNotifier 物件2. 所有 field 先取得預設值 channelName = null3. 執行 AbstractNotifier constructor4. constructor 呼叫 channel()5. 因為實際物件是 EmailNotifier, 所以執行 EmailNotifier.channel()6. 但 channelName 還沒初始化,得到 null7. superclass constructor 結束後, channelName 才被設為 "Email"因此輸出不是 “Email”,而是:
null如果 override method 使用了該欄位:
@Overrideprotected String channel() { return channelName.toUpperCase();}甚至會在 superclass constructor 中拋出 NullPointerException
比較安全的方式是直接把 constructor 所需資料傳給 superclass:
abstract class AbstractNotifier { private final String channelName;
protected AbstractNotifier(String channelName) { this.channelName = channelName; System.out.println(channelName); }}
class EmailNotifier extends AbstractNotifier { EmailNotifier() { super("Email"); }}簡單原則是:
- constructor 主要負責初始化欄位
- 避免直接或間接呼叫可被 override 的 method
- constructor 內呼叫 private 或 final method 通常較安全,因為 subclass 不能 override
- 如果需要 subclass 的資料,透過 super(…) 的參數傳入,或等整個物件建構完成後再呼叫相關 method
完成後,應該能回答以下問題
- interface 與 abstract class 各自適合放什麼
extends與implements有什麼差別@Override能協助 compiler 檢查什麼- 為什麼
List<Notifier>可以同時保存三種 class notifier.send(...)最後執行哪個 method 是如何決定的- 新增一種通知方式時,為什麼原本迴圈不需要改動
結論
- interface 定義共同契約,讓呼叫端不依賴具體實作
- abstract class 可以保存共用狀態、constructor 與部分流程
- class 使用
extends繼承一個 superclass,使用implements實作一個或多個 interface - override 讓 subclass 提供不同的 instance method 行為,建議固定使用
@Override - polymorphism 讓同一種 reference type 在 runtime 執行不同 object type 的實作
- reference type 決定可以呼叫什麼,object type 決定 overridden instance method 實際執行什麼
- 繼承要表達可替代的 is-a 關係,只需要重用程式碼時應先考慮 composition
真正熟悉 OOP 的關鍵,不是背下每個關鍵字,而是能把穩定契約、共用流程與實作差異放在合適的位置
參考資料
- https://docs.oracle.com/javase/specs/jls/se26/html/jls-8.html
- https://docs.oracle.com/javase/specs/jls/se26/html/jls-8.html#jls-8.4.8.1
- https://docs.oracle.com/javase/specs/jls/se26/html/jls-9.html
- https://docs.oracle.com/javase/specs/jls/se26/html/jls-15.html#jls-15.12.4.4
- https://docs.oracle.com/javase/specs/jls/se26/html/jls-12.html#jls-12.5