Kotlin使用心得(八):無形勝有形 — 抽象類別、方法與介面
前情提要
還記得以前剛學Java的時候,有點不了解abstract class與interface的用法
其實兩者「用起來」差不多,只是概念上有點不一樣:
abstract class的abstract方法比較偏向於:
既然你是一個繼承了abstract class的類別,那abstract class會用到的方法,你也應該要會用到
interface的方法比較偏向於:
你可以選擇要implement哪個interface,但一旦你implement了,你就得要用到interface中定義的方法
舉個例子:
Soldier
抽象類別
2. TankSkill
介面
3. MedicSkill
介面
4. SuperPrivate
類別
可以看到SuperPrivate
(超級士兵!)類別繼承了抽象類別Soldier
,所以它是Soldier
的一種,既然Soldier
會fight()
,那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. 實現屬性訪問器
也可以指定當呼叫類別T
的property
屬性時,直接返回預設的值
到這邊可能會突發奇想,如果在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不能儲存狀態,所以我歸納如下:
- 初始化後會有數值需要被儲存在記憶體中,interface不能儲存這些屬性的數值,所以屬性當然不能初始化。
field
代表的就是儲存的數值,既然interface不能儲存屬性數值,那field
也就不能用。- 為什麼用實作訪問器
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
屬性,而發生不可預期的結果。
我覺得雖然介面屬性使用限制較多,但也因為這個設計,可以讓我們更好地進行抽象化,也更容易地將職責分離開來。
介面的繼承
這邊我要偷懶一下,直接引用官網上的代碼:
還算滿清楚的,繼承父介面的子介面會繼承「父介面的抽象屬性」,換句話說,父介面要求要有的屬性,子介面也一定會有,至於後續想怎麼覆寫,就看開發者的心情辣。
解決覆寫上的衝突
舉個例子,現在有兩個介面,Miner
與Farmer
,它們同有一個名為「work()
」的方法,有一個Career
類別實作了這兩個介面,也順利的覆寫了work()
方法:
奇怪,阿我到底是覆寫了誰的work()
方法?
好,為了驗證到底是覆寫誰的,來call一下預設的work()
方法,Miner
與Farmer
兩介面的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完全部介面的全部方法: