在 Kotlin 中使用類別和物件

1. 事前準備

本程式碼研究室將教導您如何在 Kotlin 中使用類別和物件。

類別提供了用來建構物件的藍圖。物件為類別的例項,由該物件的特定資料組成。「物件」和「類別例項」同義,可以交替使用。

請想像您要建造一幢房子,以此做為比喻。類別類似建築師的設計規畫,也稱為藍圖。藍圖不是真正的房子,而是說明房子的建造方式。房子是根據藍圖建成的實物,即物件。

每個類別都有各自的設計和用途,就如同房屋藍圖規劃了多個房間,而每個房間都有各自的設計和用途。如要瞭解如何設計類別,您必須熟悉「物件導向程式設計」(OOP),透過這個架構瞭解如何在物件中納入資料、邏輯和行為。

OOP 可協助您將複雜的實際問題簡化成小型物件。OOP 涵蓋以下四種基本概念,本程式碼研究室將逐一說明:

  • 封裝:整合相關的屬性,以及在類別中處理這些屬性的方法。以行動電話為例,其包含相機、螢幕、記憶卡和其他多種硬體與軟體元件,而您無需擔心這些元件的內部接線方式。
  • 抽象:封裝的擴充功能,其用意是盡可能隱藏內部的實作邏輯。舉例來說,假設您要使用手機拍照,只要開啟相機應用程式,將手機鏡頭對準您要拍攝的場景,並點選拍照按鈕即可。您不需要瞭解相機應用程式的建構方式,或手機的相機硬體實際運作方式。簡單來說,相機應用程式的內部機制和行動裝置相機拍照的方式採用了抽象化設計,讓您專心執行重要的工作。
  • 繼承:可讓您設定父項與子項關係,藉此根據其他類別的特性和行為來建立類別。舉例來說,不同的製造商生產各種搭載 Android 作業系統的行動裝置,但每種裝置的使用者介面都不相同。換句話說,製造商會繼承 Android 作業系統的功能,並以此為基礎打造其他自訂功能。
  • 多態性:英文為 Polymorphism,結合希臘字根「poly-」(意思是許多) 和「-morphism」(意思是型態)。多態性是指可透過單一常見方式使用不同的物件。舉例來說,當您為藍牙喇叭與手機建立連線時,手機只需要知道目前有可透過藍牙播放音訊的裝置。雖然可供選擇的藍牙喇叭有許多種,但手機不必知道這些喇叭個別的使用方式。

最後,您會學到屬性委派項目,其中可重複使用的程式碼有助您以簡潔的語法管理屬性值。在本程式碼研究室中,您將建立用於智慧型住宅應用程式的類別結構,透過這個過程瞭解這些概念。

必要條件

  • 瞭解如何在 Kotlin Playground 中開啟、編輯及執行程式碼。
  • Kotlin 程式設計的基本知識,包括變數、函式以及 println()main() 函式。

課程內容

  • OOP 總覽。
  • 類別的相關說明。
  • 如何使用建構函式、函式和屬性定義類別。
  • 如何建立物件例項。
  • 繼承的相關說明。
  • IS-A 與 HAS-A 關聯性的差異。
  • 如何覆寫屬性和函式。
  • 瀏覽權限修飾符的相關說明。
  • 委派項目的相關說明以及 by 委派項目的使用方式。

建構項目

  • 智慧住宅裝置的類別結構。
  • 代表智慧型裝置 (例如智慧型電視和智慧型燈具) 的類別。

軟硬體需求

  • 可連上網際網路並具備網路瀏覽器的電腦

2. 定義類別

定義類別時,您必須指定該類別的所有物件應包含的屬性和方法。

類別定義以 class 關鍵字為開頭,後面接有名稱和一組大括號。左大括號前的語法部分也稱為類別標頭。您可以在大括號中指定類別的屬性和函式。您馬上就會學到屬性和函式。類別定義的語法如下圖所示:

以類別關鍵字開頭,後面接有名稱和一組左右大括號。大括號內含用來描述藍圖的類別主體。

建議遵循的類別命名慣例如下:

  • 您可以選擇任何類別名稱,但不要使用 Kotlin 關鍵字做為類別名稱,例如 fun 關鍵字。
  • 類別名稱以 PascalCase 編寫,因此每個字詞皆以大寫字母開頭,且字詞之間沒有空格。以 SmartDevice 為例,每個字詞的第一個字母為大寫,且字詞之間沒有空格。

類別由三個主要部分組成:

  • 屬性:指定類別物件屬性的變數。
  • 方法:包含類別行為和動作的函式。
  • 建構函式:特殊的成員函式,可在定義類別的整個程式中建立該類別的例項。

這不是您第一次使用類別。在先前的程式碼研究室中,您已瞭解 IntFloatStringDouble 等資料類型。在 Kotlin 中,這些資料類型已定義為類別。定義以下程式碼片段所示的變數時,您必須建立 Int 類別的物件 (該類別以 1 值建立例項):

val number: Int = 1

定義 SmartDevice 類別:

  1. Kotlin Playground 中,將內容替換為空白的 main() 函式:
fun main() {
}
  1. main() 函式之前的行上,使用含有「// empty body」註解的主體來定義 SmartDevice 類別:
class SmartDevice {
    // empty body
}

fun main() {
}

3. 建立類別的例項

如先前所學,類別是物件的藍圖。Kotlin 執行階段會使用類別 (或藍圖) 來建立該特定類型的物件。有了 SmartDevice 類別,等同您掌握智慧型裝置的藍圖。如要讓程式中「實際」顯示智慧型裝置,就需要建立 SmartDevice 物件例項。建立例項的語法以類別名稱開頭,後面接有一組括號,如下圖所示:

1d25bc4f71c31fc9.png

如要使用物件,您必須建立物件並指派給變數,做法與定義變數的方式類似。建立不可變變數時,請使用 val 關鍵字;如果是可變動變數,則使用 var 關鍵字。valvar 關鍵字後面依序接有變數名稱、= 指定運算子和類別物件例項。語法如下圖所示:

f58430542f2081a9.png

建立 SmartDevice 類別的物件例項:

  • main() 函式中,使用 val 關鍵字建立名為 smartTvDevice 的變數,並將該變數初始化,設為 SmartDevice 類別的例項:
fun main() {
    val smartTvDevice = SmartDevice()
}

4. 定義類別方法

您在單元 1 中學到以下內容:

  • 函式定義會使用 fun 關鍵字,後面接有一組括號和一組大括號。大括號內含的程式碼提供執行工作時所需的相關指示。
  • 呼叫函式會讓系統執行該函式中的程式碼。

類別可執行的動作可定義為類別中的函式。舉例來說,假設您擁有智慧型裝置、智慧型電視或智慧型燈具,這些設備都可透過手機開啟或關閉。在程式語言中,我們會將智慧型裝置轉譯為 SmartDevice 類別,並以 turnOn()turnOff() 函式代表切換開關的動作,用來啟動開啟與關閉行為。

在類別中定義函式的語法與先前所學相同,唯一的差別在於函式是放在類別主體中。在類別主體中定義「函式」時,該函式稱為成員函式或「方法」,代表類別的行為。在本程式碼研究室的其他章節中,出現在類別主體中的函式都一律稱為方法。

SmartDevice 類別中定義 turnOn()turnOff() 方法:

  1. SmartDevice 類別的主體中,定義主體為空白的 turnOn() 方法:
class SmartDevice {
    fun turnOn() {

    }
}
  1. turnOn() 方法的主體中新增 println() 陳述式,然後傳入 "Smart device is turned on." 字串:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. turnOn() 方法之後新增 turnOff() 方法來輸出 "Smart device is turned off." 字串:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

物件呼叫方法

截至目前,您已定義一個類別做為智慧型裝置的藍圖、建立類別的例項,並將例項指派給變數。現在可透過 SmartDevice 類別的方法開啟或關閉裝置。

如要呼叫類別中的方法,做法與您在先前的程式碼研究室中從 main() 函式呼叫其他函式的方式類似。舉例來說,如果您需要從 turnOn() 方法呼叫 turnOff() 方法,可編寫與以下程式碼片段類似的內容:

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

如要在類別外部位置呼叫類別方法,請以類別物件為開頭,後面依序接上 . 運算子、函式名稱以及一組括號。視情況在括號中納入方法所需的引數。語法如下圖所示:

fc609c15952551ce.png

對物件呼叫 turnOn()turnOff() 方法:

  1. main() 函式中 smartTvDevice 變數之後的行上,呼叫 turnOn() 方法:
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. turnOn() 方法之後的行上呼叫 turnOff() 方法:
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 執行程式碼。

輸出內容如下:

Smart device is turned on.
Smart device is turned off.

5. 定義類別屬性

您在單元 1 中學到變數基本知識,瞭解變數就是單一資料的容器。此外,您也學習了如何使用 val 關鍵字建立唯讀變數,以及如何使用 var 關鍵字建立可變動變數。

方法用來定義類別可執行的動作,而屬性則用來定義類別的特性或資料屬性。舉例來說,智慧型裝置具備以下屬性:

  • 名稱:裝置名稱。
  • 類別:智慧型裝置的類型,例如娛樂、公用事業項目或烹飪。
  • 裝置狀態:裝置處於開啟、關閉、連線或離線狀態。裝置只要連上網際網路即視為是連線狀態,否則為離線狀態。

基本上,屬性是在類別主體 (而非函式主體) 中定義的變數。也就是說,用來定義屬性和變數的語法都相同。定義不可變屬性時必須使用 val 關鍵字,定義可變動屬性時則使用 var 關鍵字。

實作上述特性做為 SmartDevice 類別的屬性:

  1. turnOn() 方法之前的行上,定義 name 屬性並指派給 "Android TV" 字串:
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. name 屬性之後的行上,定義 category 屬性並指派給 "Entertainment" 字串,然後定義 deviceStatus 屬性並指派給 "online" 字串:
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. smartTvDevice 變數之後的行上,呼叫 println() 函式並傳入 "Device name is: ${smartTvDevice.name}" 字串:
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 執行程式碼。

輸出內容如下:

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

屬性中的 getter 和 setter 函式

屬性的用途比變數廣泛。舉例來說,假設您建立了類別結構來代表智慧型電視,您會執行的其中一個常見動作就是調高及調低音量。如要以程式語言表示這項動作,請建立名為 speakerVolume 的屬性,其中包含電視喇叭目前設定的音量等級,但音量值有範圍限制。可設定的最小音量為 0,最大音量為 100。為確保 speakerVolume 屬性絕不會超過 100 或低於 0,您可以編寫「setter」函式。當您更新屬性的值時,必須檢查該值是否介於 0 到 100 的範圍內。再舉一個例子,假設您必須確保名稱一律為大寫,可以實作「getter」函式將 name 屬性轉換為大寫。

在深入瞭解如何實作這些屬性之前,您必須先瞭解宣告屬性的完整語法。定義「可變動」屬性的完整語法必須以變數定義開頭,後面接有選用的 get()set() 函式。語法如下圖所示:

f2cf50a63485599f.png

如果您未為屬性定義 getter 和 setter 函式,Kotlin 編譯器會在內部建立這兩個函式。舉例來說,當您使用 var 關鍵字定義 speakerVolume 屬性並為其指派 2 值時,編譯器會自動產生 getter 和 setter 函式,如以下程式碼片段所示:

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

您不會看到這幾行程式碼,因為編譯器會在背景中加入程式碼。

「不可變」屬性的完整語法有以下兩個差異:

  • 開頭為 val 關鍵字。
  • val 類型的變數是唯讀變數,因此不含 set() 函式。

Kotlin 屬性會使用「支援欄位」將值保留在記憶體中。基本上,「支援欄位」是在屬性中內部定義的類別變數。「支援欄位」的限定範圍為屬性,代表您只能透過 get()set() 屬性函式存取該欄位。

如要讀取 get() 函式中的屬性值或更新 set() 函式中的值,需要使用該屬性的「支援欄位」。這個欄位是由 Kotlin 編譯器自動產生,並以 field ID 參照。

舉例來說,如果您想要在 set() 函式中更新屬性值,可使用 set() 函式的參數 (稱為 value 參數),並將其指派給 field 變數,如以下程式碼片段所示:

var speakerVolume = 2
    set(value) {
        field = value    
    }

舉例來說,如要確保指派給 speakerVolume 屬性的值介於 0 到 100 之間,您可以實作「setter」函式,如以下程式碼片段所示:

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

您可以在 set() 函式中使用 in 關鍵字,並在後面加上值的範圍,以檢查 Int 值是否介於 0 到 100 的範圍內。如果該值在預期範圍內,系統會更新 field 值,否則屬性的值維持不變。

在本程式碼研究室的「實作類別之間的關聯」章節中,您已將這個屬性納入類別,因此現在不需要在程式碼中加入 setter 函式。

6. 定義建構函式

「建構函式」的主要用途是指定類別物件的建立方式。換句話說,建構函式會初始化物件,讓物件可供使用。您在建立物件例項時就已完成這項工作。建立類別的物件例項時,系統會執行建構函式中的程式碼。您可以定義包含或不含參數的建構函式。

預設建構函式

預設建構函式為不含參數的建構函式。定義預設建構函式的做法如以下程式碼片段所示:

class SmartDevice constructor() {
    ...
}

Kotlin 旨在簡化程式碼,因此如果建構函式中沒有任何註解或瀏覽權限修飾符,即可移除 constructor 關鍵字。如果建構函式沒有任何參數,也可以移除括號,如以下程式碼片段所示:

class SmartDevice {
    ...
}

Kotlin 編譯器會自動產生預設建構函式。您不會在程式碼中看到自動產生的預設建構函式,因為編譯器會在背景自動加入。

定義參數化建構函式

SmartDevice 類別中,namecategory 屬性為不可變屬性。您必須確保 SmartDevice 類別的所有例項都初始化 namecategory 屬性。在目前的實作中,namecategory 屬性的值都採用硬式編碼。也就是說,所有智慧型裝置都會以 "Android TV" 字串命名,並以 "Entertainment" 字串分類。

為了維持不變性,同時避免使用硬式編碼值,請使用參數化建構函式進行初始化:

  • SmartDevice 類別中,將 namecategory 屬性移至建構函式,且不指派預設值:
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

建構函式現在可用參數設定本身的屬性,因此為該類別建立物件例項的方法也跟著改變。建立物件例項的完整語法如下圖所示:

bbe674861ec370b6.png

程式碼範例如下:

SmartDevice("Android TV", "Entertainment")

建構函式的這兩個引數都是字串,因此我們不清楚該為哪個參數指定值。解決這個問題的做法與傳遞函式引數的方式類似,只要建立包含具名引數的建構函式即可,如以下程式碼片段所示:

SmartDevice(name = "Android TV", category = "Entertainment")

Kotlin 中的建構函式主要有兩種類型:

  • 主要建構函式:一個類別只能有一個主要建構函式。主要建構函式須在類別標頭中定義,可以是預設或參數化的建構函式。主要建構函式沒有主體,代表其中不包含任何程式碼。
  • 次要建構函式:一個類別可以有多個次要建構函式。您可以定義包含或不含參數的次要建構函式。次要建構函式可以初始化類別,具備包含初始化邏輯的主體。如果類別有主要建構函式,每個次要建構函式都必須初始化主要建構函式。

您可以使用主要建構函式,在類別標頭中初始化屬性。傳遞至建構函式的引數會指派給屬性。定義主要建構函式的語法會以類別名稱為開頭,後面接有 constructor 關鍵字和一組括號,而括號中包含主要建構函式的參數。如果有多個參數,請將參數定義以半形逗號分隔。定義主要建構函式的完整語法如下圖所示:

aa05214860533041.png

次要建構函式包含在類別的主體中,其語法包含下列三個部分:

  • 次要建構函式宣告:次要建構函式的定義以 constructor 關鍵字為開頭,後面接有括號。視情況在括號中納入次要建構函式所需的參數。
  • 主要建構函式初始化:初始化作業會以半形冒號為開頭,後面接有 this 關鍵字和一組括號。視情況在括號中納入主要建構函式所需的參數。
  • 次要建構函式主體:主要建構函式初始化後面接有一組大括號,其中包含次要建構函式的主體。

語法如下圖所示:

2dc13ef136009e98.png

舉例來說,假設您想整合由智慧型裝置供應商開發的 API。不過,API 會傳回 Int 類型的狀態碼來表示初始裝置狀態。如果裝置為「離線」狀態,API 會傳回 0 值;如果裝置為「連線」狀態,則會傳回 1 值。其他整數值則視為「未知」狀態。您可以在 SmartDevice 類別中建立次要建構函式,將 statusCode 這個參數轉換為字串表示法,如以下程式碼片段所示:

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. 實作類別之間的關聯

繼承可讓您根據其他類別的特性和行為建立類別。您可以透過這項強大機制編寫可重複使用的程式碼,以及建立類別之間的關聯。

舉例來說,市面上有許多智慧型裝置,例如智慧型電視、智慧型燈具和智慧型開關。當您將智慧型裝置以程式語言表示時,這些裝置會有一些通用屬性 (例如名稱、類別和狀態) 和通用行為 (例如具備開啟或關閉功能)。

不過,每部智慧型裝置開啟或關閉的方式並不相同。假設您要開啟電視,必須先開啟螢幕,然後設定最近一次的已知音量和頻道。另一方面,開啟燈具時則只需調高或調低亮度即可。

此外,每部智慧型裝置都各自具備其他功能和動作供您使用。舉例來說,您可以在電視上調整音量並切換頻道,使用燈光時則可調整亮度或色彩。

簡單來說,所有智慧型裝置都有不同的功能,但也有一些共同的特性。您可以針對每部智慧型裝置類別複製這些通用特性,或透過繼承讓程式碼可重複使用。

如要這麼做,您必須建立 SmartDevice 父項類別,並定義這些通用屬性和行為。接著,建立子項類別 (例如 SmartTvDeviceSmartLightDevice 類別) 來繼承父項類別的屬性。

在程式設計術語中,假設 SmartTvDeviceSmartLightDevice 類別會「擴充」SmartDevice 父項類別。父項類別也稱為「父類別」,子項類別也稱為「子類別」。這些類別之間的關聯如下圖所示:

展示類別間繼承關聯的圖表。

然而,Kotlin 中的所有類別皆為最終層級。也就是說,您無法擴充這些類別,因此必須定義類別之間的關聯。

定義 SmartDevice 父類別及其子類別之間的關聯:

  1. SmartDevice 父類別中的 class 關鍵字前面加上 open 關鍵字,使其可供擴充:
open class SmartDevice(val name: String, val category: String) {
    ...
}

open 關鍵字會告知編譯器該類別可供擴充,因此其他類別現在可擴充該類別。

建立子類別的語法與您至目前為止建立類別標頭的結構相同。建構函式的右括號後面接有空格、冒號、另一個空格、父類別名稱和一組括號。視情況在括號中納入父類別建構函式所需的參數。語法如下圖所示:

1ac63b66e6b5c224.png

  1. 建立擴充 SmartDevice 父類別的 SmartTvDevice 子類別:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

SmartTvDeviceconstructor 定義無法指定屬性是否可變動,這表示 deviceNamedeviceCategory 參數只是 constructor 參數,而不是類別屬性。您無法在類別中使用這些參數,只能將其傳遞至父類別建構函式。

  1. SmartTvDevice 子類別主體中,新增您在學習 getter 和 setter 函式時所建立的 speakerVolume 屬性:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 定義指派為 1 值的 channelNumber 屬性,並納入指定 0..200 範圍的 setter 函式定義:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. 定義會調高音量並輸出 "Speaker volume increased to $speakerVolume." 字串的 increaseSpeakerVolume() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    } 
}
  1. 新增可增加頻道號碼並輸出 "Channel number increased to $channelNumber." 字串的 nextChannel() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
    
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. SmartTvDevice 子類別之後的行上,定義可擴充 SmartDevice 父類別的 SmartLightDevice 子類別:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. SmartLightDevice 子類別主體中,定義指派為 0 值的 brightnessLevel 屬性,並納入指定 0..100 範圍的 setter 函式定義:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 定義會調高亮度並輸出 "Brightness increased to $brightnessLevel." 字串的 increaseBrightness() 方法:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

類別之間的關係

使用繼承時,就會在兩個類別之間建立關聯,也稱為「IS-A 關聯」。如果物件是繼承自某個類別,也會是該類別的例項。在「HAS-A 關聯」中,物件可擁有其他類別的例項,且不必是該類別的例項。下方是這些關聯的簡略示意圖:

HAS-A 與 IS-A 關聯的高階示意圖。

IS-A 關聯

SmartDevice 父類別和 SmartTvDevice 子類別之間指定 IS-A 關聯時,即表示 SmartDevice 父類別能執行的動作,SmartTvDevice 子類別也都能執行。這種關聯為單向性,因此您可以說每部智慧型電視「都是」智慧型裝置,但不能說每部智慧型裝置「都是」智慧型電視。IS-A 關聯的範例程式碼如以下程式碼片段所示:

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

請不要只為了讓程式碼可重複使用而使用繼承。在您決定使用之前,請檢查這兩個類別彼此是否有關聯性。如果兩者之間有某種關聯性,請檢查是否符合 IS-A 關聯的定義。不妨試問自己:「我可以說子類別就是父類別嗎?」例如,Android「是」一種作業系統。

HAS-A 關聯

HAS-A 關聯是指定兩個類別之間關聯性的另一種方法。舉例來說,假設您要使用家中的智慧型電視。在這個情況下,智慧型電視和住家之間有某種關聯性。住家中包含智慧型裝置,亦即住家「有」智慧型裝置。這兩個類別之間的「HAS-A」關聯也稱為「組合」

您目前已建立了幾部智慧型裝置,而現在可開始建立 SmartHome 類別,其中包含智慧型裝置。SmartHome 類別可讓您與智慧型裝置進行互動。

使用 HAS-A 關聯來定義 SmartHome 類別:

  1. SmartLightDevice 類別和 main() 函式之間,定義 SmartHome 類別:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() { 
    ...
}
  1. SmartHome 類別建構函式中,使用 val 關鍵字來建立 SmartTvDevice 類型的 smartTvDevice 屬性:
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. SmartHome 類別的主體中,定義 turnOnTv() 方法,以便對 smartTvDevice 屬性呼叫 turnOn() 方法:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. turnOnTv() 方法之後的行上,定義 turnOffTv() 方法,以便對 smartTvDevice 屬性呼叫 turnOff() 方法:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. turnOffTv() 方法之後的行上,定義 increaseTvVolume() 方法,以便對 smartTvDevice 屬性呼叫 increaseSpeakerVolume() 方法,然後定義 changeTvChannelToNext() 方法,以便對 smartTvDevice 屬性呼叫 nextChannel() 方法:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. SmartHome 類別建構函式中,將 smartTvDevice 屬性參數移至其專屬的行,後面加上半形逗號:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. smartTvDevice 屬性之後的行上,使用 val 關鍵字來定義 SmartLightDevice 類型的 smartLightDevice 屬性:
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. SmartHome 主體中,定義 turnOnLight() 方法,以便對 smartLightDevice 物件呼叫 turnOn() 方法,並且定義 turnOffLight() 方法,以便對 smartLightDevice 物件呼叫 turnOff() 方法:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. turnOffLight() 方法之後的行上,定義 increaseLightBrightness() 方法,以便對 smartLightDevice 屬性呼叫 increaseBrightness() 方法:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}
  1. increaseLightBrightness() 方法之後的行中定義 turnOffAllDevices() 方法,以便呼叫 turnOffTv()turnOffLight() 方法:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

覆寫子類別中的父類別方法

如先前所述,雖然所有智慧型裝置都支援開啟和關閉功能,但功能執行方式有所不同。如要提供這類特定裝置獨有的行為,您需覆寫父類別中定義的 turnOn()turnOff() 方法。覆寫代表要攔截動作,通常以手動控制。覆寫方法時,子類別中的方法會中斷系統執行父類別中定義的方法,並提供其自有的執行內容。

覆寫 SmartDevice 類別 turnOn()turnOff() 方法:

  1. SmartDevice 父類別主體中,找到每個方法的 fun 關鍵字並在前面加上 open 關鍵字:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. SmartLightDevice 類別的主體中,定義主體為空白的 turnOn() 方法:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
    }
}
  1. turnOn() 方法的主體中,分別將 deviceStatusbrightnessLevel 屬性設為「on」字串與 2 值,接著新增 println() 陳述式,然後傳入 "$name turned on. The brightness level is $brightnessLevel." 字串:
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. SmartLightDevice 類別的主體中,定義主體為空白的 turnOff() 方法:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
    }
}
  1. turnOff() 方法的主體中,分別將 deviceStatusbrightnessLevel 屬性設為「off」字串與 0 值,接著新增 println() 陳述式,然後傳入 "Smart Light turned off" 字串:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}
  1. SmartLightDevice 子類別中,找到 turnOn()turnOff() 方法的 fun 關鍵字,並在前面加上 override 關鍵字:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

override 關鍵字會告知 Kotlin 執行階段,執行子類別中定義的方法所納入的程式碼。

  1. SmartTvDevice 類別的主體中,定義主體為空白的 turnOn() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
        
    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
        
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
    
    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    fun turnOn() {
    }
}
  1. turnOn() 方法的主體中,將 deviceStatus 屬性設為字串「on」並新增 println() 陳述式,然後傳入 "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber." 字串:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }
}
  1. SmartTvDevice 類別的主體中,於 turnOn() 方法後定義主體為空白的 turnOff() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. turnOff() 方法的主體中,將 deviceStatus 屬性設為字串「off」並新增 println() 陳述式,然後傳入 "$name turned off" 字串:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. SmartTvDevice 類別中,找到 turnOn()turnOff() 方法的 fun 關鍵字,並在前面加上 override 關鍵字:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. main() 函式中,使用 var 關鍵字定義 SmartDevice 類型的 smartDevice 變數,將可接受 "Android TV" 引數和 "Entertainment" 引數的 SmartTvDevice 物件例項化:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. smartDevice 變數之後的行上,對 smartDevice 物件呼叫 turnOn() 方法:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. 執行程式碼。

輸出內容如下:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. 在呼叫 turnOn() 方法之後的行中,重新指派 smartDevice 變數,將可接受 "Google Light" 引數和 "Utility" 引數的 SmartLightDevice 類別例項化,然後對 smartDevice 物件參照呼叫 turnOn() 方法:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. 執行程式碼。

輸出內容如下:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

這是多態性的一種例子。程式碼會對 SmartDevice 類型的變數呼叫 turnOn() 方法,並根據變數的實際值執行不同的 turnOn() 方法實作內容。

super 關鍵字在子類別中重複使用父類別程式碼

如果您仔細查看 turnOn()turnOff() 方法,會發現每次在 SmartTvDeviceSmartLightDevice 子類別中呼叫方法時,更新 deviceStatus 變數的方式都很類似,這是因為有重複的程式碼。因此,更新 SmartDevice 類別中的狀態時,您可以重複使用程式碼。

如要從子類別呼叫父類別中遭到覆寫的方法,需要使用 super 關鍵字。呼叫父類別方法的做法與呼叫類別外部方法類似。您在物件與方法之間不能使用 . 運算子,而必須改用 super 關鍵字,這樣才能告知 Kotlin 編譯器呼叫父類別 (而不是子類別)。

用來呼叫父類別方法的語法以 super 關鍵字為開頭,後面接有 . 運算子、函式名稱和一組括號。視情況在括號中納入引數。語法如下圖所示:

18cc94fefe9851e0.png

重複使用 SmartDevice 父類別的程式碼:

  1. turnOn()turnOff() 方法中移除 println() 陳述式,並將重複的程式碼從 SmartTvDeviceSmartLightDevice 子類別移至 SmartDevice 父類別:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. SmartTvDeviceSmartLightDevice 子類別中,使用 super 關鍵字呼叫 SmartDevice 類別的方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

覆寫子類別中的父類別屬性

與方法類似,您也可以透過相同的步驟覆寫屬性。

覆寫 deviceType 屬性:

  1. SmartDevice 父類別中 deviceStatus 屬性之後的行上,使用 openval 關鍵字來定義 deviceType 屬性,並將屬性設為 "unknown" 字串:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. SmartTvDevice 類別中,使用 overrideval 關鍵字來定義 deviceType 屬性,並將屬性設為 "Smart TV" 字串:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. SmartLightDevice 類別中,使用 overrideval 關鍵字定義 deviceType 屬性,並將屬性設為 "Smart Light" 字串:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. 瀏覽權限修飾符

瀏覽權限修飾符是達成封裝的要角,具備以下用途:

  • 在「類別」中隱藏屬性和方法,防範類別外未經授權的存取。
  • 在「套件」中隱藏類別和介面,防範套件外未經授權的存取。

Kotlin 提供四種瀏覽權限修飾符:

  • public:預設的瀏覽權限修飾符,可讓系統在任何位置存取宣告內容。如果要使用的屬性和方法位於類別外,可以將這類屬性和方法標示為 public。
  • private:可讓系統在相同類別或來源檔案中存取宣告內容。

部分屬性和方法可能只在類別內使用,而且您不一定會希望其他類別使用。您可以使用 private 瀏覽權限修飾符標示這類屬性和方法,來確保其他類別無法意外存取這些項目。

  • protected:可讓系統在子類別中存取宣告內容。如果要使用類別中定義的屬性和方法及其子類別,可用 protected 瀏覽權限修飾符加以標示。
  • internal:可讓系統在相同模組中存取宣告內容。internal 修飾符與 private 類似,但您可以在類別外存取類別內部的屬性和方法,只要在相同模組中進行存取即可。

定義類別後,該類別便會公開顯示,且供所有匯入該類別的套件存取。也就是說,除非您指定瀏覽權限修飾符,否則系統預設會公開該類別。同樣地,根據預設,在類別中定義或宣告屬性和方法後,您也可以透過該類別物件,在類別外存取這些項目。請務必為程式碼定義適當的瀏覽權限,主要目的是隱藏屬性和方法,不讓其他沒有必要存取的類別使用。

舉例來說,不妨試想駕駛如何操控汽車的例子。我們會預設隱藏汽車組合零件詳細資訊以及汽車內部運作方式。汽車的設計理念是盡可能提供直覺的操控方式,因此,您不會希望汽車的操控方式像民航機一樣複雜,就如同您不希望其他開發人員或未來的自己,對要使用類別的哪個屬性和方法感到困惑。

瀏覽權限修飾符可協助您將程式碼相關部分提供給專案中的其他類別使用,並確保實作內容不會意外遭到使用,進而讓程式碼易於瞭解且不易出錯。

當您宣告類別、方法或屬性時,必須將瀏覽權限修飾符放置在宣告語法之前,如下圖所示:

dcc4f6693bf719a9.png

為屬性指定瀏覽權限修飾符

為屬性指定瀏覽權限修飾符的語法以 privateprotectedinternal 修飾符為開頭,後面接有定義屬性的語法。語法如下圖所示:

47807a890d237744.png

舉例來說,您可以查看以下程式碼片段,瞭解如何將 deviceStatus 屬性設為不公開:

open class SmartDevice(val name: String, val category: String) {

    ...

    private var deviceStatus = "online"

    ...
}

您也可以將瀏覽權限修飾符設為 setter 函式,並將修飾符放在 set 關鍵字前面。語法如下圖所示:

cea29a49b7b26786.png

對於 SmartDevice 類別,deviceStatus 屬性的值應可透過類別物件在類別外讀取。不過,只有該類別及其子類別可以更新或寫入這個值。如要實作這項規定,您必須對 deviceStatus 屬性的 set() 函式使用 protected 修飾符。

deviceStatus 屬性的 set() 函式使用 protected 修飾符:

  1. SmartDevice 父類別的 deviceStatus 屬性中,為 set() 函式新增 protected 修飾符:
open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set(value) {
           field = value
       }

    ...
}

您不會在 set() 函式中執行任何動作或檢查,只需將 value 參數指派給 field 變數即可。如先前所學,這類似屬性 setter 的預設實作方式。在這個情況下,您可以省略 set() 函式的括號和主體:

open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. SmartHome 類別中定義 deviceTurnOnCount 屬性,將該屬性的值設為 0,並納入不公開的 setter 函式:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. 將後面接有 ++ 算術運算子的 deviceTurnOnCount 屬性加到 turnOnTv()turnOnLight() 方法中,然後將後面接有 -- 算術運算子的 deviceTurnOnCount 屬性加到 turnOffTv()turnOffLight() 方法中:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }
    
    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

適用於方法的瀏覽權限修飾符

為方法指定瀏覽權限修飾符的語法會以 privateprotectedinternal 修飾符為開頭,後面接有定義方法的語法。語法如下圖所示:

e0a60ddc26b841de.png

舉例來說,您可以查看以下程式碼片段,瞭解如何在 SmartTvDevice 類別中為 nextChannel() 方法指定 protected 修飾符:

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    protected fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }      

    ...
}

適用於建構函式的瀏覽權限修飾符

無論是要為建構函式指定瀏覽權限修飾符,還是定義主要建構函式,兩者的語法都很相似,但有以下差異:

  • 指定修飾符的位置必須在類別名稱之後,但在 constructor 關鍵字之前。
  • 指定主要建構函式的修飾符時,即使函式內沒有參數也必須保留 constructor 關鍵字和括號。

語法如下圖所示:

6832575eba67f059.png

舉例來說,您可以查看以下程式碼片段,瞭解如何將 protected 修飾符新增到 SmartDevice 建構函式中:

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

適用於類別的瀏覽權限修飾符

為類別指定瀏覽權限修飾符的語法會以 privateprotectedinternal 修飾符為開頭,後面接有定義類別的語法。語法如下圖所示:

3ab4aa1c94a24a69.png

舉例來說,您可以查看以下程式碼片段,瞭解如何為 SmartDevice 類別指定 internal 修飾符:

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

在理想情況下,您應盡力嚴格管控屬性和方法的瀏覽權限,因此請盡可能透過 private 修飾符宣告這些屬性和方法。如果無法確保這些內容不對外公開,請使用 protected 修飾符。如果無法保護這些內容,請使用 internal 修飾符。如果無法讓這些內容僅限內部使用,請使用 public 修飾符。

指定適當的瀏覽權限修飾符

下表可協助您根據類別/建構函式的屬性/方法存取位置,決定適當的瀏覽權限修飾符:

修飾符

可在相同類別中存取

可在子類別中存取

可在相同模組中存取

可在模組外存取

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

SmartTvDevice 子類別中,建議您不要允許在類別外部位置控管 speakerVolumechannelNumber 屬性。這些屬性只能透過 increaseSpeakerVolume()nextChannel() 方法控管。

同樣地,在 SmartLightDevice 子類別中,brightnessLevel 屬性只能透過 increaseLightBrightness() 方法控管。

將適當的瀏覽權限修飾符加入 SmartTvDeviceSmartLightDevice 子類別:

  1. SmartTvDevice 類別中,將 private 瀏覽權限修飾符加到 speakerVolumechannelNumber 屬性中:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. SmartLightDevice 類別中,將 private 修飾符加到 brightnessLevel 屬性中:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. 定義屬性委派項目

您在先前的章節中學到,Kotlin 中的屬性會使用「支援欄位」將值保留在記憶體中。您可以使用 field ID 來參照支援欄位。

如果您查看到目前為止完成的程式碼,可以發現重複的程式碼;該程式碼可檢查 SmartTvDeviceSmartLightDevice 類別中 speakerVolumechannelNumberbrightnessLevel 屬性的值是否介於指定範圍內。您可以在 setter 函式中,透過「委派」項目重複使用這段檢查範圍的程式碼。委派項目可代替使用欄位、getter 和 setter 函式的方式來管理這個值。

建立屬性委派項目的語法會以變數宣告為開頭,後面接有 by 關鍵字,以及用來處理屬性的 getter 和 setter 函式的委派物件。語法如下圖所示:

928547ad52768115.png

在實作您可以委派實作內容的目標類別之前,請先熟悉「介面」的基本知識。介面是類別在實作介面內容時須遵循的合約,著重於執行的「內容」,而不是執行的「方法」。簡單來說,介面能協助您達成「抽象」

舉例來說,您在建造房子之前會將您的需求告知建築師,而您的需求可能包含臥室、兒童房、客廳、廚房和兩間浴室。簡單來說,在您指出「個人需求」之後,建築師會具體規劃「滿足您需求的方法」。建立介面的語法如下圖所示:

bfe3fd1cd8c45b2a.png

您已學會如何「擴展」類別及「覆寫」類別的功能。只要透過介面,類別就能「實作」介面內容。類別可針對介面中宣告的方法和屬性提供實作詳細資訊。您將執行與 ReadWriteProperty 介面類似的內容來建立委派項目。我們會在下一個單元中進一步說明介面。

如要為 var 類型建立委派類別,您必須實作 ReadWriteProperty 介面。同樣地,您也必須實作 val 類型的 ReadOnlyProperty 介面。

建立 var 類型的委派項目:

  1. main() 函式之前,建立實作 ReadWriteProperty<Any?, Int> 介面的 RangeRegulator 類別:
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

但請不要擔心角括號及其中的內容。這些項目屬於一般類型,我們會在下一個單元中詳細介紹。

  1. RangeRegulator 類別的主要建構函式中,新增 initialValue 參數、不公開狀態的 minValue 屬性以及不公開狀態的 maxValue 屬性,並將這些屬性都設為 Int 類型:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. RangeRegulator 類別的主體中,覆寫 getValue()setValue() 方法:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

這些方法可做為屬性的 getter 和 setter 函式。

  1. SmartDevice 類別之前的行上,匯入 ReadWritePropertyKProperty 介面:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. RangeRegulator 類別中 getValue() 方法之前的行上,定義 fieldData 屬性並透過 initialValue 參數初始化該屬性:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

這個屬性是做為變數的「支援欄位」

  1. getValue() 方法的主體中,傳回 fieldData 屬性:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}
  1. setValue() 方法的主體中,先檢查指派的 value 參數是否位於 minValue..maxValue 範圍內,再將該參數值指派給 fieldData 屬性:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. SmartTvDevice 類別中,使用委派類別來定義 speakerVolumechannelNumber 屬性:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...

}
  1. SmartLightDevice 類別中,使用委派類別來定義 brightnessLevel 屬性:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    ...

}

10. 測試解決方案

您可以在下方程式碼片段中查看解決方案程式碼:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

輸出內容如下:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

11. 試試這項挑戰

  • SmartDevice 類別中,定義可輸出 "Device name: $name, category: $category, type: $deviceType" 字串的 printDeviceInfo() 方法。
  • SmartTvDevice 類別中,定義可降低音量的 decreaseVolume() 方法以及前往上一個頻道的 previousChannel() 方法。
  • SmartLightDevice 類別中,定義可降低亮度的 decreaseBrightness() 方法。
  • SmartHome 類別中,確保只有在每部裝置的 deviceStatus 屬性設為 "on" 字串時,系統才會執行所有動作。此外,請確認 deviceTurnOnCount 屬性已正確更新。

完成實作後:

  • SmartHome 類別中,定義 decreaseTvVolume()changeTvChannelToPrevious()printSmartTvInfo()printSmartLightInfo()decreaseLightBrightness() 方法。
  • SmartHome 類別的 SmartTvDeviceSmartLightDevice 類別中呼叫適當的方法。
  • main() 函式中,呼叫這些新增的方法來進行測試。

12. 結語

恭喜!您已瞭解如何定義類別並建立物件例項,也學會如何建立類別之間的關聯以及屬性委派。

摘要

  • OOP 有四項主要原則:封裝、抽象、繼承和多態性。
  • 類別是透過 class 關鍵字定義,包含屬性和方法。
  • 屬性與變數類似,但屬性可包含自訂 getter 和 setter。
  • 建構函式會指定如何建立類別的物件例項。
  • 定義主要建構函式時可省略 constructor 關鍵字。
  • 繼承可讓您輕鬆重複使用程式碼。
  • IS-A 關聯指的是繼承關係。
  • HAS-A 關聯指的是組成關係。
  • 瀏覽權限修飾符是達成封裝的要角。
  • Kotlin 提供四種瀏覽權限修飾符:publicprivateprotectedinternal 修飾符。
  • 屬性委派可讓您在多個類別中重複使用 getter 和 setter 程式碼。

瞭解詳情