Kotlin使用心得(七):繼承與覆寫

Carter Chen
10 min readJun 10, 2018

--

“Sliced onion, parsnip, carrots, and spices on a cutting board” by Webvilla on Unsplash

正文開始

為了更容易的了解繼承的機制,先設計兩個簡單的類別:AnimalTiger

Kotlin中的繼承,是使用「:」繼承:

class Tiger:Animal()

但這個時候compiler靠腰了,它說:

This type is final, so it cannot be inherited from

原來啊,在Kotlin中所有的東西預設都是final的,也就是說,預設都無法進行改變。我們如果要讓一個class是「可以被繼承的」,要在class前面加上「open」:

open class Animal

如此一來,就可以順利進行繼承了:

接下來,我們要為Animal類別增加一個「name」屬性,依照我們先前的經驗,最簡單的方式為:

這時候compiler靠腰了,因為:

open class Animal(val name: String)

一個類別在初始化時,如果它有父類別,一定會先call父類別的主要建構式(Primary Constructor)。從上述的代碼來看,我們已經讓Animal類別有了主要建構式,但它的子類別:Tiger,在初始化時,並沒有去call Animal的主要建構式,所以產生錯誤:

class Tiger : Animal()  
// Tiger在初始化時會call Animal不帶任何參數的預設建構式
// 但Animal已經使用了主要建構是來取代預設建構式,所以會產生錯誤

為了解決這個問題,我們只要改為繼承Animal的主要建構式,並將相對應的參數填入:

就可以編譯成功:

那如果我們想要在Tiger類別初始化時,動態指定屬性name的值呢?如下:

val tiger = Tiger("Leo")

那就改成下面這樣,直接將Tiger主要構造式中的「name」直接傳遞給Animal的主要建構式:

那如果,我們想讓子類別可以在初始化的時候,不透過父類別,直接將屬性初始化呢?可以進行以下的嘗試:

class Tiger(val name: String) : Animal(name)

上述的代碼代表的意思是,我今天初始化Tiger這個類別時,會先傳入一個數值作為Tiger的「name」屬性,然後,也將這個屬性傳遞給Tiger的父類別:Animal去初始化類別與Animal的「name」屬性,但是這樣子的話,不就父類別和子類別都有了一個重複的「name」屬性了嗎?那當我們去調用Tiger.name的時候,得到的到底是Tigername屬性,還是Animalname屬性呢?

還好compiler也有同樣的疑慮,所以身為我們的好朋友,它提出了一下的建議:

‘name’ hides member of supertype ‘Animal’ and needs ‘override’ modifier

它說,如果要用在初始化的同時就建立好類別屬性的方式,而子類別中又有屬性與父類別的屬性重複的話,就用override吧,這樣一來你call Tiger.name的時候,就會得到Tiger類別的name屬性,而不是Animal類別的name屬性了。

於是我們將Tiger的主要建構式調整一下:

class Tiger(override val name: String) : Animal(name)

Compiler:安捏母湯哦,還記得為什麼嗎?

‘name’ in ‘Animal’ is final and cannot be overridden

讓我們再鄭重的聲明一次,所有在Kotlin中的東西,預設都是final,所以幫它加上個open,從此就可以過著幸福快樂的日子了:

open class Animal(open val name: String)

大家有發現嗎?我們在設計一個class時,若不用去考慮一個class的繼承關係,只求快速的進行初始化,並順便建立好屬性時,以Animal類別為例,使用下列方式非常方便:

class Animal(val name: String)

但是如果遇到類別需要被繼承、類別屬性需要被繼承,那就必須在子類別中覆寫父類別的屬性;如果父類別的屬性需要被覆寫,那就必須將訪問的權限從final改為open;其實屬性一多,改來改去也是滿麻煩的。

而且在正常的繼承行為中,其實根本也不需要在子類別中去設定子類別特有的屬性,物件導向的精神本身就在於,一個類別中的屬性可以重複使用與沿用。

所以我們可以再改一下代碼,改成下面這樣:

讓建構式全部從在初始化時就順便建立好屬性的方式改為傳遞參數的方式,後續再決定要如何設定這些參數。可以很明顯的看到,當我們對Tiger進行初始化時:

  1. 會先傳一個參數「Leo」到Tiger類別的主要建構式
  2. Tiger類別將「Leo」參數傳遞給它的父類別Animal的主要建構式
  3. Animal類別從主要建構式中接收到「Leo」參數,並將參數設定為自己的name屬性

經由這樣的調整,我只能說,不但代碼量變少了,易讀性也提高了呢!

雖然Kotlin提供了我們許多更便捷的方法,但如果我們每次都只走捷徑,不退後一步去觀察全貌,有時候糖果也會變成毒藥。

先前我們已經為Animal類別與Tiger類別建立了可以傳遞一個參數的主要建構式。今天我們遇到了新需求,開發者希望能在實體化的同時,傳遞Animalnameage兩個參數,如下:

val animal = Animal("Kitty", 1)

於是我們將次要建構式加入Animal,以達成我們的需求:

接下來我們希望能像實體化Animal一樣,可以丟兩個參數給Tiger,並得到一個Tiger類別的實體,如下:

val tiger2 = Tiger("Kitty", 1)

於是我們對Tiger類別做了一些修改,因為要call父類別的主構造函數,我們使用super()看看:

class Tiger(name: String) : Animal(name) {
constructor(name: String, age: Int) : super(name, age)
}

結果!super()被compiler劃了底線,說:

Primary constructor call expected

意思是,Tiger的主構造函數沒有人call,所以我們將super()改成this看看:

class Tiger(name: String) : Animal(name) {
constructor(name: String, age: Int) : this(name)
}

主構造函數經由this被調用之後,警告果然消失了,但這樣的話,this調用的也就只有Tiger本身,並沒辦法調用到Animal類別的構造函數。

所以我們要想想辦法,將代碼改成可以呼叫Animal類別構造函數的樣子。

於是狠下心來,將Tiger類別與Animal類別的主構造函數給砍了:

class Tiger : Animal {
constructor(name: String) : super(name)
constructor(name: String, age: Int) : super(name, age)
}

運行看看,成功:

原來,主構造函數並不是必要的,可以用次構造函數來替代。

覆寫方法

我們接下來來探討一下如何覆寫方法。我們對現成的代碼中的Animal類別加入新的方法eat()

既然Animaleat()Tiger經過繼承後也是個(is-a)Animal,所以呼叫Tigereat()方法時預設是呼叫Animal類別的eat()方法,但是因為Tiger類別和其他的Animal類別不太一樣,它是肉食性的,我們想讓Tiger類別的eat()方法做和Animal類別的eat()方法有所區別,所以在Tiger類別中也加入了eat()方法:

fun eat() {
print("eat meat")
}

compiler:

‘eat’ hides member of supertype ‘Animal’ and needs ‘override’ modifier
翻譯:父類別中已經有同樣名稱的方法了啦,你可以用「override」來進行覆寫

馬上迅速改了一下:

override fun eat() {
print("eat meat")
}

compiler:

‘eat’ in Animal is final and cannot be overriden
翻譯:……(被打斷)

讓我們再鄭重的聲明一次,所有在Kotlin中的東西,預設都是final。所以呢?對!被覆寫的方法和類別一樣,都要加上「open」:

open fun eat() {
println("eat something")
}

順利執行:

代碼寫到這邊,有個瘋狂開發者想說,應該也會有基因突變的Tiger類別吧?應該也會有老虎是吃素的吧?於是他嘗試著新增了一個新的VegeterianTiger類別,並且順利的繼承了Tiger類別:

這時候原開發者震怒了,他認為是老虎,就該吃肉,而且我也只想讓牠吃肉,不管之後是什麼類別來繼承Tiger類別,關於吃的行為,就是只能吃肉。

於是他在Tiger類別的eat()方法前面加上「final」:

final override fun eat() {
print("eat meat")
}

瘋狂開發者這時候發現,compiler跟他靠腰:

‘eat’ in ‘Tiger’ is final and cannot be overridden
翻譯:登愣,eat()方法不能被覆寫了喔,科科

然後他就玻璃心碎滿地了。

覆寫屬性

現在我們要為Animal類型去定義它的種類,因此我們為它新增了一個名為「type」的屬性,然後在Tiger類型中去覆寫它:

已經不用再鄭重宣布了,一樣是把「open」與「override」放到該放的地方。

這邊直得注意的是,被覆寫的屬性如果是val,那可以被覆寫為valvar

override val type: String = "carnivore"

override var type: String = "carnivore"

也就是說,本來只能被get()的屬性,覆寫後也可以set();換句話說,一個一開始該屬性被設計為read-only的類別,他的子類別可以對該屬性進行覆寫,在子類別中的該屬性就可以被設定成可以write;

但本來如果是var的屬性,就不能被覆寫為val,本來能被get()set()的屬性,覆寫後還是可以被get()set();換句話說,一個一開始該屬性被設計為可讀可寫的類別,之後進行繼承的子類別中的該屬性,依然是可讀可寫。

我認為這種預設封閉,在需要的時候才開放的特質,相較於預設開放,在封閉後無法重新開放的設計來說,彈性大多了。

初始化順序

我們來了解一下,在Kotlin中當使用繼承時,程式執行的先後順序。以下範例來自於Kotlin官網:

從上面的代碼我們可以觀察得到,當一個類型被初始化時:

  1. 程式會先執行該類型的主要建構函數
  2. 執行該類型所繼承父類型的主要建構函數
  3. 依照順序執行父類型中的init區塊或屬性
  4. 依照順序執行該類型中的init區塊或屬性

--

--

Responses (1)