Kotlin使用心得(八):無形勝有形 — 抽象類別、方法與介面

Carter Chen
9 min readJun 19, 2018

--

“Mountain peak in Emerald Bay peeks through morning mist” by Etienne Desclides on Unsplash

前情提要

還記得以前剛學Java的時候,有點不了解abstract class與interface的用法
其實兩者「用起來」差不多,只是概念上有點不一樣:

abstract class的abstract方法比較偏向於:

既然你是一個繼承了abstract class的類別,那abstract class會用到的方法,你也應該要會用到

interface的方法比較偏向於:

你可以選擇要implement哪個interface,但一旦你implement了,你就得要用到interface中定義的方法

舉個例子:

  1. Soldier抽象類別

2. TankSkill介面

3. MedicSkill介面

4. SuperPrivate類別

可以看到SuperPrivate(超級士兵!)類別繼承了抽象類別Soldier,所以它是Soldier的一種,既然Soldierfight(),那SuperPrivate也要會fight()

SuperPrivate類別同時也實作了TankSkill介面與MedicSkill介面,所以必須實作對應的driveTank()heal()方法,但這兩個介面可以選擇是否要實作,可以在需要driveTank()技能的時候再實作TankSkill介面,需要heal()技能的時候再實作MedicSkill介面。

Kotlin中的abstract class與interface

宣告的方式和Java差不多,讓我們來創造一個抽象類別A與介面I,並賦予他們抽象方法:

嘗試讓一個類別繼承抽象類別A與實作介面I

class T : A(), I

Compiler碎碎念:

class ‘T’ must be declared abstract or implement abstract base class member public abstract fun functionA():Unit defined in …

class ‘T’ must be declared abstract or implement abstract base class member public abstract fun functionI():Unit defined in …

翻譯:阿你要override抽象方法啊怎麼沒override

所以override之後就沒事了:

和「以往的Java」不同的是,Kotlin的Interface function是可以有body的,而且也只有沒有body的function需要被實作。在以下這個例子,類別T雖然實作了介面I,但是不override interfaceFunctionWhichHasBody()這個方法也是OK的:

那為什麼說是「以往的Java」呢?因為在Java 8以後,只要對interface中定義的method加上default修飾符,就可以為它加上body,同時也不會去強迫實作的類別必須覆寫這個method。

抽象方法的open vs final

奇怪,我們在先前文章中提到,被覆寫的方法不是都要加open修飾符才能被覆寫嗎?但是到目前爲止好像我們不加open修飾符好像也跟compiler相安無事?

其實不管是abstract class或interface的abstract method,本身就是設計來讓其他類別實作、覆寫的,所以並不需要加open修飾符喔。

你要加也是可以辣:

interface I {

open fun functionI()

...
}

compiler悄悄話(提示代替警告):

Modifier ‘open’ is redundant for abstract interface members

翻譯:阿……那個,其實可以不用加open的喲,對於abstract interface來說是多餘的。

你如果不想要一個方法被覆寫,硬是要加上final的話:

interface I {

final fun functionI()

...
}

就會出現錯誤警告了

compiler:

Modifier ‘final’ is not applicable inside ‘interface’

翻譯:你不能指定interface的method為final!我很生氣!你既然不想被覆寫那還創造一個interface幹嘛?

介面屬性

讓我們創造另外一個介面P,並給它一個屬性:

interface P {

val property:String

}

然後讓類別T也實作P這個介面:

class T : A(), I, P {

override fun functionI() {}

override fun functionA() {}
}

嗶嗶嗶!compiler警告:

Class ‘T’ must be declared abstract or implement abstract member public abstract val property:String defined in …

翻譯:跟abstract method一樣,你這個property也是abstract的,所以你要嘛就實作(覆寫)這個property,不然,你就用abstract class來實作(這樣就不用覆寫這個property)

那要如何解決哩?

1. 用建構式賦值

雖然這邊也用了override,但由於在interface中的方法與屬性都是抽象的,所以原來的屬性也不需要加open就能override囉!

2. 實現屬性訪問器

也可以指定當呼叫類別Tproperty屬性時,直接返回預設的值

到這邊可能會突發奇想,如果在interface中直接對屬性進行初始化,可行嗎?

讓我們修改一下介面P

interface P {

val property: String = "property"

}

這時compiler又開始碎碎念了:

Property initializers are not allowed in interfaces

翻譯:你不能在介面中初始化屬性!門都沒有!

那我們換個方法,在實現訪問器的get()方法時,返回field看看:

interface P {

val property: String
get() = field

}

我真的覺得compiler HEN煩:

Property in an interface cannot have a backing field

翻譯:你在介面裡面也是不能用field啦,省省吧你

但換成class就可以

class P {

var property: String = "property"
get() = field

}

原來,interface和class不同的地方在於,interface不能儲存狀態,所以我歸納如下:

  1. 初始化後會有數值需要被儲存在記憶體中,interface不能儲存這些屬性的數值,所以屬性當然不能初始化。
  2. field代表的就是儲存的數值,既然interface不能儲存屬性數值,那field也就不能用。
  3. 為什麼用實作訪問器get()方法就可以?因為get()返回的數值並不是儲存在記憶體中的,而是我們事先指定好,當外部進行訪問時,就給他內定數值。

介面屬性真的是很龜毛的一種設計,但也因為龜毛,限制夠多,才夠嚴謹。我們已經知道了介面屬性要賦值的話,它不能被初始化、被訪問時不能返回field(也沒field可返回),我覺得這已經很過分了,但是更過份的是,它還強制一定要有返回值!可以看以下的例子,不指定值給property屬性:

interface P {

val property: String

fun printProperty(){
println(property)
}
}

這時候當有個類型去實作介面P,compile就會該該叫:

Property must be initialized

我們必須在實作介面P的類別中去初始化property這個屬性,因為在介面P中是預設要有property屬性的,不然printProperty()會呼叫到還沒有值的property屬性,而發生不可預期的結果。

我覺得雖然介面屬性使用限制較多,但也因為這個設計,可以讓我們更好地進行抽象化,也更容易地將職責分離開來。

介面的繼承

這邊我要偷懶一下,直接引用官網上的代碼

還算滿清楚的,繼承父介面的子介面會繼承「父介面的抽象屬性」,換句話說,父介面要求要有的屬性,子介面也一定會有,至於後續想怎麼覆寫,就看開發者的心情辣。

解決覆寫上的衝突

舉個例子,現在有兩個介面,MinerFarmer,它們同有一個名為「work()」的方法,有一個Career類別實作了這兩個介面,也順利的覆寫了work()方法:

奇怪,阿我到底是覆寫了誰的work()方法?
好,為了驗證到底是覆寫誰的,來call一下預設的work()方法,MinerFarmer兩介面的work()方法剛好可以印出不同的結果,我們可以好好來驗證一下:

class Career : Miner, Farmer{

override fun work() {
super.work()
}

}

compiler:

Many supertypes available, please specify the one you mean in angle brackets, e.g. ‘super<Foo>’

翻譯:我被你搞得好亂啊,你到底是想call哪一個work方法?

解決方式,在super後使用<類別>來標示,讓我們來call Miner介面的work()方法:

使用這種方式,你也可以一次call完全部介面的全部方法:

--

--