Kotlin 類、對象和接口(一)——定義類繼承結(jié)構(gòu)
在 Java 中一個類可以聲明一個或多個構(gòu)造方法,Kotlin 也是類似的,只是做出了一點修改:區(qū)分了主構(gòu)造方法 (通常是主要而間接的初始化類的方法,并且在類體外部聲明) 和從構(gòu)造方法(在類體內(nèi)部聲明)。同樣也允許在初始化語句塊中添加額外的初始化邏輯。
初始化類:主構(gòu)造方法和初始化語句塊
class User(val nickname: String)
通常來講,類的所有聲明都在花括號中,但上面這個類沒有花括號而是只包含了聲明在括號中。這段被括號圍起來的語句塊就叫做主構(gòu)造方法。它主要有兩個目的:表明構(gòu)造方法的參數(shù),以及定義使用那些參數(shù)初始化的屬性。它的工作原理以及完成同樣事情的最明確的代碼如下:
// 帶一個參數(shù)的主構(gòu)造方法
class User constructor(_nickname: String) {
val nickname: String
// 初始化語句塊
init {
nickname = _nickname
}
}
在這個例子中,constructor 關(guān)鍵字用來開始一個主構(gòu)造方法或從構(gòu)造方法的聲明。init 關(guān)鍵字用來引入一個初始化語句塊。這種語句塊包含了在類被創(chuàng)建時執(zhí)行的代碼,并會與主構(gòu)造方法一起使用。因為主構(gòu)造方法有語法限制,不能包含初始化代碼,這就是為什么要使用初始化語句塊的原因。也可以在一個類中聲明多個初始化語句塊。
構(gòu)造方法參數(shù) _nickname 中的下劃線用來區(qū)分屬性的名稱和構(gòu)造方法參數(shù)的名字。另一個可選方案是使用同樣的名字,通過 this 來消除歧義,就像 Java 中的常用做法一樣:this.nickname = nickname。
在這個例子中,不需要把初始化代碼放在初始化語句塊中,因為它可以與 nickname 屬性的聲明結(jié)合。如果主構(gòu)造方法沒有注解或可見性修飾符,同樣可以去掉 constructor 關(guān)鍵字,代碼如下:
// 帶一個參數(shù)的主構(gòu)造方法
class User(_nickname: String) {
// 用參數(shù)來初始化屬性
val nickname = _nickname
}
這就是聲明同樣的類的另一種方法。墻面兩個例子在類體中使用 val 關(guān)鍵字聲明了屬性,如果屬性用相應(yīng)的構(gòu)造方法參數(shù)來初始化,代碼可以通過把 val 關(guān)鍵字加在參數(shù)前的方式來進行簡化。可以替換類中的屬性定義:
// "val" 意味著相應(yīng)的屬性會用構(gòu)造方法的參數(shù)來初始化
class User(val nickname: String)
所有 User 類的聲明都是等價的,但是最后一個使用了最簡明的語法。
可以像函數(shù)一樣為構(gòu)造方法參數(shù)聲明一個默認(rèn)值:
// 為構(gòu)造方法參數(shù)提供一個默認(rèn)值
class User(val nickname: String, val isSubscribed: Boolean = true)
創(chuàng)建一個類的實例,只需要直接調(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ù)表明名稱
val carol = User("Carol", isSubscribed = false)
注意 如果所有的構(gòu)造方法參數(shù)都有默認(rèn)值,編譯器會生成一個額外的不帶參數(shù)的構(gòu)造方法來使用所有的默認(rèn)值。這可以讓 Kotlin 使用庫時變得更簡單,因為可以通過無參構(gòu)造方法來實例化類。
如果類具有一個父類,主構(gòu)造方法同樣需要初始化父類??梢酝ㄟ^在基類列表的父類引用中提供父類構(gòu)造方法參數(shù)的方式來做到這一點:
open class User(val nickname: String) {...}
class TwitterUser(nickname: String) : User(nickname) {...}
如果沒有給一個類聲明任何的構(gòu)造方法,將會生成一個不做任何事情的默認(rèn)構(gòu)造方法:
// 將會生成一個不帶任何參數(shù)的默認(rèn)構(gòu)造方法
open class Button
如果繼承了 Button 類并且沒有提供任何的構(gòu)造方法,必須顯式的調(diào)用父類的構(gòu)造方法,即使它沒有任何的參數(shù):
class RadioButton: Button()
注意與接口的區(qū)別:接口沒有構(gòu)造方法,所以在實現(xiàn)一個接口的時候,不需要在父類型列表中它的名稱后面加上括號。
如果想要確保類不被其他代碼實例化,必須把構(gòu)造方法標(biāo)記為 private。把主構(gòu)造方法標(biāo)記為 private 代碼如下:
// 這個類有一個 private 構(gòu)造方法
class Secretive private constructor() {}
因為 Secretive 類只有一個 private 的構(gòu)造方法,這個類外部的代碼不能實例化它。
private 構(gòu)造方法的替代方案
在 Java 中,可以通過使用 private 構(gòu)造方法禁止實例化這個類來表示一個更通用的意思:這個類是一個靜態(tài)實用工具成員的容器或者是單例的。Kotlin 針對這種目的具有內(nèi)建的語言級別的功能。可以使用頂層函數(shù)作為靜態(tài)實用工具,要想表示單例,可以使用對象聲明。
在大多數(shù)真實的場景中,類的構(gòu)造方法是非常簡明的:它要么沒有參數(shù)或者直接把參數(shù)與對應(yīng)的屬性關(guān)聯(lián)。這就是為什么 Kotlin 有為主構(gòu)造方法設(shè)計的簡潔的語法:在大多數(shù)的情況下都能很好地工作。
構(gòu)造方法:用不同的方式來初始化父類
通常來講,使用多個構(gòu)造方法的類在 Kotlin 代碼中不如在 Java 中常見。大多數(shù)在 Java 中需要重載構(gòu)造方法的場景都被 Kotlin 支持參數(shù)默認(rèn)值和參數(shù)命名的語法涵蓋了。
Tips 不要聲明多個從構(gòu)造方法來重載和提供參數(shù)的默認(rèn)值。取而代之的是,應(yīng)該直接標(biāo)明默認(rèn)值。
但是還是會有需要多個構(gòu)造方法的情景。最常見的一種就來自于當(dāng)需要擴展一個框架來提供多個構(gòu)造方法,以便于通過不同的方式來初始化類的時候。如一個在 Java 中聲明的具有兩個構(gòu)造方法的類,Kotlin 中相似的聲明如下:
open class View {
constructor(ctx: Context) {
// some code
}
constructor(ctx: Context, attr: AttributeSet) {
// some code
}
}
這個類沒有聲明一個主構(gòu)造方法(因為類頭部的類名后面并沒有括號),但是它聲明了兩個從構(gòu)造方法。從構(gòu)造方法使用 constructor 關(guān)鍵字引出。只要需要們可以聲明任意多個從構(gòu)造方法。
如果想擴展這個類,可以聲明同樣的構(gòu)造方法:
class MyButton: View {
constructor(ctx: Context) : super(ctx) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
這里定義了兩個構(gòu)造方法,他們都是用 super() 關(guān)鍵字調(diào)用了對應(yīng)的父類構(gòu)造方法。就像在 Java 中一樣,也可以使用 this() 關(guān)鍵字,從一個構(gòu)造方法中調(diào)用自己類中的另一個構(gòu)造方法,如下:
class MyButton: View {
// 委托給這個類的另一個構(gòu)造方法
constructor(ctx: Context) : this(ctx, MY_STYLE) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
可以修改 MyButton 類使得一個構(gòu)造方法委托給同一個類的另一個構(gòu)造方法(使用 this),為參數(shù)傳入默認(rèn)值。如果類沒有主構(gòu)造方法,那么每個從構(gòu)造方法必須初始化基類或者委托給另一個這樣做了的構(gòu)造方法,每個構(gòu)造方法必須以一個朝外的箭頭開始并且結(jié)束于任意一個基類構(gòu)造方法。
實現(xiàn)在接口中聲明的屬性
在 Kotlin 中,接口可以包含抽象屬性聲明。如下:
interface User {
val nickname: String
}
這就意味著實現(xiàn) User 接口的類需要提供一個取得 nickname 值的方式。接口并沒有說明這個值應(yīng)該存儲到一個支持字段還是通過 getter 來獲取。接口本身并不包含任何狀態(tài),因此只是實現(xiàn)這個接口的類在需要的情況下會存儲這個值。
// 代碼清單 2.1 實現(xiàn)一個接口屬性
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)
}
對于 PrivateUser 來說,使用了簡潔的語法直接在主構(gòu)造方法中聲明了一個屬性。這個屬性實現(xiàn)了來自 User 的抽象屬性,所以將其標(biāo)記為 override。
對于 SubscribingUser 來說,nickname 屬性通過一個自定義 getter 實現(xiàn),這個屬性沒有一個支持字段來存儲它的值,它只有一個 getter 在每次調(diào)用時從 email 中得到昵稱。
對于 FacebookUser 來說,在初始化時將 nickname 屬性與值關(guān)聯(lián)。使用了被認(rèn)為可以通過賬號 ID 返回 Facebook 用戶名稱的 getFacebookName 函數(shù)。
除了抽象屬性聲明外,接口還可以包含具有 getter 和 setter 的屬性,只要它們沒有應(yīng)用一個支持字段(支持字段需要在接口中存儲狀態(tài),而這是不允許的)。
通過 getter 或 setter 訪問支持字段
前面介紹了屬性的兩種類型:存儲值的屬性和具有自定義訪問器在每次訪問時計算值的屬性。若想要結(jié)合這兩種來實現(xiàn)一個既可以存儲值又可以在值被訪問和修改時提供額外邏輯的屬性,就需要能夠從屬性的訪問其中訪問它的支持字段。
假設(shè)想在任何對存儲在屬性中的數(shù)據(jù)進行修改時輸出日志,聲明了一個可變屬性并且在每次 setter 訪問時執(zhí)行額外的代碼:
// 代碼清單 2.2 在 setter 中訪問支持字段
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent())// 讀取支持字段的值
// 更新支持字段的值
field = value
}
}
可以像平常一樣通過使用 user.address = "new value" 來修改一個屬性的值,這其實在底層調(diào)用了 setter。
在 setter 的函數(shù)體中,使用了特殊的標(biāo)識符 field 來訪問支持字段的值。在 getter 中,只能讀取值;在 setter 中,既能讀取它也能修改它。
可以只重定義可變屬性的一個訪問器。
訪問屬性的方式不依賴于它是否有支持字段,如果顯式地引用或使用默認(rèn)的訪問器實現(xiàn),編譯器會為屬性生成支持字段。如果提供了一個自定義的訪問器實現(xiàn)并且沒有使用 field,支持字段將不會被呈現(xiàn)出來。
修改訪問器的可見性
訪問器的可見性默認(rèn)與屬性的可見性相同。但是如果需要,可以通過在 get 和 set 關(guān)鍵字前放置可見性修飾符的方式來修改它。
// 代碼清單 2.3 聲明一個具有 private setter 的屬性
class LengthCounter {
var counter: Int = 0
// 不能在類外部修改這個屬性
private set
fun addWord(word: String) {
counter += word.length
}
}
這個類用來計算單詞加在一起的總長度。持有總長度的屬性是 public 的,因為它是這個類提供給客戶的 API 的一部分。但是需要確保它只能在類中被修改,否則外部代碼有可能會修改它并存儲一個不正確的值。因此,讓編譯器生成一個默認(rèn)可見性的 getter 方法,并且將 setter 的可見性修改為 private。