Kotlin 類(lèi)拜银、對(duì)象和接口(一)——定義類(lèi)繼承結(jié)構(gòu)
在 Java 中一個(gè)類(lèi)可以聲明一個(gè)或多個(gè)構(gòu)造方法殊鞭,Kotlin 也是類(lèi)似的,只是做出了一點(diǎn)修改:區(qū)分了主構(gòu)造方法 (通常是主要而間接的初始化類(lèi)的方法尼桶,并且在類(lèi)體外部聲明) 和從構(gòu)造方法(在類(lèi)體內(nèi)部聲明)操灿。同樣也允許在初始化語(yǔ)句塊中添加額外的初始化邏輯。
初始化類(lèi):主構(gòu)造方法和初始化語(yǔ)句塊
class User(val nickname: String)
通常來(lái)講泵督,類(lèi)的所有聲明都在花括號(hào)中趾盐,但上面這個(gè)類(lèi)沒(méi)有花括號(hào)而是只包含了聲明在括號(hào)中。這段被括號(hào)圍起來(lái)的語(yǔ)句塊就叫做主構(gòu)造方法。它主要有兩個(gè)目的:表明構(gòu)造方法的參數(shù)救鲤,以及定義使用那些參數(shù)初始化的屬性久窟。它的工作原理以及完成同樣事情的最明確的代碼如下:
// 帶一個(gè)參數(shù)的主構(gòu)造方法
class User constructor(_nickname: String) {
val nickname: String
// 初始化語(yǔ)句塊
init {
nickname = _nickname
}
}
在這個(gè)例子中,constructor
關(guān)鍵字用來(lái)開(kāi)始一個(gè)主構(gòu)造方法或從構(gòu)造方法的聲明本缠。init
關(guān)鍵字用來(lái)引入一個(gè)初始化語(yǔ)句塊斥扛。這種語(yǔ)句塊包含了在類(lèi)被創(chuàng)建時(shí)執(zhí)行的代碼,并會(huì)與主構(gòu)造方法一起使用丹锹。因?yàn)橹鳂?gòu)造方法有語(yǔ)法限制稀颁,不能包含初始化代碼,這就是為什么要使用初始化語(yǔ)句塊的原因楣黍。也可以在一個(gè)類(lèi)中聲明多個(gè)初始化語(yǔ)句塊匾灶。
構(gòu)造方法參數(shù) _nickname 中的下劃線用來(lái)區(qū)分屬性的名稱(chēng)和構(gòu)造方法參數(shù)的名字。另一個(gè)可選方案是使用同樣的名字租漂,通過(guò) this 來(lái)消除歧義阶女,就像 Java 中的常用做法一樣:this.nickname = nickname。
在這個(gè)例子中哩治,不需要把初始化代碼放在初始化語(yǔ)句塊中秃踩,因?yàn)樗梢耘c nickname 屬性的聲明結(jié)合。如果主構(gòu)造方法沒(méi)有注解或可見(jiàn)性修飾符锚扎,同樣可以去掉 constructor 關(guān)鍵字隔盛,代碼如下:
// 帶一個(gè)參數(shù)的主構(gòu)造方法
class User(_nickname: String) {
// 用參數(shù)來(lái)初始化屬性
val nickname = _nickname
}
這就是聲明同樣的類(lèi)的另一種方法丽已。墻面兩個(gè)例子在類(lèi)體中使用 val 關(guān)鍵字聲明了屬性,如果屬性用相應(yīng)的構(gòu)造方法參數(shù)來(lái)初始化炸庞,代碼可以通過(guò)把 val 關(guān)鍵字加在參數(shù)前的方式來(lái)進(jìn)行簡(jiǎn)化惯疙〈涿悖可以替換類(lèi)中的屬性定義:
// "val" 意味著相應(yīng)的屬性會(huì)用構(gòu)造方法的參數(shù)來(lái)初始化
class User(val nickname: String)
所有 User 類(lèi)的聲明都是等價(jià)的,但是最后一個(gè)使用了最簡(jiǎn)明的語(yǔ)法霉颠。
可以像函數(shù)一樣為構(gòu)造方法參數(shù)聲明一個(gè)默認(rèn)值:
// 為構(gòu)造方法參數(shù)提供一個(gè)默認(rèn)值
class User(val nickname: String, val isSubscribed: Boolean = true)
創(chuàng)建一個(gè)類(lèi)的實(shí)例对碌,只需要直接調(diào)用構(gòu)造方法,不需要 new 關(guān)鍵字:
// 為 isSubscribed 參數(shù)使用默認(rèn)值 “true”
val alice = User("Alice")
// 可以按照聲明順序?qū)懨魉械膮?shù)
val bob = User("Bob", false)
// 可以顯式的為某些構(gòu)造方法參數(shù)表明名稱(chēng)
val carol = User("Carol", isSubscribed = false)
注意
如果所有的構(gòu)造方法參數(shù)都有默認(rèn)值蒿偎,編譯器會(huì)生成一個(gè)額外的不帶參數(shù)的構(gòu)造方法來(lái)使用所有的默認(rèn)值朽们。這可以讓 Kotlin 使用庫(kù)時(shí)變得更簡(jiǎn)單,因?yàn)榭梢酝ㄟ^(guò)無(wú)參構(gòu)造方法來(lái)實(shí)例化類(lèi)诉位。
如果類(lèi)具有一個(gè)父類(lèi)骑脱,主構(gòu)造方法同樣需要初始化父類(lèi)〔钥罚可以通過(guò)在基類(lèi)列表的父類(lèi)引用中提供父類(lèi)構(gòu)造方法參數(shù)的方式來(lái)做到這一點(diǎn):
open class User(val nickname: String) {...}
class TwitterUser(nickname: String) : User(nickname) {...}
如果沒(méi)有給一個(gè)類(lèi)聲明任何的構(gòu)造方法叁丧,將會(huì)生成一個(gè)不做任何事情的默認(rèn)構(gòu)造方法:
// 將會(huì)生成一個(gè)不帶任何參數(shù)的默認(rèn)構(gòu)造方法
open class Button
如果繼承了 Button 類(lèi)并且沒(méi)有提供任何的構(gòu)造方法,必須顯式的調(diào)用父類(lèi)的構(gòu)造方法,即使它沒(méi)有任何的參數(shù):
class RadioButton: Button()
注意與接口的區(qū)別:接口沒(méi)有構(gòu)造方法拥娄,所以在實(shí)現(xiàn)一個(gè)接口的時(shí)候蚊锹,不需要在父類(lèi)型列表中它的名稱(chēng)后面加上括號(hào)。
如果想要確保類(lèi)不被其他代碼實(shí)例化稚瘾,必須把構(gòu)造方法標(biāo)記為 private牡昆。把主構(gòu)造方法標(biāo)記為 private 代碼如下:
// 這個(gè)類(lèi)有一個(gè) private 構(gòu)造方法
class Secretive private constructor() {}
因?yàn)?Secretive 類(lèi)只有一個(gè) private 的構(gòu)造方法,這個(gè)類(lèi)外部的代碼不能實(shí)例化它孟抗。
private 構(gòu)造方法的替代方案
在 Java 中迁杨,可以通過(guò)使用 private 構(gòu)造方法禁止實(shí)例化這個(gè)類(lèi)來(lái)表示一個(gè)更通用的意思:這個(gè)類(lèi)是一個(gè)靜態(tài)實(shí)用工具成員的容器或者是單例的。Kotlin 針對(duì)這種目的具有內(nèi)建的語(yǔ)言級(jí)別的功能凄硼∏π可以使用頂層函數(shù)作為靜態(tài)實(shí)用工具,要想表示單例摊沉,可以使用對(duì)象聲明狐史。
在大多數(shù)真實(shí)的場(chǎng)景中,類(lèi)的構(gòu)造方法是非常簡(jiǎn)明的:它要么沒(méi)有參數(shù)或者直接把參數(shù)與對(duì)應(yīng)的屬性關(guān)聯(lián)说墨。這就是為什么 Kotlin 有為主構(gòu)造方法設(shè)計(jì)的簡(jiǎn)潔的語(yǔ)法:在大多數(shù)的情況下都能很好地工作骏全。
構(gòu)造方法:用不同的方式來(lái)初始化父類(lèi)
通常來(lái)講,使用多個(gè)構(gòu)造方法的類(lèi)在 Kotlin 代碼中不如在 Java 中常見(jiàn)尼斧。大多數(shù)在 Java 中需要重載構(gòu)造方法的場(chǎng)景都被 Kotlin 支持參數(shù)默認(rèn)值和參數(shù)命名的語(yǔ)法涵蓋了姜贡。
Tips
不要聲明多個(gè)從構(gòu)造方法來(lái)重載和提供參數(shù)的默認(rèn)值。取而代之的是棺棵,應(yīng)該直接標(biāo)明默認(rèn)值楼咳。
但是還是會(huì)有需要多個(gè)構(gòu)造方法的情景。最常見(jiàn)的一種就來(lái)自于當(dāng)需要擴(kuò)展一個(gè)框架來(lái)提供多個(gè)構(gòu)造方法烛恤,以便于通過(guò)不同的方式來(lái)初始化類(lèi)的時(shí)候母怜。如一個(gè)在 Java 中聲明的具有兩個(gè)構(gòu)造方法的類(lèi),Kotlin 中相似的聲明如下:
open class View {
constructor(ctx: Context) {
// some code
}
constructor(ctx: Context, attr: AttributeSet) {
// some code
}
}
這個(gè)類(lèi)沒(méi)有聲明一個(gè)主構(gòu)造方法(因?yàn)轭?lèi)頭部的類(lèi)名后面并沒(méi)有括號(hào))缚柏,但是它聲明了兩個(gè)從構(gòu)造方法苹熏。從構(gòu)造方法使用 constructor 關(guān)鍵字引出。只要需要們可以聲明任意多個(gè)從構(gòu)造方法币喧。
如果想擴(kuò)展這個(gè)類(lèi)轨域,可以聲明同樣的構(gòu)造方法:
class MyButton: View {
constructor(ctx: Context) : super(ctx) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
這里定義了兩個(gè)構(gòu)造方法,他們都是用 super() 關(guān)鍵字調(diào)用了對(duì)應(yīng)的父類(lèi)構(gòu)造方法杀餐。就像在 Java 中一樣干发,也可以使用 this() 關(guān)鍵字,從一個(gè)構(gòu)造方法中調(diào)用自己類(lèi)中的另一個(gè)構(gòu)造方法怜浅,如下:
class MyButton: View {
// 委托給這個(gè)類(lèi)的另一個(gè)構(gòu)造方法
constructor(ctx: Context) : this(ctx, MY_STYLE) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
可以修改 MyButton 類(lèi)使得一個(gè)構(gòu)造方法委托給同一個(gè)類(lèi)的另一個(gè)構(gòu)造方法(使用 this)铐然,為參數(shù)傳入默認(rèn)值蔬崩。如果類(lèi)沒(méi)有主構(gòu)造方法,那么每個(gè)從構(gòu)造方法必須初始化基類(lèi)或者委托給另一個(gè)這樣做了的構(gòu)造方法搀暑,每個(gè)構(gòu)造方法必須以一個(gè)朝外的箭頭開(kāi)始并且結(jié)束于任意一個(gè)基類(lèi)構(gòu)造方法沥阳。
實(shí)現(xiàn)在接口中聲明的屬性
在 Kotlin 中,接口可以包含抽象屬性聲明自点。如下:
interface User {
val nickname: String
}
這就意味著實(shí)現(xiàn) User 接口的類(lèi)需要提供一個(gè)取得 nickname 值的方式桐罕。接口并沒(méi)有說(shuō)明這個(gè)值應(yīng)該存儲(chǔ)到一個(gè)支持字段還是通過(guò) getter 來(lái)獲取。接口本身并不包含任何狀態(tài)桂敛,因此只是實(shí)現(xiàn)這個(gè)接口的類(lèi)在需要的情況下會(huì)存儲(chǔ)這個(gè)值功炮。
// 代碼清單 2.1 實(shí)現(xiàn)一個(gè)接口屬性
class PrivateUser(override val nickname: String) : User
class SubscriberingUser(val email: String) : User {
override val nickname: String
// 自定義 getter
get() = email.substringBefore('@')
}
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId)
}
對(duì)于 PrivateUser 來(lái)說(shuō),使用了簡(jiǎn)潔的語(yǔ)法直接在主構(gòu)造方法中聲明了一個(gè)屬性术唬。這個(gè)屬性實(shí)現(xiàn)了來(lái)自 User 的抽象屬性薪伏,所以將其標(biāo)記為 override。
對(duì)于 SubscribingUser 來(lái)說(shuō)粗仓,nickname 屬性通過(guò)一個(gè)自定義 getter 實(shí)現(xiàn)嫁怀,這個(gè)屬性沒(méi)有一個(gè)支持字段來(lái)存儲(chǔ)它的值,它只有一個(gè) getter 在每次調(diào)用時(shí)從 email 中得到昵稱(chēng)借浊。
對(duì)于 FacebookUser 來(lái)說(shuō)塘淑,在初始化時(shí)將 nickname 屬性與值關(guān)聯(lián)。使用了被認(rèn)為可以通過(guò)賬號(hào) ID 返回 Facebook 用戶(hù)名稱(chēng)的 getFacebookName 函數(shù)蚂斤。
除了抽象屬性聲明外存捺,接口還可以包含具有 getter 和 setter 的屬性,只要它們沒(méi)有應(yīng)用一個(gè)支持字段(支持字段需要在接口中存儲(chǔ)狀態(tài)曙蒸,而這是不允許的)捌治。
通過(guò) getter 或 setter 訪問(wèn)支持字段
前面介紹了屬性的兩種類(lèi)型:存儲(chǔ)值的屬性和具有自定義訪問(wèn)器在每次訪問(wèn)時(shí)計(jì)算值的屬性。若想要結(jié)合這兩種來(lái)實(shí)現(xiàn)一個(gè)既可以存儲(chǔ)值又可以在值被訪問(wèn)和修改時(shí)提供額外邏輯的屬性逸爵,就需要能夠從屬性的訪問(wèn)其中訪問(wèn)它的支持字段具滴。
假設(shè)想在任何對(duì)存儲(chǔ)在屬性中的數(shù)據(jù)進(jìn)行修改時(shí)輸出日志凹嘲,聲明了一個(gè)可變屬性并且在每次 setter 訪問(wèn)時(shí)執(zhí)行額外的代碼:
// 代碼清單 2.2 在 setter 中訪問(wèn)支持字段
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent())// 讀取支持字段的值
// 更新支持字段的值
field = value
}
}
可以像平常一樣通過(guò)使用 user.address = "new value" 來(lái)修改一個(gè)屬性的值师倔,這其實(shí)在底層調(diào)用了 setter。
在 setter 的函數(shù)體中周蹭,使用了特殊的標(biāo)識(shí)符 field
來(lái)訪問(wèn)支持字段的值趋艘。在 getter 中,只能讀取值凶朗;在 setter 中瓷胧,既能讀取它也能修改它。
可以只重定義可變屬性的一個(gè)訪問(wèn)器棚愤。
訪問(wèn)屬性的方式不依賴(lài)于它是否有支持字段搓萧,如果顯式地引用或使用默認(rèn)的訪問(wèn)器實(shí)現(xiàn)杂数,編譯器會(huì)為屬性生成支持字段。如果提供了一個(gè)自定義的訪問(wèn)器實(shí)現(xiàn)并且沒(méi)有使用 field
瘸洛,支持字段將不會(huì)被呈現(xiàn)出來(lái)揍移。
修改訪問(wèn)器的可見(jiàn)性
訪問(wèn)器的可見(jiàn)性默認(rèn)與屬性的可見(jiàn)性相同。但是如果需要反肋,可以通過(guò)在 get 和 set 關(guān)鍵字前放置可見(jiàn)性修飾符的方式來(lái)修改它那伐。
// 代碼清單 2.3 聲明一個(gè)具有 private setter 的屬性
class LengthCounter {
var counter: Int = 0
// 不能在類(lèi)外部修改這個(gè)屬性
private set
fun addWord(word: String) {
counter += word.length
}
}
這個(gè)類(lèi)用來(lái)計(jì)算單詞加在一起的總長(zhǎng)度。持有總長(zhǎng)度的屬性是 public 的石蔗,因?yàn)樗沁@個(gè)類(lèi)提供給客戶(hù)的 API 的一部分罕邀。但是需要確保它只能在類(lèi)中被修改,否則外部代碼有可能會(huì)修改它并存儲(chǔ)一個(gè)不正確的值养距。因此诉探,讓編譯器生成一個(gè)默認(rèn)可見(jiàn)性的 getter 方法,并且將 setter 的可見(jiàn)性修改為 private棍厌。