---
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 基礎`