Kotlin 類、對象和接口(一)——定義類繼承結(jié)構(gòu)
Kotlin 類煤率、對象和接口(一)——定義類繼承結(jié)構(gòu)
Java 平臺定義了一些需要在許多類中呈現(xiàn)的方法,并且通常是以一種很機械的方式實現(xiàn)墓律,譬如 equals钧嘶、hashCode 及 toString。幸運的是撰豺,Java IDE 可以將這些方法的生成自動化粪般,所以通常不需要手動書寫它們。這種情況下污桦,代碼庫就包含了樣板代碼亩歹。Kotlin 的編譯器領先了一步:它能將這些呆板的代碼生成放到母后,并不會因為自動生成的結(jié)果導致源代碼文件變得混亂。
通用對象方法
就像 Java 中的情況一樣捆憎,所有的 Kotlin 類也有許多可能需要重寫的方法:toString舅柜、equals 和 hashCode。先看一下一個簡單的用來存儲客戶名字和郵編的 Client 類躲惰。
// 代碼清單 3.1 Client 類的最初聲明
class Client(val name: String, val postalCode: Int)
字符串表示:toString()
Kotlin 中所有類同 Java 一樣致份,提供了一種方式來獲取類對象的字符串表示形式,默認來說础拨,一個對象的字符串表示形如 Client@5e9f23b4氮块,這并不是十分有用。想要改變它诡宗,需要重寫 toString 方法滔蝉。
// 代碼清單 3.2 為 Client 實現(xiàn) toString()
class Client(val name: String, val postalCode: Int) {
override fun toString(): String = "Client(name='$name', postalCode=$postalCode)"
}
對象相等性:equals()
所有關(guān)于 Client 類的計算都發(fā)生在其外部,這個類只是用來存儲數(shù)據(jù)塔沃,這意味著簡單和透明蝠引。盡管如此,也許還會有一些針對這個類行為的需求蛀柴。例如螃概,假設想要將包好相同數(shù)據(jù)的對象視為相等:
>>> val client1 = Client("Alice", 342562)
>>> val client1 = Client("Alice", 342562)
// 在 kotlin 中,== 檢查對象是否相等鸽疾,而不是比較引用吊洼。這里會編譯成調(diào)用"equals"
>>> println(client1 == client2)
false
對象并不相等,意味著必須為 Client 類重寫 equals制肮。
==表示相等性
在 Java 中冒窍,可以使用 == 運算符來比較基本數(shù)據(jù)類型和引用類型。如果引用在基本數(shù)據(jù)類型上豺鼻,Java 的 == 比較的是值综液,然而在引用類型上 == 比較的是引用。因此拘领,在 Java 中意乓,眾所周知的實踐是總是調(diào)用 equals。
在 Kotlin 中约素,== 運算符是比較兩個對象的默認方式:本質(zhì)上說它就是通過調(diào)用 equals 來比較兩個值的届良。因此,如果 equals 在類中被重寫了圣猎,就能夠很安全地使用 == 來比較實例士葫。要想進行引用比較,可以使用 === 運算符送悔,這與 Java 中的 == 比較對象引用的效果一模一樣慢显。
修改后的 Client 類
// 代碼清單 3.3 為 Client 實現(xiàn) equals()
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Client
if (name != other.name) return false
if (postalCode != other.postalCode) return false
return true
}
override fun toString(): String = "Client(name='$name', postalCode=$postalCode)"
}
Hash 容器:hashCode()
hashCode 方法通常與 equals 一起重寫爪模。
創(chuàng)建一個元素的 set:一個名為 Alice 的客戶。接著荚藻,創(chuàng)建一個新的包含相同數(shù)據(jù)的 Client 實例并檢查它是否包含在 set 中屋灌。期望檢查會返回 true,因為這兩個實例是相等的应狱,但實際上返回的是 false:
>>> val processed = hashSetOf(Client("Alice", 342562))
>>> println(processed.contains(Client("Alice", 342562)))
false
原因是 Client 類缺少了 hashCode 方法共郭。因此它違反了通用的 hashCode 契約:如果兩個對象相等,他們必須有著相同的 hash 值疾呻。processed set 是一個 HashSet除嘹,在 HashSet 中值是以一種優(yōu)化過的方式來比較的:首先比較它們的 hash 值,然后只有當它們相等時才會去比較真正的值岸蜗。上面例子中 Client 類的兩個不同的實例有著不同的 hash 值尉咕,所以 set 認為它不包含第二個對象,即使 equals 會返回 true璃岳。
要修復這個問題年缎,可以向類中添加 hashCode 的實現(xiàn):
// 代碼清單 3.4 為 Client 實現(xiàn) hashCode()
class Client(val name: String, val postalCode: Int) {
// ...
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + postalCode
return result
}
}
現(xiàn)在這個類在所有的情況下都能按預期來工作了。
數(shù)據(jù)類:自動生成通用方法的實現(xiàn)
如果想要的類是一個方便的數(shù)據(jù)容器铃慷,需要重寫這些方法:toString晦款、equals 和 hasCode。通常來說這些方法的實現(xiàn)十分簡單枚冗,很多 IDE 也能夠自動生成它們,并確保它們的實現(xiàn)是正確且一致的蛇损。
不過赁温,在 Kotlin 中不必再去生成這些方法了。如果為類添加 data 修飾符淤齐,必要的方法將會自動生成好股囊。
// 代碼清單 3.5 數(shù)據(jù)類 Client
data class Client(val name: String, val postalCode: Int)
這樣就得到了一個重寫了所有標準 Java 的類:
- equals 用來比較實例
- hashCode 用來作為例如 HashMap 這種基于哈希容器的鍵
- toString 用來為類生成按聲明順序排列的所有字段的字符串表達式
equals 和 hashCode 方法會將所有在主構(gòu)造方法中聲明的屬性納入考慮。生成的 equals 方法會檢測所有的屬性值是否相等更啄。hashCode 方法會返回一個根據(jù)所有屬性生成的哈希值稚疹。沒有在主構(gòu)造方法中聲明的屬性將不會加入到相等性檢查和哈希值計算中去。
數(shù)據(jù)類和不可變性:copy() 方法
雖然數(shù)據(jù)類的屬性并沒有要求是 val祭务,同樣可以使用 var内狗,但還是強烈建議推薦只是用只讀屬性,讓數(shù)據(jù)類的實例不可變义锥。如果想使用這樣的實例作為 HashMap 或者類似容器的鍵柳沙,這會是必需的要求。因為如果不這樣拌倍,被用作鍵的對象在加入容器后被修改了赂鲤,容器可能會進入一種無效的狀態(tài)噪径。不可變對象同樣更容易理解,特別是在多線程代碼中:一旦一個對象被創(chuàng)建出來数初,他會一直保持初始狀態(tài)找爱,也不用擔心在代碼工作時其他線程修改了對象的值。
為了讓使用不可變對象的數(shù)據(jù)類變得更容易泡孩,Kotlin 編譯器為它們多生成了一個方法:一個允許 copy 類的實例的方法车摄,并在 copy 的同時修改某些屬性的值。創(chuàng)建副本通常是修改實例的好選擇:副本有著單獨的生命周期并且不會影響代碼中引用原始實例的位置珍德。
類委托:使用 "by" 關(guān)鍵字
設計大型面向?qū)ο笙到y(tǒng)的一個常見問題就是由繼承的實現(xiàn)導致的脆弱性练般。當擴展一個類并重寫某些方法是,代碼就變得依賴繼承的那個類的實現(xiàn)細節(jié)锈候。當系統(tǒng)不斷演進并且基類的實現(xiàn)被修改或者新方法被添加進去是薄料,做出的關(guān)于類行為的假設會失效,所以代碼也許最后就以不正確的行為而告終泵琳。
Kotlin 的設計就識別了這樣的問題摄职,并默認將類視為 final 的。這確保了只有那些設計成可擴展的類可以被繼承获列。當使用這樣的類時谷市,就會意識到它是開放的,就會注意這些修改需要與派生類兼容击孩。
但是常常需要向其他類添加一些行為迫悠,即使它沒有被設計為可擴展的。一個常用的實現(xiàn)方式以 裝飾器
模式文明巩梢。這種模式的本質(zhì)就是創(chuàng)建一個新類创泄,實現(xiàn)與原始類一樣的接口并將原來的類的實例作為一個字段保存。與原始類擁有同樣行為的方法不會被修改括蝠,只需要直接轉(zhuǎn)發(fā)到原始類的實例鞠抑。
這種方式的一個缺點是需要相當多的樣板代碼(像 Intellij IDEA 一樣的眾多 IDE 都有專門生成這樣代碼的功能)。Kotlin 將委托作為一個語言級別的功能做了頭等支持忌警。無論什么時候?qū)崿F(xiàn)一個接口搁拙,都可以使用 by 關(guān)鍵字將接口的實現(xiàn)委托
到另一個對象。
class DelegatingCollection<T> (
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
類中所有的方法都消失了法绵,編譯器會生成它們箕速。當需要修改某些方法的行為是,可以重寫它們礼烈,這樣方法就會被調(diào)用而不是使用生成的方法弧满。可以保留感到滿意的委托給內(nèi)部的實例中種默認實現(xiàn)此熬。