若要建構較為複雜的應用程式,必須深入瞭解類別與繼承的運作方式,以便充分運用 Android 平台所提供的功能。
學習目標
- 建立Kotlin程式,並使用繼承實作類別階層。
- 擴充類別、覆寫現有的功能,並新增功能。
- 選擇變數適用的適當瀏覽權限修飾符。
什麼是類別階層?
使用者習慣為類似的屬性和行為項目分門別類,甚至排列一些類型的階層。舉例來說,在蔬菜等廣泛的類別下,可以細分為豆類等類型。豆類下可以再細分為豌豆、四季豆、扁豆、鷹嘴豆、大豆等。
而這可用階層表示,因為豆類包含或沿用蔬菜的所有屬性 (例如兩者均為可食用植物)。以此類推,豌豆、四季豆、扁豆都有豆類的屬性,外加豆類獨有的屬性。
以下說明如何以程式設計術語表示上述關係。如果在 Kotlin 中將 Vegetable(蔬菜) 設為類別,即可建立 Legume(豆類) 做為子項,或 Vegetable類別的子類別。即 Legume類別會繼承 (也可使用) Vegetable類別所有的屬性和方法。
以下的類別階層圖表可顯示這項關係。您可以參照 Vegetable 做為 Legume 類別的父項或父類別。
接著建立 Legume 的子類別 (例如 Lentil(扁豆) 和 Chickpea(鷹嘴豆)),然後繼續展開類別階層。這會將 Legume 設為 Vegetable 的子項或子類別,同時也設為 Lentil 和 Chickpea 的父項或父類別。Vegetable 是這個階層的「根」層級或頂層 (或基礎) 類別。
Android 類別中的繼承
舉例來說,Android 的 View 類別代表螢幕上負責繪圖和事件處理的長方形區域。TextView 類別是 View 類別的子類別,代表 TextView 繼承 View 類別的所有屬性和功能,並新增向使用者顯示文字的特定邏輯。
更進一步來說,EditText 和 Button 類別是 TextView 類別的子項。這些類別繼承 TextView 和 View 類別所有的屬性和方法,並新增專屬的特定邏輯。例如,EditText 加入可編輯螢幕上文字的專屬功能。
EditText 可以直接加入 TextView 類別的子類別,同時加入 View 類別的子類別,不必在 EditText 類別複製及貼上 View 和 TextView 類別的所有邏輯。然後,EditText 類別中的程式碼可以致力讓 UI 元件不同於其他檢視畫面。
在 developers.android.com 網站 Android 類別的文件資訊頁面頂端上,即可找到類別階層圖。如果您在階層頂端看到 kotlin.Any,原因是 Kotlin 中所有類別都有相同的父類別「Any」。
建立基礎類別
住宅類別階層
該程式使用住宅和樓層空間、樓層和居民做為範例,示範類別階層的運作方式。
以下是您要建構的類別階層圖。根層級的 Dwelling(住宅) 會指定住宅為 true 的屬性和功能 (類似於藍圖)。接著,您會看到方形小屋(SquareCabin)、圓型茅屋(RoundHut),以及 圓塔(RoundTower,也就是多樓層的 RoundHut)。
實作的類別:
Dwelling:代表非特定遮蔽處的基本類別,包含所有住宅通用資訊。SquareCabin:正方形樓層面積的木製正方形小屋。RoundHut:茅草建造的圓形茅屋,包含圓形樓層面積和 RoundTower 的父項。RoundTower:石頭建造的圓塔,包含圓形樓層面積和多個樓層。
建立抽象住宅類別
「abstract」(抽象) 類別是因為未完整實作,無法執行個體化的類別。您可以將其視為草圖。草圖包含專案的靈感和規劃,但往往缺少資訊建構專案。請使用草圖(abstract class) 建立藍圖(class),然後建構實際的物件執行個體(object/instance)。
建立父類別常見的好處是,包含所有子類別通用的屬性和函式。如果不知道屬性的值和函式的實作,請將類別設為抽象。舉例來說,Vegetables 包含許多所有蔬菜通用的屬性,但您無法為非特定蔬菜建立執行個體,因為您沒有具體資訊,例如形狀或顏色。因此,Vegetable 是抽象類別,由子類別確定每種蔬菜的特定詳細資料。
抽象類別的宣告是以 abstract 關鍵字做為開頭。
Dwelling 是 Vegetable 的抽象類別,它會包含很多類型的住宅通用的屬性和函式,但不知道屬性確切的值和函式實作的詳細資料。
- 前往 Kotlin Playground:https://developer.android.com/training/kotlinplayground。
- 在編輯器中,刪除 main() 函式中的 println(“Hello, world!”)。
- 然後在 main() 函式下方加入以下程式碼,建立名為 Dwelling 的 abstract 類別
1 | abstract class Dwelling(){ |
新增建材的屬性
在這個 Dwelling 類別中,您可以定義所有住宅為 true 的建材,即使建材會因不同的住宅而異。所有住宅都使用建材建構。
- 在
Dwelling中,建立類型String的buildingMaterial變數以代表建材。因為建材不會變更,請使用val做為建材不可變的變數。
1 | abstract class Dwelling(){ |
- 執行程式後,您會看到以下錯誤訊息。
1 | Property must be initialized or be abstract |
buildingMaterial 屬性沒有值。其實您「無法」指定值,因為非特定的建築物不會使用任何特定建材。所以如錯誤訊息所示,您可以在 buildingMaterial 的宣告中前置 abstract 關鍵字,表示不會在這裡定義此屬性。
- 在變數定義中加入
abstract關鍵字。
1 | abstract class Dwelling(){ |
- 重新執行程式後,將不會出現錯誤。
- 在
main()函式中建立Dwelling的執行個體,並執行程式碼。
1 | fun main() { |
- 錯誤訊息的原因是,無法建立抽象
Dwelling類別的執行個體。
1 | Cannot create an instance of an abstract class |
- 刪除不正確的程式碼。
目前完成的程式碼:
1 | abstract class Dwelling(){ |
新增容量屬性
住宅的另一個屬性是居住的人數。
所有住宅的容量都不會變更。但 Dwelling 父類別中無法設定容量。如果是特定類型的住宅,請在子類別定義容量。
在 Dwelling 中,新增名為 capacity 的 abstract 整數 val。
1 | abstract class Dwelling(){ |
新增居民數目的私有屬性
所有住宅都有很多 residents(居民) 居住在其中 (數目可能小於或等於 capacity),所以為了繼承並使用屬性,請定義所有子類別 Dwelling 中的 residents 屬性。
您可以將 residents 參數傳遞至 Dwelling 類別建構函式。residents 屬性為 var,因為建立執行個體後,可以變更居民數目。
1 | abstract class Dwelling(private var residents: Int) { |
residents 屬性會標示 private 關鍵字。Private(私有) 是 Kotlin 的可見度修飾符,即只有這個類別能看到 (並使用) residents 屬性。程式其他位置無法存取此屬性。您可以使用 private 關鍵字標示屬性或方法。否則,在未指定瀏覽權限修飾符時,這些屬性和方法會預設為 public,並可透過程式的其他位置存取。有鑑於住宅的居民數目通常是私人資訊 (相對於建材或建築物容量),這樣的決定很合理。
使用住宅的 capacity 和目前 residents 定義的數目,您可以建立函式 hasRoom(),判斷住宅是否空房容納其他居民。您可以在 Dwelling 類別中定義並實作 hasRoom() 函式,因為計算所有住宅是否有空房,使用的是相同的公式。如果 residents 的數目小於 capacity,Dwelling 中會顯示空房,然後此函式會根據比較值傳回 true 或 false 。
新增 hasRoom() 函式至 Dwelling 類別。
1 | fun hasRoom(): Boolean { |
完成的程式碼如下:
1 | abstract class Dwelling(private var residents: Int) { |
建立子類別
建立 SquareCabin 子類別
- 在
Dwelling類別下方,建立名為SquareCabin的類別。
1 | class SquareCabin |
- 接著,您必須指出
SquareCabin和Dwelling的關連。在程式碼中,您要指出SquareCabin是Dwelling的延伸模組 (或Dwelling) 的子類別,因為SquareCabin會提供Dwelling抽象部分的實作)。
1 | class SquareCabin: Dwelling() |
- 從父類別延伸時,您必須傳入父類別必要的參數。
Dwelling要求residents數目做為輸入內容。您可以傳入居民的固定數目 (例如3)。
1 | class SquareCabin: Dwelling(3) |
- 但若要程式更有彈性,並可以使用
SquareCabins居民的變數數目,請在SquareCabin類別定義中,將residents設為參數。請勿將residents宣告為val,,因為您會重複使用父項類別Dwelling中宣告的屬性。
1 | class SquareCabin(residents: Int): Dwelling(residents) |
- 執行程式碼。
- 這會導致錯誤發生。
1 | Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling |
宣告抽象函式和變數和 promise 一樣,您會在之後提供值並實作。以變數來說,即代表您必須提供值給抽象類別的子類別。以函式來說,即子類別必須實作函式主體。
在 Dwelling 類別中,您已定義 abstract 變數為 buildingMaterial。SquareCabin 是 Dwelling 的子類別,所以必須提供 buildingMaterial 的值。使用 override 關鍵字,指出父項類別已定義這個屬性,並會在此類別中覆寫。
- 在
SquareCabin類別中,overridebuildingMaterial屬性並指派屬性的值為"Wood"。
1 | class SquareCabin(residents: Int): Dwelling(residents) { |
- 對
capacity執行相同動作,例如6位居民可以住在SquareCabin。1
2
3
4class SquareCabin(residents: Int): Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
完成的程式碼應如下所示:
1 | abstract class Dwelling(private var residents: Int) { |
若要測試程式碼,請在程式中建立 SquareCabin 的執行個體。
使用 SquareCabin
- 在
Dwellin和SquareCabin類別定義前,插入空白的main()函式。
1 | fun main() { |
- 在
main()函式中,建立容納6位居民,名為squareCabin的SquareCabin執行個體。新增建材、容量的輸出陳述式,以及hasRoom()函式。
1 | fun main() { |
執行程式碼,程式碼會輸出以下內容:
1 | Square Cabin |
您已建立容納 6 位居民 (亦等於 capacity) 的 squareCabin,因此 hasRoom() 會傳回 false。使用較少的 residents 嘗試初始化 SquareCabin,並再次執行程式後,hasRoom() 會傳回 true。
residents(居民)改成5:
1 | val squareCabin = SquareCabin(5) |
執行結果:
1 | Square Cabin |
使用 with 簡化程式碼
在 println() 陳述式中,每次參考 squareCabin 的屬性或函式時,請注意您必須重複 squareCabin. 的原因。因為複製及貼上輸出陳述式時,可能會發生錯誤。
使用類別的特定執行個體,且必須存取該執行個體的多個屬性和函式時,可以使用 with 陳述式說明:「對這個執行個體物件執行下列所有作業」。以關鍵字 with 開頭,接著是括號中的執行個體名稱,後面加上大括號,包含您要執行的作業。
1 | with (instanceName) { |
1 | with(squareCabin) { |
以下是已完成的程式碼:
1 | fun main() { |
程式碼可正常執行,並顯示相同的輸出內容:
1 | Square Cabin |
建立 RoundHut 子類別
- 與
SquareCabin使用相同的方式,新增另一個子類別(RoundHut)至Dwelling。 - 覆寫
buildingMaterial並指定值為"Straw"。 - 覆寫
capacity並設為4。
1 | class RoundHut(residents: Int) : Dwelling(residents) { |
- 在
main()中,建立容納3位居民的RoundHut執行個體,並輸出roundHut的資訊。
1 | fun main() { |
- 執行程式碼,整個程式的輸出內容應為:
1 | Square Cabin |
目前的類別階層如下所示,其中 Dwelling 是根層級類別,而 SquareCabin 和 RoundHut 是 Dwelling 的子類別。
建立 RoundTower 子類別
這個類別階層最後一個類別是圓塔。圓塔就像是石頭建造、多個樓層的圓形小屋。所以您可以將 RoundTower 設為 RoundHut 的子類別。
- 建立
RoundTower類別,即RoundHut的子類別。將residents參數新增至RoundTower的建構函式,然後將該參數傳遞至RoundHut父類別的建構函式。 - 將
buildingMaterial覆寫為"Stone"。 - 將
capacity設為4。
1 | class RoundTower(residents: Int): RoundHut(residents) { |
- 執行這個程式碼後,您會看到錯誤訊息。
1 | This type is final, so it cannot be inherited from |
這個錯誤代表無法將 RoundHut 類別設為子類別 (或繼承該類別)。根據預設,在 Kotlin 中,類別為最終類別(final),且無法加入子類別。只能繼承 abstract 類別,或標示 open 關鍵字的類別。因此,您必須以 open 關鍵字標示 RoundHut 類別,才能繼承類別。
- 請在
RoundHut宣告開頭加入open關鍵字。
1 | open class RoundHut(residents: Int) : Dwelling(residents) { |
- 在
main()中,建立roundTower的執行個體,並輸出相關資訊
1 | fun main() { |
完整程式碼:
1 | fun main() { |
執行結果:
1 | Square Cabin |
新增多個樓層至 RoundTower
RoundHut (圓形茅屋) 是單層建築物。塔通常是多層 (樓層)。
想一下容量,塔的樓層越多,容量就越多。
您可以修改 RoundTower 為多個樓層,並根據樓層數調整容量。
- 更新
RoundTower建構函式,並接受另一個整數參數valfloors作為樓層的數量。將其置於residents之後。請注意,您不必傳遞參數至父項RoundHut建構函式,因為floors是在RoundTower中定義的,而RoundHut沒有floors。
1 | class RoundTower(residents: Int, val floors: Int) : RoundHut(residents) { |
- 執行程式碼。在
main()方法中建立roundTower時發生錯誤,因為您未提供floors引數的數字。您可以新增遺漏的引數。
您也可以在 RoundTower 的類別定義中,新增 floors 的預設值,如下所示。接著,如果 floors 的值未傳遞至建構函式,您可以使用預設值來建立物件執行個體。
- 在程式碼中,在
floors的宣告後方加上= 2,即可指派預設值2。
1 | class RoundTower(residents: Int, val floors: Int = 2) : RoundHut(residents) { |
執行程式碼。程式碼目前應該可以編譯,因為
RoundTower(4)已可使用2個樓層的預設值,建立RoundTower物件執行個體。在
RoundTower類別中,更新capacity即可乘以樓層數。
1 | override val capacity = 4 * floors |
- 執行程式碼,並留意
RoundTower2個層樓的容量目前為8。
以下是已完成的程式碼:
1 | fun main() { |
執行結果:
1 | Square Cabin |
修改階層中的類別
計算樓層面積
在本練習中,您將瞭解在抽象類別中,如何宣告抽象函式,並在子類別實作函式。
所有住宅都有樓層面積,但根據住宅的形狀,計算方式會有所不同。
定義住宅類別的 floorArea()
- 首先,新增
abstractfloorArea()函式至Dwelling類別。傳回Double。Double是資料類型 (例如 String 和 Int),並用於浮點數目,即包含小數點與其後小數部分的數字 (例如 5.8793)。
1 | abstract fun floorArea(): Double |
抽象類別中定義的所有抽象方法必須在其子類別中實作。執行程式碼前,您必須在子類別中實作 floorArea()。
實作 SquareCabin 的 floorArea()
和 buildingMaterial 和 capacity 一樣,既然要實作父項類別定義的 abstract 函式,就必須使用 override 關鍵字。
- 在
SquareCabin類別中,以關鍵字override做為開頭,接著實際實作floorArea()函式,如下所示。
1 | override fun floorArea(): Double { |
- 傳回計算的
樓層面積。長方形或正方形的面積是邊長乘以另一個邊長。函式主體是return length * length。
1 | override fun floorArea(): Double { |
長度不是類別中的變數,也與所有執行個體不同,所以您可以新增長度,做為 SquareCabin 類別的建構函式參數。
- 變更
SquareCabin的類別定義,並新增Double類型的length參數。請宣告屬性為val,因為建築物的長度不變。
1 | class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) { |
所以 Dwelling 和所有子類別都包含 residents,做為建構函式的引數。因為這是 Dwelling 建構函式的第一個引數,所以最佳做法是將該引數設為所有子類別建構函式的第一個引數,並以相同的順序在所有類別定義中放置引數。因此,請在 residents 參數後方插入新的 length 參數。
- 在
main()中更新squareCabin執行個體的建立作業。傳遞50.0至SquareCabin建構函式,做為length。
原本:
1 | val squareCabin = SquareCabin(6) |
改成:
1 | val squareCabin = SquareCabin(6, 50.0) |
- 在
squareCabin的with陳述式中,新增樓層面積的輸出陳述式。
1 | println("Floor area: ${floorArea()}") |
- 執行程式碼會發生以下錯誤
1 | 'floorArea' overrides nothing |
造成錯誤的原因是也必須在 RoundHut 中實作 floorArea()。
實作 RoundHut 的 floorArea()
請以相同方式,實作 RoundHut 的樓層面積。RoundHut 也是 Dwelling 的直接子類別,所以您必須使用 override 關鍵字。
圓形住宅的樓層面積是 PI * 半徑 * 半徑。
PI 是數學值,由數學程式庫定義。程式庫是程式外部定義的函式和數的集合,可供程式使用。若要使用程式庫的函式或值,您必須說明編譯器要使用的函式或值。方法是在程式匯入函式或值。若要在程式中使用 PI,請匯入 kotlin.math.PI。
- 從 Kotlin 數學程式庫匯入
PI。在檔案頂端的main()前放置下列內容。
1 | import kotlin.math.PI |
- 實作
RoundHut的floorArea()函式。
1 | override fun floorArea(): Double { |
- 更新
RoundHut建構函式,並傳入radius。
1 | open class RoundHut( |
- 在
main()中,傳遞10.0的radius至RoundHut建構函式,即可更新roundHut的初始化作業。
原本:
1 | val roundHut = RoundHut(3) |
改成:
1 | val roundHut = RoundHut(3, 10.0) |
- 在
roundHut的with陳述式中,加入輸出陳述式。
1 | println("Floor area: ${floorArea()}") |
實作 RoundTower 的 floorArea()
程式碼無法執行,並因為下列錯誤失敗:
1 | Error: No value passed for parameter 'radius' |
在 RoundTower 中,若要程式順利編譯,其實不必實作 floorArea(),因為圓塔會繼承 RoundHut 的樓層面積,但為了使用相同的 radius 引數做為其父項 RoundHut,您必須更新 RoundTower 類別定義。
- 變更
RoundTower的建構函式,讓其也接受radius。在residents後方和floors前放置radius。建議您使用結尾列出包含預設值的變數。請記得傳遞radius至父項類別建構函式。
原本:
1 | class RoundTower( |
改成:
1 | class RoundTower( |
- 更新
main()中的roundTower初始化作業。
原本:
1 | val roundTower = RoundTower(4) |
改成:
1 | val roundTower = RoundTower(4, 15.5) |
- 然後加入呼叫
floorArea()的輸出陳述式。
1 | println("Floor area: ${floorArea()}") |
您現在可以執行程式碼了!
請注意,
RoundTower的計算不正確,因為圓塔繼承RoundHut,所以不會計算floors的數目。在
RoundTower中,overridefloorArea()提供不同的實作,讓面積可以乘以樓層數。請注意,您可以在抽象類別(Dwelling)中定義函式、在子類別(RoundHut)中實作函式,然後在子類別(RoundTower)的子類別中,再次覆寫該函式。這是兩個類別的最佳做法,您可以繼承需要的功能,並覆寫不必要的功能。
1 | override fun floorArea(): Double { |
這個程式碼可以使用,但 RoundHut 父項類別中,已有方法可以避免重複的程式碼。您可以從父類別 RoundHut 呼叫 floorArea() 函式,該函式會傳回 PI * radius * radius。然後再將此結果乘以 floors 的數目。
- 在
RoundTower中,更新floorArea()並使用floorArea()的父類別實作。使用super關鍵字呼叫父項中定義的函式。
1 | override fun floorArea(): Double { |
- 再次執行程式碼,然後
RoundTower會輸出多個樓層的正確樓層空間。
以下是已完成的程式碼:
1 | import kotlin.math.PI |
輸出內容應如下所示:
1 | Square Cabin |
允許新居民取得房間
新增功能,使用 getRoom() 函式增加 1 位居民的數目,讓新居民取得房間。這個邏輯適用所有住所,所以您可以在 Dwelling 中實作這個函式,同時所有子類別和其子項都可使用這個函式。
- 使用
if陳述式,只在有容量剩餘時,才能新增居民。 - 輸出結果訊息。
- 您可以使用
residents++做為新增1至residents變數的函式residents = residents + 1簡寫。 - 在
Dwelling類別中實作getRoom()函式。
1 | fun getRoom() { |
- 新增一些輸出陳述式至
roundHut的with陳述式區塊,並觀察搭配使用getRoom()和hasRoom()的變化。
1 | println("Has room? ${hasRoom()}") |
這些輸出陳述式的輸出內容:
1 | Has room? true |
調整圓形住宅的地毯
假設您需要知道 RoundHut 和 RoundTower 的地毯側邊長度是多少。在 RoundHut 放置函式,供所有圓形住所使用。
- 首先,從
kotlin.math程式庫匯入sqrt()函式。
1 | import kotlin.math.sqrt |
- 在
RoundHut類別中實作calculateMaxCarpetLength()函式。以下公式可以計算置於圓形居所的方形地毯長度:sqrt(2) * radius。詳情請見上圖。
1 | fun calculateMaxCarpetLength(): Double { |
將
Double值2.0傳至數學函式sqrt(2.0),因為函式的傳回類型為Double,而非Integer。RoundHut和RoundTower執行個體目前已可呼叫calculateMaxCarpetLength()方法。新增輸出陳述式至main()函式中的roundHut和roundTower。
完整程式碼
1 | /** |
執行結果:
1 | Square Cabin |
總結
瞭解如何:
- 建立類別階層 (即類別的樹狀結構),以及子項會繼承父項類別的功能。子類別會繼承屬性和函式。
- 建立
abstract類別,其子類別會實作其餘的部分功能。所以abstract類別無法執行個體化。 - 建立
abstract類別的子類別。 - 使用
override關鍵字覆寫子類別的屬性和函式。 - 使用
super關鍵字參考父項類別的函式和屬性。 - 建立類別
open,即可加入子類別。 - 建立屬性
private,然後只能在類別中使用該屬性。 - 使用
with建構函式,多次呼叫相同的物件執行個體。 - 從
kotlin.math程式庫匯入功能