--- title: 'Java 物件導向 - 繼承 & 組合 & 內部類' disqus: kyleAlien --- Java 物件導向 - 繼承 & 組合 & 內部類 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile_cat: 這裡章節不說明基礎的繼承使用、特性,而是更深入的觀察並分析繼承所帶來的優點、缺點 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深度探究物件導向:繼承的利與弊 | Java、Kotlin 為例 | 最佳實踐**](https://devtechascendancy.com/deep-dive-into-oop-inheritance/) ::: [TOC] ## 物件導向:繼承的特性 繼承是一種 **提高程式碼的可用性**,並提高系統的可擴充性的有效手段,而組合也達到相同效果(之後會拿「繼承」與「組合」作比較),然而繼承是完全沒有區點的嘛?是否有副作用?… 就是接下來要探討的重點 ### 繼承的弱點 * 繼承雖然可以達成高效的覆用,而我們換到物件導向的想法,**物件導向同時也注重於封裝**,封裝細節,不讓其類它關注其細節 而繼承則弱點正在於封裝,**繼承它打破了封裝的規則,讓子類參與到開發的細節,同時自身也會影響到子類的實現!** :::warning * **這種子類、父類的關係就是「緊耦合」關係**,之所以是「緊」的原因在於它們的關係建立在程式語言的語法之上,並且連接在編譯期間! ::: * 繼承的弱點 1:**子類的實現會影響父類** ```kotlin= abstract class Shop { abstract fun isShopOpen(): Boolean fun buy() { // 子類的實現會有關細到父類的行為 if (!isShopOpen()) { throw Exception("Shop not open") } // do something } } class BookShop: Shop() { override fun isShopOpen(): Boolean { return false } } ``` * 繼承的弱點 2:**父類的改變會影響子類**;最明顯的就是父類別的方法改變、拓展,子類必須被迫實現 >  ### 繼承的缺點 * **繼承是一「強制性」擴充**:強制其子類必須繼承父類所有的方法、屬性(**這也包括了私有**),**最終可能導致子類必須實現它不需要的方法** ```kotlin= abstract class Firmware { abstract fun getHardwareVersion(): String abstract fun getFirmwareVersion(): String abstract fun update(): Boolean } class Motor: Firmware() { override fun getHardwareVersion(): String { return "1.1.0" } override fun getFirmwareVersion(): String { return "2.0.13" } override fun update(): Boolean { println("Update Motor") return true } } class LED: Firmware() { override fun getHardwareVersion(): String { return "1.0.0" } override fun getFirmwareVersion(): String { return "1.1.1" } // 非必要的方法! override fun update(): Boolean { throw UnsupportedOperationException() } } ``` * **如果維護者不清晰了解父類,那在拓展時會增加多型的不穩定**: 由於子類繼承後,可以覆寫(`Override`)父類的方法,這是多型的特徵,但假 **如子類覆寫時曲解了方法的意義,可能導致不安全性的發生** ```kotlin= abstract class Shop { // 原本的意義是只准反為 Boolean 判斷 abstract fun isShopOpen(): Boolean fun buy() { if (!isShopOpen()) { throw Exception("Shop not open") } // do something } } class BookShop: Shop() { // 而子類卻曲解其意義,改為拋出! override fun isShopOpen(): Boolean { throw Exception("Book shop not open.") } } ``` ## 繼承的原則 最基礎的就是開發文檔要寫清楚,並且說明實做該方法會牽連影響到哪些方法,最終可能導致哪些結果;當然除了文檔之外,我們也可以透過一些規則來實做繼承 ### 層級限制 * 繼承的層級最好做適當的管控,否則容易造成類別拓展的負擔,並也降低程式的理解性、可讀性… 建議:**繼承層次應該保持在不超過 3 層** 的架構(不考慮到 `Object` 類) ```kotlin= // kotlin // 第一層 abstract class Food { } // 第二層 abstract class Fruit: Food { } // 第三層(最多) class Apple: Fruit { } ``` ### 界面 & 繼承 的宣告 * **如果有界面(`interface`)的實做 & 繼承(`abstract class`)**:我們應該 **盡可能的使用界面類作為宣告**,而非使用抽象類; 這是因為抽象類所代表的責任、含義會比介面更大(因為抽象類是介於介面、實體類的中間,它往往也承擔了部分的細節做法);概念程式如下… * 介面 & 繼承的相關程式 ```kotlin= // kotlin 範例 interface IEatAction { fun eat(); } abstract class Fruit: Food, IEatAction { override fun eat() { println("Eat fruit"); } } class Apple: Fruit { } ``` * 我們這裡建議使用界面作為宣告,來降低對於實作細節的依賴! ```kotlin= fun main() { // 應該使用「界面」作為宣告 (使用這個更好!) val apple: IEatAction = Apple(); // 使用 抽象 作為宣告,會與實體的細節產生更多切合面的接觸點 val apple2: Fruit = Apple(); } ``` ### 有規劃的設計父類 * **盡量在父類完成共有的方法**,為子類提供一系列預設的實做(這也提高了程式碼的重用性);而實際上通常並非這麽順利,通常有需要子類實現的方法,如下︰ * **父類完成方法**:某些方法適用於所有的子類(**同常是邏輯**),那就可以在父類完成該方法 ```kotlin= abstract class LoadFile { abstract fun load(): File fun getContent(): String { val file = load() val reader = file.reader() return reader.use { it.readText().run { this.ifEmpty { "The file is empty" } } } } } ``` * **子類完成方法**:某些方法的實做取決於各個子類別的特定屬性、實做細節 ```kotlin= class LoadDiskFile: LoadFile() { override fun load(): File { return File("file://....") } } class LoadWebFile: LoadFile() { override fun load(): File { return File(URI("www.google.tw")) } } ``` :::warning * 盡量的管控,不要讓子類去複寫父類已經完成的方法(也就是建議父類已經完成的方法使用 `final` 宣告) ::: * **基於 盡量在父類完成共有的方法 的原則**,我們在設計父類別時 **可以使用一些技巧來將方法限制在父類別中** 1. 使用 `private` 修飾共同的邏輯方法,**不讓子類別去訪問** ```kotlin= abstract class MyClass { private fun commonLogic() { // do nothing } } ``` 2. 使用 `final` 來描述共同的方法,**不讓子類別去覆寫(Override)** ```kotlin= abstract class MyClass { final fun sayHello() { // do nothing } } ``` 3. **父類別不應該在建構函數(`Consturctor`)呼叫子類別實現的方法**,否則可能造成各種狀況的崩潰 (`Crash`) 下面範例可能會造成的問題是:**JVM 在建構時會先建構基類(也就是父類別),這會導致子類別尚未建立完成就被呼叫,即可能造成 `Crush`** ```kotlin= abstract class BaseMessageStore { constructor() { getMessage() } abstract fun getMessage(): String } class MessageStore: BaseMessageStore() { override fun getMessage(): String { return "HelloWorld" } } ``` ## 繼承的考量 由上面我們可以知道繼承是一種有代價(而且不低)的行為,那我們甚麽時候該用繼承呢? ### 濫用繼承 * 先來看一個繼承的濫用案例 ```kotlin= abstract class Clothe { abstract fun color(): String } class GreenClothe: Clothe() { override fun color(): String { return "Green" } } class RedClothe: Clothe() { override fun color(): String { return "Red" } } class WhiteClothe: Clothe() { override fun color(): String { return "White" } } ``` 這個設計 **沒有詳加考慮到業務邏輯關係**,僅因為讓子類別來定義簡易屬性,而使用繼承; 它除了類別名稱不同之外,屬性、行為接相同,這等同於 **濫用繼承,這猶如殺雞用牛刀,完全沒有必要** :::warning * **繼承必須要父類、子類參與邏輯關係**,其中也許可有不同的屬性、私有行為 ::: ## 繼承與組合的比較 在軟體的開發階段,會經過幾個時期 * **早期階段**:早期是 **創建** 階段,從基礎簡單的呈現出符合業務邏輯的,在經歷整體類別到區域類別的分解(抽出重複部份),從 **子類到到父類別的抽象過程** * **後期階段**:後期階段基本上是進行 **維護**,維護已經創建的抽象父類別,如果這時要擴充,就需要進行區域類別的繼承、組合 ### 早期:組合的分解 & 繼承的抽象 :::info * **組合關係的分解過程,對應繼承關係的抽象過程**: 接著我們用同一個案例但不同手段(繼承、組合)來完成目的;當我們收到業務之後,需要從 0 建構出一個類,這個過程是 **實體到抽象** ::: > 假設我們有個新需求,要創建兩種類型的 Licence 解析,分別是 JWT、RSA 的 License 驗證 * **繼承關係的抽象過程(繼承的抽象)**:會經過兩個過程實做、抽象化 1. **從實做分析**:寫出各個實做的細節 ```kotlin= class RSALicense { fun parserLicenseContent(content: String): Boolean { if (content.isNotEmpty()) { return false } return content.startsWith("RSA") } } class JWTLicense { fun parserLicenseContent(content: String): Boolean { if (content.isNotEmpty()) { return false } return content.contains("JWT") } } ``` 2. **抽象化**:分析實做相關性並抽象化 ```kotlin= abstract class LicenseCheck { protected abstract fun parserLicenseContent(content: String): Boolean fun check(content: String): Boolean { if (content.isNotEmpty()) { return false } return parserLicenseFile(content) } } ``` * **組合關係的分解過程(組合的分解)**:將相同之處進行分解,拆分到另一類,並且重新組合; * **抽出相同方法,透過界面抽象化**:其中相同之處就在於 `parserLicenseFile` 方法 :::success **這裡透過界面(`interface`)抽象化,讓組合類具有拓展性** ::: ```kotlin= interface IParser { fun parserLicenseContent(content: String): Boolean } class RSAParserLicenseContent constructor(val content: String): IParser { override fun parserLicenseContent(content: String): Boolean { return content.startsWith("RSA") } } class JWTParserLicenseContent constructor(val content: String): IParser { override fun parserLicenseContent(content: String): Boolean { return content.contains("JWT") } } ``` * **使用組合方式將相關處理類組合並使用** ```kotlin= // 組合 IParser 抽象方法 class LicenseCheckComponent constructor(val parser: IParser) { fun check(content: String): Boolean { if (content.isNotEmpty()) { return false } return parser.parserLicenseContent(content) } } ``` ### 後期:組合的組合 & 繼承的擴充 :::info * **組合關係的「組合過程」,對應繼承關係的「擴充過程」**: 接著我們用同一個案例但不同手段(繼承、組合)來完成相同的目的;**以覆用、拓展為目的,並以維護為目的** ::: > 假設我們在一個已經成熟的專案中,有一個拓展的新需求;而已達成的類如下描述,要透過以下類進行拓展… ```kotlin= // 已達成的類如下 interface ICheck { fun check(content: String): Boolean } abstract class LicenseCheck: ICheck { protected abstract fun parserLicenseContent(content: String): Boolean override fun check(content: String): Boolean { if (content.isNotEmpty()) { return false } return parserLicenseContent(content) } } // 使用 open 描述,讓其可被繼承 open class RSALicense: LicenseCheck() { override fun parserLicenseContent(content: String): Boolean { return content.startsWith("RSA") } } // 使用 open 描述,讓其可被繼承 open class JWTLicense: LicenseCheck() { override fun parserLicenseContent(content: String): Boolean { return content.contains("JWT") } } ``` * **繼承關係的擴充過程**: * **透過繼承**,拓展出新的功能:覆用父類已完成的方法,並加上自身的業務邏輯 ```kotlin= class RSALicence128: RSALicense() { override fun parserLicenseContent(content: String): Boolean { return super.parserLicenseContent(content) // 加上自身的業務邏輯 && content.length == 128 } } class JWTLicence256: JWTLicense() { override fun parserLicenseContent(content: String): Boolean { return super.parserLicenseContent(content) // 加上自身的業務邏輯 && content.length == 256 } } ``` :::warning * 這種繼承關係,會引入與父類別的強關聯關係,也就是子類必須相當了解父類別實現的方法 ::: * **組合關係的組合過程**: * **透過組合**,拓展出新的功能:將需求透過「新建立的類」個別建立,並 **完成部份業務需求** ```kotlin= class Licence128: LicenseCheck() { override fun parserLicenseContent(content: String): Boolean { return content.length == 128 } } class Licence256: JWTLicense() { override fun parserLicenseContent(content: String): Boolean { return content.length == 256 } } ``` **組合新建立的類(須拓展的新方法)、已有的類(舊有的功能),來完整達成需求** ```kotlin= class RSALicence128_2 constructor(val l128: Licence128, val rsa: RSALicense): ICheck { override fun check(content: String): Boolean { return l128.check(content) && rsa.check(content) } } class JWTLicence256_2 constructor(val l285: Licence256, val jwt: JWTLicense): ICheck { override fun check(content: String): Boolean { return l285.check(content) && jwt.check(content) } } ``` :::success * 以拓展、組合的方式去建立新的功能,可以保證舊有的類不受到影響,並且也符合類的設計原則 [「**開閉原則 Open Close Principle**」](https://devtechascendancy.com/object-oriented-design-principles_2/#%E9%96%8B%E9%96%89%E5%8E%9F%E5%89%87_Open_Close_Principle) ::: ### 繼承 vs. 組合的結論 * 接著,我們說說繼承的「特徵」,謂何說繼承的代價是比較大的? * **系統的複查度**:多層級繼承會使的系統變得複雜,不易維護 > 組合相對比起來複雜度低,容易做插拔替換 * **靜態繼承關係**:運行時會被迫接受父類的所有特徵(包括私有方法、成員),並且 **無法改變父類**(必須一直在父類環境下運行) > 組合則不會有這種困擾,允許替換功能的環境 * 在 UML 中的關聯關係、聚合關係可以統一稱為組合,使用組合可以完成跟繼承相同的事情;其中兩個的差異比較如下表 | \ | 組合關係 | 繼承關係 | | - | - | - | | 封裝 | (v) 保有每個類的封裝性,獨立性高 | (x) 破壞封裝性,子類父類都會相互影響 | | 擴充性 | (v) 針對不同的細節實做不同的類,擴充性高 | (x) 有擴充性,不過 **相對的代價也高、複雜度變高** | | 動態性 | (v) 支援動態組合,可以透過 `setter` 替換不同的實做 | (x) 子類無法改變父類的實做,與父類是強耦合關係 | | 可變性 | (v) **基於依賴倒置關係,可以將封裝的方法抽象為界面,實做方只需要依賴所需的界面組合就可以** | (x) 子類強制繼承父類的所有方法、屬性 | | 界面的關聯性 | (x) 隔離了界面的,不需要手動獲取界面的方法 | (v) **自動擁有父類的界面** | | 建立物件的代價 | (x) 必須手動傳入組合類 | (v) **子類別建立時,同時就建立好父類** | ## Java 內部類的不同 Java 的內部類有分為多種,如果使用得當,可以優雅的規劃出類的責任、關聯範疇;我們可以做以下分類來區別一下不同的內部類 1. **實名內部類** | 分類 | 關鍵 | | - | - | | 實體內部類 | **與外部有引用關係**,必須先創建外部類才能創建內部類 | | 靜態內部類 | 關鍵字 `static`,與外部類無明顯關係 | | 區域(方法)內部類 | 這種方法內部類較少使用;它不能用 `public`、`protected`、`private` 描述 | 2. **變數(匿名)內部類** | 分類 | 關鍵 | | - | - | | 實體變數 | 在創建類後添加 `{}` 即是一個實體變數;自動有外部引用 | | 靜態變數 | 靜態特性的匿名類,沒有外部引用 | | 局部(方法)變數 | 在方法內創建匿名類 | * 內部類通常都會在以下需求中被使用 * **封裝內部(區域)所需資料** * 分開類別後,**方便直接存取、呼叫外部類成員**(包括 private 成員) ### 實名內部類 1. **實體內部類** **與外部有引用關係**,必須先創建外部類才能創建內部類 > 由於與外部類有引用關係,所以內部類創建完後,可以使用外部類的成員 ```java= // java 範例 class OuterClass { private int outerValue = -1; class InnerClass { InnerClass() { // 直接使用外部類成員 outerValue = 1000; } } public static void main(String[] args) { OuterClass oc = new OuterClass(); OuterClass.InnerClass ic = oc.new InnerClass(); } } ``` 2. **靜態內部類** 與外部「無」直接依賴慣係(不會自動持有外部類的實體引用參考),可以直接創建物件的實例 ```java= // java 範例 class OuterClass { static class StaticInnerClass { StaticInnerClass() { // 非法行為!無法通過編譯 // outerValue = -100; } } public static void main(String[] args) { OuterClass.StaticInnerClass osi = new StaticInnerClass(); } } ``` 3. **區域(方法)內部類** 僅限於方法內可創建(可見範圍只在方法內) ```java= class OuterClass { private int outerValue = -1; int myMethod() { int testVar = 100; final int testVar2 = 300; class DataClass { final int outsideVar = outerValue; final int localVar = testVar; final int localFinalVar = testVar2; int total() { return outsideVar + localVar + localFinalVar; } } DataClass dc = new DataClass(); return dc.total(); } } ``` ### 匿名內部類 :::info **最常見的通常是匿名介面類** ::: 1. **實體變數**:自動有外部引用 ```java= class Message { String message; } class MyClazz { int value = 100; Message message = new Message() { // 匿名類的建構函數 { message = "Hello anonymous class"; System.out.println("message: " + message + ", value: " + value); } }; public static void main(String[] args) { new MyClazz(); } } ``` :::success * 匿名類沒有建構函數,但我們可以在匿名類別中 `{ }` 符號中撰寫程式,這段程式可作為建構函數呼喚(由 JVM 自動呼叫該區塊) ::: >  2. **靜態變數**:沒有外部引用 ```java= class MyClazz { int value = 100; static Message staticMessage = new Message() { { message = "Hello anonymous class"; // Error! 無法存取外部成員變數 // System.out.println("message: " + message + ", value: " + value); // 必須自己創建外部物件 MyClazz mc = new MyClazz(); System.out.println("message: " + message + ", value: " + mc.value); } }; } ``` 3. **局部(方法)變數**: ```java= void showMessage() { Message message = new Message() { { message = "Hello anonymous class"; } }; System.out.println("message: " + message.message + ", value: " + value); } ``` ### 內部類:Java 編譯後的文件名 * 經過 `javac` 編譯過後,JVM 對內類別名的規則如下 | 內部類 | 規則 | 說明 | | - | - | - | | 成員內部類 | `外部名$內部名` | 由於有實體名稱,所以使用外部實體名、內部實體名 | ```java= public class ClzName { static class StaticClz { } class InnerClz { } } ``` :::info * Enum 也算實名內部類!編譯後的規則也如上 ::: >  | 內部類 | 規則 | 說明 | | - | - | - | | 區域內部類(函數內的類) | `外部名$數字內部類名` | - | ```java= public class ClzName { void demoFunction() { class MethodClz { } class HelloClz { } } } ``` >  | 內部類 | 規則 | 說明 | | - | - | - | | 匿名內部類 | `外部類$數字` | 由於是匿名類,所以後面沒有類別名稱很正常 | ```java= class MyClz { } public class ClzName { MyClz myClz = new MyClz() { }; void demoFunction() { MyClz funcMyClz = new MyClz() { }; } } ``` >  ## 更多的 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 基礎`
×
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