若要建構較為複雜的應用程式,必須深入瞭解類別與繼承的運作方式,以便充分運用 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
類別中,override
buildingMaterial
屬性並指派屬性的值為"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
建構函式,並接受另一個整數參數val
floors
作為樓層的數量。將其置於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 |
- 執行程式碼,並留意
RoundTower
2
個層樓的容量目前為8
。
以下是已完成的程式碼:
1 | fun main() { |
執行結果:
1 | Square Cabin |
修改階層中的類別
計算樓層面積
在本練習中,您將瞭解在抽象類別
中,如何宣告抽象函式
,並在子類別
實作函式
。
所有住宅都有樓層面積
,但根據住宅的形狀,計算方式會有所不同。
定義住宅類別的 floorArea()
- 首先,新增
abstract
floorArea()
函式至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
中,override
floorArea()
提供不同的實作,讓面積可以乘以樓層數。請注意,您可以在抽象類別(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
程式庫匯入功能