[Google Course] Android Basics in Kotlin(第3篇) — 類別繼承及變數可見性
就在上次寫完Google Course 上的 Android Basics in Kotlin 單元一之後,居然!!!隔天Google 就上傳了單元二的課程,於是就來記錄一下內容!
第二單元主要是在學習Android的Layout設計,次要帶入一些程式設計的技巧,不過因為在一篇文章中寫這麼多內容會太混亂,所以之後的文章會將Google Course 分為更小的章節來發布文章,而這篇文章內容如下:
● 類別的繼承 (Inheritance of Class)
●覆寫父類別變數 — override
● with 語法
● 關鍵字 open
● 覆寫父類別函式 — override
● QA
為了怕有些人沒有看之前的文章,文章中會使用到的編譯環境與參考內容也在這邊列出給大家:
[類別的繼承 (Inheritance of Class)]
今天就先來接續上次的類別class繼續來學習,文章結束預計會學會用物件導向的繼承概念以及如何實際應用,我們先來複習一下類別的概念並且用具體的例子說明:
類別 class:單純定義物件的藍圖,沒有創造出實體物件
類別內函式:我們稱為類別的方法成員
類別內變數:我們稱為類別的屬性成員
類別是程式設計者自訂的資料型態,將程式中彼此相關的變數與函式整合在一起,成為一個類別
我們想像我們要做一個賽車遊戲,以車子來當作範例,每台車子從外觀來看就有顏色、人數、品牌,另外還有價錢、速度、位置(姑且先不管方向,你的車只能一直往前哈哈哈),你也可以加入你認為車子應該要有的屬性及功能,因此按照我們上篇文章的類別寫法,你可以試著寫出一個名稱為Car的類別嗎?我們一起來寫寫看:
class Car()
{
var brand : String = "" /// 品牌 屬性成員
var color : String = "" /// 顏色 屬性成員
var capacity : Int = 0 /// 人數 屬性成員
var price : Int = 15000 /// 價錢 屬性成員
var speed : Int = 30 /// 速度 /// 30m / 1s 屬性成員
var position : Int = 0 /// 位置 屬性成員
}
而車子最主要的動作(action)就是啟動後會跑,因此我們要賦予車子一個能力,“跑” 這個函式加進車子的類別中:
fun run() /// 方法成員
{
position = position + speed
}
對我們而言,上述說的屬性都是車子共同會有的,我們將這些特性組成一個組別,甚至我們也可以從這個組別延伸出具有層級的結構,例如車子類別中可以有一個更特定的類型,像是休旅車,而休旅車又可以再延伸出更多特定的類型,像是越野休旅車及都會休旅車,可以用以下的繼承關係圖來描述:
敞篷車、休旅車及貨車包含了車子的所有屬性及方法,休旅車的子類別也包含了車子的所有屬性及方法,他們同時繼承的車子的屬性及方法,並且能夠延伸出屬於自己的屬性及方法,以及被繼承
這代表著我們可以持續不斷的衍伸、拓展新的類別來繼承我們原本設計好的父類別,這就是物件導向其中一個重要的概念 — 繼承(Inheritance)
在Android 中的元件有很多範例值得我們學習,也都是以物件導向的繼承概念來製作成的,可以看到通常我們在開發者文件中左上角會看到如下的階層表:
這是代表Button是從TextView繼承而來,有文字 text、寬度 width、高度height等等,Button卻又比TextView多了按鈕觸發的事件動作,其他還有像是EditText也是繼承TextView,但也比TextView多了編輯文字的動作
而在Kotlin中要如何繼承一個class呢?在這篇文章想像我們是一個房仲公司 — 異想世界房屋股份有限公司,要來建立一個房仲網站,在這個網站我們需要建立良好的系統架構來描述各種房子,好讓我們的使用者了解各種房子的資訊
目前異想世界房屋收到兩個屋主的房子要出租,他們分別出租A屋主 — 木製方艙(SquareCabin)、B屋主 — 稻草圓屋(RoundHut)、B屋主 — 石製圓塔(RoundTower)
所以這邊我們會建立一個房屋架構來展示物件導向繼承的概念,我們將會建立一個父類別(Dwelling)是房子總稱,接著創建三個子類別(方型小屋、圓形小屋、圓形塔樓)都是繼承Dwelling的:
以下是我們會創造的類別的用途:
● Dwelling:非特定的類別,泛指所有建築物,包含所有一般來說房子所擁有的資訊
● SquareCabin: 一個由木頭(wood)所製作的正方形房屋,容納人數為6人
● RoundHut:一個由稻草(straw)所製作的圓形房屋,同時也是圓塔的父類別,容納人數為4人
● RoundTower: 一個由石頭(stone)所製作的圓塔,並且擁有多層樓,一層容納人數為4人
讓我們先創建一個新的類別叫Dwelling:
abstract class Dwelling(){
}
有發現這次創建的 class 前面多了一個abstract 嗎?
這是Kotlin的關鍵字之一,這是代表這一個class是沒有辦法被實體化成物件的抽象類別,你可以把這個想像程式一個類別的草圖,它代表著我們構想的一些想法,但是他並不足以構成一個物件
以房子為例,房子有很多個屬性,但是你沒有辦法創建房子的實體,因為你不知道你的房子有什麼屬性,大小是什麼,用什麼材質,這些必須要看有哪些人要出租他們的房屋之後才能決定,所以房子是一個抽象類別,包含了一般房子擁有的屬性及方法,但是確定的數值及方法的實作是保留給子類別來決定的
接著,我們試著為房子定義一些屬性 — 建立材質、容納人數,而在抽象類別中的屬性,我習慣是定義為抽象屬性,的確在還沒決定是哪種房子前,我們無法決定他的大小及材質對吧!
abstract class Dwelling()
{
abstract val buildingMaterial: String
abstract val capacity: Int
}
再來因為每種房子可以容納的人數不同,而實際要租屋的人數依據現實考量都會不一樣,我們可以在建構房子類別時,就賦予類別一個參數residents
,參數的型態寫在後方,是一個Int類型的變數,而這個residents 勢必必須比capacity來得小,所以我們再建一個函式名稱為 hasroom
,來判斷是否還有房間可以出租給別人,回傳一個布林值:
abstract class Dwelling(private var residents: Int)
{
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean
{
return residents < capacity
}
}
注意到 private var residents: Int
前方有一個關鍵字 — private,private 是一個變數可見性的關鍵字,這是代表residents 這個變數只能夠在單一物件類別中使用,在程式的其他地方並不能獲取這個變數值(就像是你從房子的外觀並不能知道裡面住了多少人一樣),所以我們將它設為private,而變數的可見性在下週文章會提及,在期待一下吧~
在Kotlin 的函數回傳值的方式和Java不太一樣,回傳值的型態寫在函式名稱後方並用冒號區隔,在函式內我們直接return 回傳一個判斷式結果(布林值)
這些屬性和方法都可以被設計在Dwelling的類別內,因為他們都是房子的共同成員
接著我們要來創建更多子類別來練習一下繼承的實作,相信這對大家設計程式的架構會有很大的幫助的!!
首先我們先創建一個類別 SquareCabin
,繼承自 Dwelling
:
class SquareCabin : Dwelling(3){} ---- 1
在繼承類別時你可以用方法1來繼承Dwelling,但是這樣會限制住SquareCabin
一開始創建實體物件時的租屋人數為3,但如果你想要更有彈性的設計SquareCabin
,你可以用方法2:
class SquareCabin(residents: Int) : Dwelling(residents){} ---- 2
方法2我們讓居住人數residents
成為SquareCabin
創建時的參數,並且透過繼承,讓父類別也使用同樣的residents
,我們現在的程式如下,你可以執行看看程式:
abstract class Dwelling(private var residents: Int)
{
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean
{
return residents < capacity
}
}class SquareCabin(residents: Int) : Dwelling(residents)
{}fun main()
{
println("Hello, world!")
}
他應該會出現以下的錯誤(Class ‘SquareCabin’ is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String define in Dwelling):
[覆寫父類別變數 — override]
這是在提醒我們SquareCabin
這個類別繼承了Dwelling
,但是卻沒有實作裡面相關的抽像成員 — buildingMaterial、capacity,抽象成員必須在子類別中被實作,我們在SquareCabin
的內部加入抽象成員的值及抽象方法的實作:
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood" /// 屋主提供
override val capacity = 6 /// 屋主提供
}
這邊我們又看到一個新的關鍵字 — override:
override 是指覆載,意思是這個變數是繼承自父類別,並且獲得一個重新指定的數值,而只要是抽象成員,不管是函式或是變數,子類別都必須要覆載override這個成員
現在我們就可以在主程式中建立類別的實體物件:
fun main() {
val squareCabin = SquareCabin(6)
println("\nSquare Cabin\n============")
println("Capacity: ${squareCabin.capacity}")
println("Material: ${squareCabin.buildingMaterial}")
println("Has room? ${squareCabin.hasRoom()}")
}
val squareCabin = SquareCabin(6)
這行是建立一個方艙物件變數提供給6人居住,並且在一開始就有6個人租賃此方艙
當創建一個實體物件之後,我們回憶一下如何println 帶有變數的字串,Kotlin提供一個方便的語法,在字串內使用$符號之後加上大括號,大括號內放置變數名稱 — ” ${
變數 }
“,就可以同時把變數print出來,這對於學過Java的人真是一大方便啊,過去在Java可是不能這樣使用的啊
而上面的程式值得注意的一點是,在SquareCabin類別裡並沒有定義hasRoom這個函式,而是定義在Dwelling裡,不過因為繼承的關係,可以被所有SquareCabin 物件呼叫,現在執行你的程式,他應該會跑出下列的結果:
接著我們來定義另一個一樣是繼承Dwelling的類別 — RoundHut ,是用稻草製作而成,能夠容納的人數為4個:
class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw" /// 屋主提供
override val capacity = 4 /// 屋主提供
}
在主程式部分我們一樣加入RoundHut的物件:
fun main() {
val squareCabin = SquareCabin(6)
println("\nSquare Cabin\n============")
println("Capacity: ${squareCabin.capacity}")
println("Material: ${squareCabin.buildingMaterial}")
println("Has room? ${squareCabin.hasRoom()}") val roundHut = RoundHut(6)
println("\nSquare Cabin\n============")
println("Capacity: ${roundHut.capacity}")
println("Material: ${roundHut.buildingMaterial}")
println("Has room? ${roundHut.hasRoom()}"
}
[with 語法]
有沒有發現我們每次在println使用squareCabin
都會一直重複squareCabin
的變數名稱,這邊我們來學習一個Kotlin簡化程式的語法 — with
,使用方法如下:
with(變數名稱){....}
以上的程式以with為開頭,後面放置特定的物件變數在小括號內,告訴電腦with區塊中所有的運算都是以小括號內的物件變數的屬性成員、方法成員來操作
因此我們的主程式可以修改成如下:
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(6) /// 大括號內均以squareCabin為操作目標
with(squareCabin){
println("\nSquare Cabin\n============")
/// 獲取成員時可以省略物件名稱
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
/// 使用成員時可以省略物件名稱
println("Has room? ${hasRoom()}")
} /// 大括號內均以roundHut為操作目標
with(roundHut){
println("\nSquare Cabin\n============")
/// 獲取成員時可以省略物件名稱
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
/// 使用成員時可以省略物件名稱
println("Has room? ${.hasRoom()}"
}
}
目前我們定義了三個類別,一個是抽象類別,另外兩個是實作抽象類別的類別 — RoundHut
、SquareCabin
:
[關鍵字 open ]
在Kotlin 中我們稱
RoundHut
、SquareCabin
為終端類別(final class),代表他是一個最後的類別,無法被繼承的
而除了抽象類別以外的類別預設都是final class,因此如果要繼續繼承RoundHut
類別,需要在定義類別前加上open 這個關鍵字
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
因為B屋主告訴我們他要出租的兩種類型房屋,有一些共同的性質,所以現在我們可以想像有一個圓塔是仿照圓形房屋來製作,不過他使用的材質是Stone,容納人數為一層樓4個人,而且有2層樓的圓塔要出租,因此我們要創建一個類別RoundTower
繼承至RoundHut
而且因為塔是高樓層的,我們在定義塔的類別時幫他多新增一個參數 — floors,意指有幾層樓,所以圓塔居住人數 — capacity為 (4 * floors)人:
class RoundTower(residents: Int, val floors: Int = 2) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
}
我們看到RoundTower第二個參數跟第一個參數的寫法不同,第二個參數預設是一個定值val,值為2,也因為他有預設的數值,因此我們在創建實體物件時有兩種創建方式,可以選擇要不要設定第二個參數:
val roundTower = RoundTower(4) /// 方式ㄧ :人數4,容納人數 4 * 2
val roundTower = RoundTower(4, 4) /// 方式ㄧ :人數4,容納人數 4 * 4
**你也可以試試看不加上 open
關鍵字在RoundHut
會有什麼樣的錯誤發生,他應該會顯示如下的錯誤訊息,說明RoundHut
是final class 無法被繼承:
現在你的全部程式應該會是這樣:
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(residents: Int, val floors: Int = 2) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
}fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(6)
val roundTower = RoundTower(4, 4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
[覆寫父類別函式 — override]
在租房子的時候,除了材質及居住人數,我們還會很在意房間的大小,而房間的大小是每個房子都會有的特性,所以我們應該要將房間大小這個屬性在Dwelling就規劃好,並且因為每種形狀的房子面積計算方式都不一樣,我們將會新增floorArea()
這個函式來計算面積大小
你可以試著自己新增抽象屬性成員 — area、抽象方法成員 — floorArea,看看能不能自己寫出來:
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
abstract val area : Double fun hasRoom(): Boolean {
return residents < capacity
} abstract fun floorArea() : Double
}
Double是一種變數類型,和String、Int、Float 一樣,Double 是雙精度的浮點數(比Float可以容納更多的小數點位數)
而這邊我們定義一個抽象函式,抽象函式在抽象類別裡可以先不用被實作,因此你可以特別注意到抽象函式後方並沒有寫上大括號,這部分在寫程式時常常會忘記,要特別小心!!
接著我們來試著實現floorArea()在另外三個類別,在SquareCabin
加入:
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
override var area = 0.0 override fun floorArea(): Double {
area = length * length
return area
}
}
這邊有發現length
是一個尚未被定義的變數嗎??因為房屋的大小在一開始就確定,之後也無法更改,因此這個類別我們將它放到類別的參數中,當成第二個參數,並設定成 val
:
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents)
這邊注意因為length 是Double型態,所以在給值的時候要給小數型態的數值,在主程式就可以這樣使用:
val squareCabin = SquareCabin(6, 50.0)
println("Floor area: ${floorArea()}") /// 寫在with(squareCabin)裡面
接著實現floorArea()在RoundHut
:
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
override var area = 0.0 override fun floorArea(): Double {
area = PI * radius * radius
return area
}
}
這邊我們看到有兩個變數: PI 、radius,radius
一樣是在一開始就確定,之後也無法更改,我們將它放到類別的參數中,當成第二個參數,並設定成 val
:
open class RoundHut(residents: Int, val radius: Double) : Dwelling(residents)
PI 就是大家有印象數學中的PI 圓周率,在Kotlin 內有個負責數學計算的函式庫,這邊我們必須把它import 進來專案裡:
import kotlin.math.PI
在主程式修改RoundHut的參數,一樣注意radius 是Double型態,參數必須是小數型態,就可以先執行一下程式囉:
val roundHut = RoundHut(6, 3.0)
println("Floor area: ${floorArea()}") /// 寫在with(roundHut)裡面
如果你的程式沒有其他語法上的錯誤,那執行完應該會剩下唯一一個錯誤(No value passed for parameter ‘radius’):
這是在提醒我們在RoundHut少了一個參數 — radius,因為RoundTower是繼承自RoundHut,他必須要符合他的父類別的所有參數,因此我們加入radius: Double
到RoundTower,也將這個參數放置RoundHut的類別參數裡:
class RoundTower(residents: Int, radius: Double, val floors: Int = 2) : RoundHut(residents, radius) {
在主程式創建roundTower的時候給三個參數值:
val roundTower = RoundTower(4, 15.5, 3)
println("Floor area: ${floorArea()}") /// 寫在with(roundTower)裡面
因為RoundTower的floorArea跟RoundHut的算法不同,所以我們要來改寫繼承自RoundHut的floorArea函式,徒法煉鋼半徑*半徑 * PI * 樓層數:
override fun floorArea(): Double {
return PI * radius * radius * floors
}
你也透過super 這個關鍵字使用父類別的floorArea()
先算出單一層的面積,在直接乘上樓層數:
override fun floorArea(): Double {
return super.floorArea() * floors
}
現在我們的程式已經完成,你的程式應該會是長這個樣子:
import kotlin.math.PI
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
abstract val area : Double
fun hasRoom(): Boolean {
return residents < capacity
}
abstract fun floorArea() : Double
}class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
override var area = 0.0
override fun floorArea(): Double {
area = length * length
return area
}
}open class RoundHut(residents: Int, val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
override var area = 0.0
override fun floorArea(): Double {
area = PI * radius * radius
return area
}
}class RoundTower(residents: Int, radius: Double, val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
override fun floorArea(): Double {
return PI * radius * radius * floors
}
}
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(6, 3.0)
val roundTower = RoundTower(4, 15.5, 4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
}
執行完的結果如下:
這次用範例講解了物件導向的繼承概念,我們應該學會了以下的觀念:
●創造抽象類別,並且知道如何繼承類別
●創造抽象成員,讓子類別去實作
●override 關鍵字:重寫父類別屬性及函式
●open 關鍵字:將類別變成可被繼承的
●private 關鍵字:限制變數的可見性(是否能被類別以外的程式使用等等....)
●with 語法:with(變數名稱){....}
●Kotlin 數學函式庫:kotlin.math.PI
希望大家覺得物件導向是個有趣的主題,以前我在學習程式設計時,最喜歡的就是物件導向部分了,我認為物件導向是一個能夠訓練自己創造力、邏輯能力與整體設計架構的練習,因此在工作上我也儘量強迫自己一定要基於物件導向的概念,才能夠寫出有邏輯,架構不混亂的程式!
下面一樣有幾個QA給大家測試看看今天的學習成效囉!一起加油!
[QA]
● 文章一開始的車子和休旅車,哪個是父類別?哪個是子類別?
● Kotlin的抽象類別比較適合用來設計車子還是休旅車??
● 抽象成員會以什麼關鍵字來表示??
● 類別如果沒有加上open 會有什麼狀況發生?
● with 語法的目的是用來做什麼??
● 是否能夠想出其他適合用物件導向描述的例子,快來分享給我知道吧!
PS. 這篇文章還有一個小功能番外篇,在Dwelling 這個抽象類別新增一個函式getRoom()
來判斷是否能有空間入住,如果足夠空間,residents就+1
算是個小彩蛋,就給看到文章最後認真的大家,程式碼在Github上,千萬不要錯過!!
喜歡我的文章的人也記得幫我按個拍手、分享,覺得很不錯的可以幫我拍個50下!
也要快點追蹤我的 FB粉絲專頁 — 飛比尋常的程式設計世界 ,不會太頻繁出現在你的塗鴉牆騷擾你,好文章生產需要一點時間,有錯誤或想討論的都歡迎留言給我唷!那就下次見拉!