--- title: '抽象 - 介面 vs 繼承' disqus: kyleAlien --- 抽象 - 介面 vs 繼承 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile_cat: 這裡不說明 `interface`、`abstract class` 的基礎用法、功能,找要分析比較介面與抽象 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入比較介面與抽象類:從多個角度剖析**](https://devtechascendancy.com/comparing-interfaces-abstract-classes/) ::: [TOC] ## 介面 vs 抽象:從不同程面比較 繼承類別(`abstract class`)、介面(`interface`)都位於物件的繼承樹上層,都無法被實體化,但兩者有哪裡不一樣嘛?這就是這個小節要探討的,我們以不同的角度來看看者兩種抽象化的差異點… ### 比較角度:隱定性、可變性 * **介面由如合約**:它對外公佈給使用方,這會導致它有一個特性,**==介面必須穩定==,不可任意更改(不管是增加、刪除)合約**,否則會影響到所有實做的子類 > 這裡我們不考慮 interface 的 `default` 方法 ```java= interface FileReader { // 新增方法 boolean isEmpty(); int read(byte[] buffer); } // 子類必須實做 isEmpty 否則會導致編譯不過 class DefaultFileReader implements FileReader { @Override public int read(byte[] buffer) { // TODO: } } ``` :::info * [**介面隔離原則**](https://devtechascendancy.com/object-oriented-design-principles_2/#%E4%BB%8B%E9%9D%A2%E9%9A%94%E9%9B%A2%E5%8E%9F%E5%89%87_Interface_Segregation)(想了解更多請點進連結) 所以在設計介面時必須要嚴加小心思考,進而產生了另一種設計界面的概念,**介面隔離(介面最小化)** 來讓介面更有可控性,降低使用代價 ::: * **抽象具有可調性**:而相對比起來,抽象在更改時不一定會影響到子類(新增共有方法,也就是預設實做),相較介面,它更具有可調整性 ```java= // 子類必須實做 isEmpty 否則會導致編譯不過 abstract class CommonFileReader { // 新增共有實做方法,子類不一定需要實現 protect boolean isValid(byte[] buffer) { // TODO: } } class UTF8FileReader extends CommonFileReader { } class UnicodeFilreReader extends CommonFileReader { } ``` ### 比較角度:複雜度、多實現 * **介面可以多實現、降低複雜度**: Java 的特性之一就是類只能單一繼承,而 **介面(`interface`)的實做可以有多個**; 而以 JVM 的角度來看介面的方法,**介面方法由有實做類來完成該方法,不會有複寫的狀況,降低了 JVM 動態連結的複雜度**(對於效能的提昇) ```java= interface IShowMessage { void showMessage(String str); } interface ISettingMessage { void setMessage(String str); } // 可實做多個界面 class DefaultShowMessage implements IShowMessage, ISettingMessage { @Override public void showMessage(String str) { // TODO } @Override public void setMessage(String str) { // TODO } } ``` :::warning * **Java 為何不設計多繼承** 像是 `C++`、`Dart`... 等等語言的多繼承,在處理類的聯繫規則時會變得複雜,而 **Java 語言的設計理念則是降低語言的複雜度** > 所以也有人說 `Java` 其實是 `C++--`,也就時降低 C++ 的複雜度,只提高它的普片通用性 ::: ### 比較角度:可維護性、重構性 * **可維護性、重構性**:從已經有的(已設計好的)類別中,要再進行抽象並不容易,因為你只要抽象化所有的方法,其繼承的子類都必須實做新的方法 > 這裡並不考慮在 `abstract class` 中的實做方法,目前只考慮「純抽象」 ```java= // 已經設計好的類 abstract class Firmware { // 要拓展(重構)的新方法,子類沒有實做是不行的 abstract boolean update(); abstract String version(); } class HMI extends Firmware { @Override String version() { return "1.1.1"; } } class LED extends Firmware { @Override String version() { return "0.3.8"; } } ``` * 那要換做界面(`interface`)會比較好嗎? 這又會牽扯到前面說明的概念,基於 **類可實現多介面** 所以我們可以透過設計界面來達成重構時的新抽象,這可以達到兩個好處 1. **不必對已有的抽象類新增抽象** ```java= // 新功能使用介面拓展 interface IUpdate { boolean update(); } // 原有的類保持 abstract class Firmware { abstract String version(); } ``` 2. **只需要對需要的類進行界面的實做(繼承無法達到,因為 Java 不許多繼承),不用依賴多餘界面** ```java= // 針對需要的類進行實做即可 class HMI extends Firmware implements IUpdate { @Override String version() { return "1.1.1"; } @Override public boolean update() { // TODO return false; } } // 不需要的類不實做介面 class LED extends Firmware { @Override String version() { return "0.3.8"; } } ``` :::success * 要如何為 **介面取名**? 1. 首先我們要知道「取名」的重要性,它有關到之後程式的可讀性、可維護性 2. 在語法上,**介面不會強迫它的實做必須與類名的實做在語意上有強烈關係**,介面名只須跟須拓展的類有關連性即可 也就是說我們在為界面取名時,要取與該界面最相關的名稱,而不是與類最相關的名稱! ::: ### 比較角度:耦合性 * **如果要選擇一個作為 ++方法宣告,選擇介面更為合適++**,原因如下 1. **介面(`interface`)更加單純,不會參雜多餘的邏輯實做** 抽象如果要過展就必須不斷的繼承下去,有很多時候會「被迫」拓展出不須的功能;**相對起來抽象更屬於實做 & 介面之間的產物** 我們看看以下範例:這個範例中闡述的是抽象類繼承的「**強迫性**」 ```java= abstract class Fan { abstract void turnOnFan(); abstract void freshAir(); } class NormalFan extends Fan { @Override void turnOnFan() { System.out.println("Start Fan~"); } // 強迫繼承了不需要的功能抽象,只能使用拋出 @Override void freshAir() { throw new UnsupportedOperationException(); } } class LGAirPurifier extends Fan { @Override void turnOnFan() { System.out.println("Start LG Fan~"); } @Override void freshAir() { System.out.println("Start LG fresh air~"); } } ``` 2. **界面語法上的支援限制更少**: **一個類可以實做多個介面,也就是說它可以完成多個功能,並不像抽象類會受到限制** ```java= interface IFan { void turnOnFan(); } interface IFreshAir { void freshAir(); } class NormalFan2 implements IFan { @Override public void turnOnFan() { System.out.println("Start Fan~"); } } class LGAirPurifier2 implements IFan, IFreshAir { @Override public void turnOnFan() { System.out.println("Start LG Fan~"); } @Override public void freshAir() { System.out.println("Start LG fresh air~"); } } ``` ## 介面、抽象:選擇考量 在上面我們用不同的角度來比較界面跟抽象,似乎界面看起來更好一點? 不!**它們各有各的特點,應該各司其職**,在正確的時機使用它們才是重點,那接著我們就來看看,要如何選擇考量… ### 介面:對外服務 * **界面(`interface`)對外提供服務**: **抽象有分「層」,最高層的抽象就是界面,++介面應該作為與外界溝通的視窗++**,並且它傳達出的是一種 **契約規範**(依照規則就可以達到像對應的處理) 不論大小系統之間,都應該使用界面進行互動,**這樣可以有效的提高鬆耦合** :::warning 注意:界面需要謹慎思考,因為 **開出的介面不可任意修改**,也請記得善用 [**介面隔離原則**](https://devtechascendancy.com/object-oriented-design-principles_2/#%E4%BB%8B%E9%9D%A2%E9%9A%94%E9%9B%A2%E5%8E%9F%E5%89%87_Interface_Segregation) ::: * 我們看看以下範例,該如何使用界面對外提供功能,**並降低類與類之間的耦合度**: 1. **`PowerManager` 子系統**: 對外提供 `IPowerManager` 介面讓其他子系統使用,也就是對外提供給使用者時,是對外提供 `IPowerManager` 介面,不讓使用者關注內部實做,進而達到 **鬆耦合** 的特色(讓使用者依賴在界面合約上,而不是實做之上!) ```java= /* * public 層級 */ public interface IPowerManager { void lowPower(); void typicalPower(); } /** * package 層級 */ class DefaultPowerManager implements IPowerManager { @Override public void lowPower() { System.out.println("Low power mode."); } @Override public void typicalPower() { System.out.println("Typical power mode."); } } ``` 2. **`WindowManager` 子系統** * `IWindow` 界面用來顯示、關閉視窗:對外部暴露界面(`IWindow`),而實做 `DefaultManager` 透過 package 層級來對外隱藏! ```java= // 對內部提供功能,也可以用契約,但是不會像外部這麼嚴厲 /* * public 層級 */ public interface IWindow { void showWindow(); void closeWindow(); } /** * package 層級 */ class DefaultManager implements IWindow { @Override public void showWindow() { System.out.println("Show window."); } @Override public void closeWindow() { System.out.println("Close window."); } } ``` * `IWindowManager` 界面內部依賴抽象 `IWindow` 界面來達到隱藏實做細節類的目的,同時對外也只提供 `IWindowManager` 界面,對實做類同樣使用 package 層級來隱藏 > 並且我們可以看到,在這樣的設計之下也隱藏了 `DefaultWindowManager` 依賴 `IPowerManager` 界面的細節! ```java= // 對外的保證契約 /* * public 層級 */ public interface IWindowManager { void addWindow(IWindow window); void removeWindow(IWindow window); } /** * package 層級 */ class DefaultWindowManager implements IWindowManager { private final List<IWindow> windowList = new ArrayList<>(); private final IPowerManager powerManager; public DefaultWindowManager(IPowerManager powerManager) { this.powerManager = powerManager; } @Override public void addWindow(IWindow window) { if(!windowList.contains(window)) { windowList.add(window); } if (windowList.size() > 100) { powerManager.lowPower(); } else { powerManager.typicalPower(); } window.showWindow(); } @Override public void removeWindow(IWindow window) { windowList.remove(window); window.closeWindow(); } } ``` * **對外提供給使用者**:可以看到我們可以對外提供給使用者純界面的類型,而不需要對外暴露實做! ```java= public SystemWindow { public static final SystemWindow instance = new SystemWindow(); private SystemWindow() { } // 對外提供界面 private final IPowerManager powerManager = new DefaultPowerManager(); // 對外提供界面 public final IWindowManager windowManager = new DefaultWindowManager(IPowerManager); } ``` ### 抽象類:對內擴充 * **抽象類(`abstract class`)繼承對內擴充**: 抽象屬於介面、實做之間的 **半成品**,抽象除去各個子類的特點,它會盡可能的完成共通邏輯的實做;盡量使用在系統內部使用 **當一個方法是為了特定的擴充(該擴充是給內部系統呼叫),而不是為了對外服務(外部呼叫該擴充則無用),那該類就可以使用抽象!** * **對內擴充的範例**: 1. **主系統如下**:透過介面來說明對外的承諾,這次在其中我們添加了一個 `VerifyRule` 類,用來給使用者擴充驗證方案 ```java= interface IVerifyManager { boolean verify(String str, VerifyRule rule); } class DefaultVerifyManager implements IVerifyManager { @Override public boolean verify(String str, VerifyRule rule) { if (str == null || str.isEmpty()) { return false; } // Maybe do something... return rule.startCheck(str); } } // 待驗證項目 abstract class VerifyRule { protected VerifyRule next; // 可拓展的方法 protected abstract boolean check(String str); // 對內統一方法 final boolean startCheck(String str) { boolean currentCheck = this.check(str); if (!currentCheck) { return false; } if (this.next != null) { return this.next.startCheck(str); } return true; } } ``` :::danger * 這裡記得,非對外提供擴充的方法要用 `final` 描述,意在於不讓外部類覆寫,保持類設計的安全性! ::: 2. **使用者拓展、擴充**:這個擴充的要點是,**它會讓內部系統經過邏輯驗證後,再去呼叫**,使用者不會知道該擴充被呼叫的時機點 ```java= class JWTRule extends VerifyRule { public JWTRule() { this.next = new CRCRule(); } @Override protected boolean check(String str) { return str.contains("JWT"); } } class CRCRule extends VerifyRule { @Override protected boolean check(String str) { return str.contains("CRC"); } } ``` ## 更多的 Java 語言相關文章 ### Java 語言深入 * 在這個系列中,我們深入探討了 Java 語言的各個方面,從基礎類型到異常處理,從運算子到物件創建與引用細節。點擊連結了解更多! :::info * [**深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構**](https://devtechascendancy.com/java-compilation_jdk_javadoc_jar_guide/) * [**深入理解 Java 異常處理:從基礎概念到最佳實踐指南**](https://devtechascendancy.com/java-jvm-exception-handling-guide/) * [**深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較**](https://devtechascendancy.com/java-operators-modifiers-key-concepts_equals/) * [**深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用**](https://devtechascendancy.com/java-object-creation_jvm-reference-details/) ::: ### Java IO 相關文章 * 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容! :::warning * [**Java File 操作指南:基礎屬性判斷、資料夾和檔案的創建、以及簡單示範**](https://devtechascendancy.com/java-file-operations-guide/) * [**深入探索 Java 編碼知識**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入理解 Java IO 操作:徹底了解流、讀寫、序列化與技巧**](https://devtechascendancy.com/deep-in-java-io-operations_stream-io/) * [**深入理解 Java NIO:緩衝、通道與編碼 | Buffer、Channel、Charset**](https://devtechascendancy.com/deep-dive-into-java-nio_buf-channel-charset/) ::: ### 深入 Java 物件導向 * 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念! :::danger * [**深入比較介面與抽象類:從多個角度剖析**](https://devtechascendancy.com/comparing-interfaces-abstract-classes/) * [**深度探究物件導向:繼承的利與弊 | Java、Kotlin 為例 | 最佳實踐 | 內部類細節**](https://devtechascendancy.com/deep-dive-into-oop-inheritance/) * [**「類」的生命週期、ClassLoader 加載 | JVM 與 Class | Java 為例**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎` `Java 設計模式`
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up