skip to content
BlogZzz

[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 是共用流程

驗證輸入
↓
輸出共同紀錄
↓
呼叫各通知管道自己的 deliver

deliver 只規定 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

@Override
public String channel() {
return "Email";
}

override 是 subclass 或實作 class 為繼承而來的 instance method 提供新實作

常見規則如下

  • method 名稱與 parameter list 必須相同
  • 回傳型別必須相容
  • 不能縮小 access level
  • superclass 中的 final method 不能 override
  • private method 不會被 subclass 繼承,所以不是 override
  • static method 屬於 class,發生的是 method hiding 而不是 override

@Override annotation 不是讓 override 發生的關鍵字,但建議固定加上

如果 method 名稱或 parameter 寫錯,compiler 會直接提醒這不是有效的 override

@Override
public void sends(String recipient, String message) {
// compiler 會指出沒有可 override 的 method
}

override 不要和 overload 混在一起

比較overrideoverload
發生位置有繼承或實作關係的型別同一個或有繼承關係的型別
method 名稱相同相同
parameter list相同不同
選擇時機instance method 通常在 runtime 決定compile time 決定
目的替換繼承而來的行為提供不同參數版本

AbstractNotifier.send 同時使用 @Override 與 final

@Override
public 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", "成績已更新");
}
}
}

編譯並執行全部檔案

Terminal window
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,但用途不完全相同

比較interfaceabstract class
主要用途定義能力或契約共用狀態、建構流程與部分實作
建立 instance不可以不可以
instance field不可以可以
constructor不可以可以
一般實作可用 default、static、private method可以有一般 instance method
多重關係class 可實作多個 interfaceclass 只能直接繼承一個 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 是一種 Notifier
SmsNotifier 是一種 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-a
EmailNotifier is a Notifier
組合(field):has-a / uses-a
ReportService 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 = null
3. 執行 AbstractNotifier constructor
4. constructor 呼叫 channel()
5. 因為實際物件是 EmailNotifier,
所以執行 EmailNotifier.channel()
6. 但 channelName 還沒初始化,得到 null
7. superclass constructor 結束後,
channelName 才被設為 "Email"

因此輸出不是 “Email”,而是:

null

如果 override method 使用了該欄位:

@Override
protected 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 的關鍵,不是背下每個關鍵字,而是能把穩定契約、共用流程與實作差異放在合適的位置

參考資料