Kotlin 知識(shí)梳理系列文章
Kotlin 知識(shí)梳理(1) - Kotlin 基礎(chǔ)
Kotlin 知識(shí)梳理(2) - 函數(shù)的定義與調(diào)用
Kotlin 知識(shí)梳理(3) - 類芒涡、對(duì)象和接口
Kotlin 知識(shí)梳理(4) - 數(shù)據(jù)類柴灯、類委托 及 object 關(guān)鍵字
Kotlin 知識(shí)梳理(5) - lambda 表達(dá)式和成員引用
Kotlin 知識(shí)梳理(6) - Kotlin 的可空性
Kotlin 知識(shí)梳理(7) - Kotlin 的類型系統(tǒng)
Kotlin 知識(shí)梳理(8) - 運(yùn)算符重載及其他約定
Kotlin 知識(shí)梳理(9) - 委托屬性
Kotlin 知識(shí)梳理(10) - 高階函數(shù):Lambda 作為形參或返回值
Kotlin 知識(shí)梳理(11) - 內(nèi)聯(lián)函數(shù)
Kotlin 知識(shí)梳理(12) - 泛型類型參數(shù)
一、本文概要
本文是對(duì)<<Kotlin in Action>>
的學(xué)習(xí)筆記费尽,如果需要運(yùn)行相應(yīng)的代碼可以訪問在線環(huán)境 try.kotlinlang.org赠群,這部分的思維導(dǎo)圖為:
二、定義類繼承結(jié)構(gòu)
2.1 Kotlin 中的接口
Kotlin
的接口可以包含以下兩種類型的方法:
- 簡單的抽象方法
- 包含默認(rèn)實(shí)現(xiàn)的抽象方法
簡單接口
- 一個(gè)簡單的
Kotlin
接口使用 interface 關(guān)鍵字來聲明旱幼,所有實(shí)現(xiàn)這個(gè)接口的非抽象類都需要實(shí)現(xiàn)接口中定義的抽象方法查描。 -
Kotlin
在類名后面使用 冒號(hào) 代替了Java
中的extends
和implements
關(guān)鍵字,一個(gè)類可以實(shí)現(xiàn)多個(gè)接口柏卤,但是只能繼承一個(gè)類冬三。 -
override
修飾符用來標(biāo)注被重寫的父類或者接口的方法和屬性,并且是 強(qiáng)制要求 的缘缚。
下面的例子中定義了一個(gè)接口勾笆,并演示了如何實(shí)現(xiàn)該接口,以及接口中定義的抽象方法:
在接口中定義方法的默認(rèn)實(shí)現(xiàn)
我們可以給接口的方法提供一個(gè)默認(rèn)的實(shí)現(xiàn)桥滨,定義的方法和普通函數(shù)相同窝爪。
如果一個(gè)類實(shí)現(xiàn)了兩個(gè)接口,而這兩個(gè)接口定義了相同的方法齐媒,并且都提供了該方法的默認(rèn)實(shí)現(xiàn)蒲每,那么該類必須顯示實(shí)現(xiàn)該方法,否則會(huì)在編譯時(shí)報(bào)錯(cuò):
調(diào)用繼承自接口的方法的實(shí)現(xiàn)
當(dāng)需要調(diào)用一個(gè)繼承的實(shí)現(xiàn)喻括,可以使用與Java
相同的關(guān)鍵字 super邀杏,并在后面的尖括號(hào)中指明父類的名字,最后是調(diào)用的方法名:
2.2 訪問性修飾符:open唬血、final望蜡、abstract
一般類
- 在
Kotlin
中,類和方法默認(rèn)都是final
的刁品,如果想允許創(chuàng)建一個(gè)類的子類,需要使用open
修飾符來標(biāo)示這個(gè)類浩姥,此外還需要給每一個(gè)允許被重寫的屬性或方法添加open
修飾符挑随。 - 如果重寫了一個(gè)基類的成員,重寫了的函數(shù)同樣默認(rèn)是
open
的勒叠,如果想改變這一行為兜挨,可以顯示地將重寫的成員標(biāo)注為final
。
抽象類
我們可以將一個(gè)類聲明為abstract
眯分,這種類不能被實(shí)例化拌汇,一個(gè)從抽象類通常包含一些沒有實(shí)現(xiàn)并且必須在子類重寫的抽象成員:
- 抽象類中的抽象函數(shù):沒有函數(shù)體就默認(rèn)是
abstract
的,不一定要加上關(guān)鍵字弊决,其訪問性始終是open
的噪舀。 - 抽象類中的非抽象函數(shù):默認(rèn)是
final
的魁淳,如果需要重寫,那么需要加上open
修飾符与倡。
小結(jié)
open
界逛、final
和abstract
這三個(gè)訪問修飾符都 只適用于類,不能用在接口 當(dāng)中:
-
open
:用于聲明一個(gè)類可以被繼承纺座,或者方法可以被子類重寫息拜。 -
final
:不允許類被繼承,或者不允許方法被重寫净响。 -
abstract
:聲明抽象類少欺,或者抽象類中的抽象方法。
當(dāng)我們需要重寫方法時(shí)馋贤,必須加上override
修飾符赞别。
2.3 可見性修飾符
Kotlin
的可見性修飾符包括以下四種:
修飾符 | 類成員 | 頂層聲明 |
---|---|---|
public | 所有地方可見 | 所有地方可見 |
internal | 模塊中可見 | 模塊中可見 |
protected | 子類中可見 | --- |
private | 類中可見 | 文件中可見 |
Java
和Kotlin
在可見性上的區(qū)別包括以下幾點(diǎn):
- 在
Java
中默認(rèn)的可見性是包私有的,而在Kotlin
中掸掸,默認(rèn)的可見性是public
的氯庆。Kotlin
用internal
作為包可見的替代方案,它表示“只在模塊內(nèi)部可見”扰付。 -
Kotlin
允許在頂層聲明中使用private
可見性堤撵,包括類、函數(shù)和屬性羽莺,這些聲明就只在聲明它們的文件中可見实昨,這是隱藏子系統(tǒng)實(shí)現(xiàn)細(xì)節(jié)的非常有用的方式。 - 類的擴(kuò)展函數(shù)不能訪問它的
private
和protected
成員盐固。 -
Kotlin
中荒给,一個(gè)外部類不能看到其內(nèi)部類中的private
成員。
2.4 內(nèi)部類和嵌套類
在Kotlin
中刁卜,如果我們像Java
一樣志电,在一個(gè)類的內(nèi)部定義一個(gè)類,那么它并不是一個(gè) 內(nèi)部類蛔趴,而是 嵌套類挑辆,區(qū)別在于嵌套類不會(huì)持有外部類的引用,也就是說它實(shí)際上是一個(gè)靜態(tài)內(nèi)部類:
如果要把它嵌套類變成一個(gè) 內(nèi)部類 來持有一個(gè)外部類的引用的話需要使用
inner
修飾符孝情,并且訪問外部類時(shí)鱼蝉,需要使用this@{外部類名}
的方式。
2.5 密封類:定義受限的類繼承結(jié)構(gòu)
之前在介紹when
表達(dá)式的時(shí)候箫荡,我們用了一個(gè)表達(dá)式的例子魁亦,Num
和Sum
繼承于基類Expr
,分別表達(dá)數(shù)字和兩個(gè)表達(dá)式之和羔挡,而對(duì)于不屬于Expr
的子類洁奈,我們需要提供額外的else
操作符间唉。
假如我們給
Expr
添加了一個(gè)新的子類,編譯器并不能發(fā)現(xiàn)有地方改變了睬魂。如果忘記了添加一個(gè)新分支终吼,就會(huì)選擇默認(rèn)的選項(xiàng),這有可能導(dǎo)致潛在的bug
氯哮。
Kotlin
為這個(gè)問題提供了一個(gè)解決方案:sealed
類际跪。為父類添加一個(gè)sealed
修飾符,對(duì)可能創(chuàng)建的子類做出嚴(yán)格的限制喉钢,所有的直接子類必須嵌套在父類中姆打。之前的例子修改如下:
這時(shí)候,我們?cè)?code>when表達(dá)式中已經(jīng)處理了所有
Expr
的子類肠虽,就不再需要提供默認(rèn)的分支幔戏,假如這時(shí)候我們給Expr
添加一個(gè)新的子類Multi
,但是不修改when
中的邏輯税课,那么就會(huì)導(dǎo)致編譯失斚醒印:提示的信息為:
在這種情況下,
Expr
類有一個(gè)只能在類內(nèi)部調(diào)用的private
構(gòu)造方法韩玩,你也不能聲明一個(gè)sealed
接口垒玲,因?yàn)槿绻@樣做,Kotlin
編譯器不能保證任何人都不能在Java
代碼中實(shí)現(xiàn)這個(gè)接口找颓。
三合愈、構(gòu)造方法
在Java
中,一個(gè)類可以聲明一個(gè)或多個(gè)構(gòu)造方法击狮,Kotlin
則將構(gòu)造方法分為兩類:
- 主構(gòu)造方法:主要而簡潔的初始化類的方法佛析,并且在 類體外部聲明。
- 從構(gòu)造方法:在 類體內(nèi)部聲明彪蓬。
3.1 初始化類:主構(gòu)造方法和初始化語句塊
假設(shè)寸莫,我們需要定義一個(gè)包含只讀nickname
屬性的User
類,最簡單的方式為:
上面這段被括號(hào)圍起來的語句塊就叫做 主構(gòu)造方法档冬,它有兩個(gè)目的:
- 表明構(gòu)造方法的參數(shù)膘茎。
- 定義使用這些參數(shù)初始化的屬性,也就是
nikename
捣郊。
用于完成上面這兩個(gè)功能的最明確的代碼如下所示:
-
constructor
:用來開始一個(gè)主構(gòu)造方法和從構(gòu)造方法的聲明辽狈。 -
init
:引入一個(gè)初始化塊語句慈参,這種語句塊包含了在類被創(chuàng)建時(shí)執(zhí)行的代碼呛牲,并會(huì)與主構(gòu)造方法一起使用,因?yàn)橹鳂?gòu)造方法有語法限制驮配,這就是為什么要使用初始化語句塊的原因娘扩。
在上面的例子中有幾個(gè)可以簡化的點(diǎn):
- 放在初始化語句塊的語句可以和
nikename
的聲明結(jié)合着茸,因此可以去掉init
語句。 - 如果主構(gòu)造方法沒有注解或可見性修飾符琐旁,可以取消
constructor
關(guān)鍵字涮阔。 - 如果屬性用相應(yīng)的構(gòu)造方法參數(shù)來初始化,代碼可以通過把
val
關(guān)鍵字加在參數(shù)前的方式來進(jìn)行簡化灰殴。
經(jīng)過了以上三點(diǎn)敬特,就會(huì)得到最前面簡化后的結(jié)果。
對(duì)于構(gòu)造方法牺陶,也可以采用之前在 Kotlin 知識(shí)梳理(2) - 函數(shù)的定義與調(diào)用 中介紹的 命名參數(shù) 和 默認(rèn)參數(shù)值 的技巧伟阔,如果所有的構(gòu)造方法都有默認(rèn)值,編譯器會(huì)生成一個(gè)額外的不帶參數(shù)的構(gòu)造方法來使用所有的默認(rèn)值掰伸。
在Java
中皱炉,如果父類定義了一個(gè)構(gòu)造方法,那么在子類的構(gòu)造方法中狮鸭,必須要通過super
方法初始化父類合搅,例如:
//父類。
public class User {
private String nikeName;
User(String nikeName) {
this.nikeName = nikeName;
}
}
//子類歧蕉。
public class TwitterUser extends User {
public TwitterUser(String nikeName) {
super(nikeName);
}
}
而在Kotlin
中灾部,可以通過在基類列表的父類引用中提供父類構(gòu)造方法參數(shù)的方式來做到這一點(diǎn):
假如一個(gè)類沒有聲明任何的構(gòu)造方法,將會(huì)生成一個(gè)不做任何事的默認(rèn)構(gòu)造方法廊谓,如果有子類繼承了它梳猪,那么必須顯示地調(diào)用父類的構(gòu)造方法,即使它沒有任何的參數(shù)蒸痹。
如果想要確保你的類不被其它代碼實(shí)例化春弥,必須把構(gòu)造方法標(biāo)記為
private
,我們對(duì)上面的例子進(jìn)行修改:
報(bào)錯(cuò)的原因?yàn)椋?/p>
在大多數(shù)真實(shí)的場(chǎng)景中叠荠,類的構(gòu)造方法是非常簡明的:它要么沒有參數(shù)或者直接與參數(shù)對(duì)應(yīng)的屬性關(guān)聯(lián)匿沛,這就是為了
Kotlin
有為主構(gòu)造方法設(shè)計(jì)的簡潔的語法。
3.2 用不同的方式來初始化父類
大多數(shù)在Java
中需要重載構(gòu)造方法的場(chǎng)景都被Kotlin
支持命名參數(shù)和參數(shù)默認(rèn)值的語法所覆蓋了榛鼎。
定義從構(gòu)造方法:constructor
而當(dāng)我們需要擴(kuò)展一個(gè)框架來提供多個(gè)構(gòu)造方法逃呼,以便于通過不同的方式來初始化類的時(shí)候,就會(huì)需要用到從構(gòu)造方法者娱,從構(gòu)造方式使用constructor
方法引出抡笼,例如下面的代碼:
運(yùn)行結(jié)果為:
子類調(diào)用父類的從構(gòu)造方法:super
如果想要擴(kuò)展這個(gè)類,可以聲明同樣的構(gòu)造方法黄鳍,并使用super
關(guān)鍵字調(diào)用對(duì)應(yīng)的父類構(gòu)造方法:
運(yùn)行結(jié)果為:
子類調(diào)用自己的另一個(gè)構(gòu)造方法:this
如果想要從一個(gè)構(gòu)造方法中推姻,調(diào)用你自己的類的另一個(gè)構(gòu)造方法,那么可以使用this
關(guān)鍵字:
運(yùn)行結(jié)果為:
需要注意:如果類沒有主構(gòu)造方法框沟,那么每個(gè)從構(gòu)造方法必須初始化基類(通過
super
關(guān)鍵字)或者委托給另一個(gè)這樣做了的構(gòu)造方法(通過this
關(guān)鍵字)藏古,也就是說增炭,每個(gè)從構(gòu)造方法必須以一個(gè)朝外的箭頭開始,并且結(jié)束于任意一個(gè)基類構(gòu)造方法拧晕,就像上面例子中Button
的帶有兩個(gè)參數(shù)的從構(gòu)造方法所做的那樣隙姿。
3.3 實(shí)現(xiàn)在接口中聲明的屬性
在Kotlin
中,接口可以包含抽象屬性的聲明:
但是接口并沒有說明這個(gè)值應(yīng)該存儲(chǔ)到一個(gè)支持字段還是通過
getter
來獲取厂捞,接口本身并不包含任何狀態(tài)输玷,因此只有實(shí)現(xiàn)這個(gè)接口的類在需要的時(shí)候會(huì)存儲(chǔ)這個(gè)值。
下面是三個(gè)例子:
-
PrivateUser
:直接在主構(gòu)造方法中聲明了這個(gè)屬性靡馁,這個(gè)屬性實(shí)現(xiàn)了來自于User
的抽象屬性饲嗽,所以要標(biāo)記為override
。 -
SubscribingUser
:通過一個(gè)自定義的getter
實(shí)現(xiàn)奈嘿,這個(gè)屬性沒有一個(gè)支持字段來存儲(chǔ)它的值貌虾,它只有一個(gè)getter
在每次調(diào)用時(shí)從email
中得到昵稱。 -
FacebookUser
:在初始化時(shí)裙犹,將nickname
屬性與值關(guān)聯(lián)尽狠。
接口除了可以聲明抽象屬性外,還可以包含具有getter
和setter
的屬性叶圃,只要它們沒有引用一個(gè)支持字段(支持字段需要在接口中存儲(chǔ)狀態(tài)袄膏,而這是不允許的):
-
email
:必須在子類中重寫。 -
nickname
:有一個(gè)自定義的getter
掺冠,可以被子類繼承沉馆。
運(yùn)行結(jié)果為:
4.4 通過 getter 和 setter 訪問支持字段
現(xiàn)在,我們已經(jīng)學(xué)習(xí)了兩種屬性的用法:
- 存儲(chǔ)值的屬性
- 具有自定義訪問器在每次訪問時(shí)計(jì)算值的屬性
現(xiàn)在德崭,我們結(jié)合以上兩種斥黑,來實(shí)現(xiàn)一個(gè)既可以存儲(chǔ)值,又可以在值被訪問和修改時(shí)提供額外邏輯的屬性:
運(yùn)行結(jié)果為:
上面的
address
就是 有支持字段的屬性眉厨,它和 沒有支持字段的屬性 的區(qū)別在于:
- 如果顯示地引用或者使用默認(rèn)的訪問器實(shí)現(xiàn)锌奴,編譯器會(huì)為屬性生成支持字段。
- 如果你提供了一個(gè)自定義的訪問器實(shí)現(xiàn)并且沒有使用
field
憾股,支持字段就不會(huì)被呈現(xiàn)出來鹿蜀。
4.5 修改訪問器的可見性
訪問器的可見性默認(rèn)與屬性的可見性相同,但是如果需要可以通過在get
和set
關(guān)鍵字前放置可見性修飾符的方式來修改它服球,例如在下面的例子中茴恰,我們將setter
的可見性修改為private
:
運(yùn)行結(jié)果為:
更多文章,歡迎訪問我的 Android 知識(shí)梳理系列:
- Android 知識(shí)梳理目錄:http://www.reibang.com/p/fd82d18994ce
- 個(gè)人主頁:http://lizejun.cn
- 個(gè)人知識(shí)總結(jié)目錄:http://lizejun.cn/categories/