Kotlin 類、對象、接口摘要

接口

接口的方法可以有一個默認(rèn)實現(xiàn)

interface Clickable {
    fun click() // 普通的方法聲明
    fun showOff() = println("I'm clickable!") // 帶默認(rèn)實現(xiàn)的方法
}

如果你實現(xiàn)了這個接口,并且對默認(rèn)行為感到滿意的話可以省略 showOff的實現(xiàn),但你需要為 click 提供一個實現(xiàn)。

如果另外一個借口也有一個同樣的方法showOff

interface Focusable {
    fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
    fun showOff() = println("I'm focusable!")
}

如果你需要在你的類中實現(xiàn)這兩個接口會發(fā)生什么?

如果你沒有顯式實現(xiàn) showOff,你會得到如下的編譯錯誤:

The class 'Button' must override public open fun showOff() because it inherits many implementations of it.

Kotlin 編譯器強制要求你提供你自己的實現(xiàn)。

現(xiàn)在 Button 類實現(xiàn)了兩個接口。你通過調(diào)用繼承的兩個父類型中的實現(xiàn)來實現(xiàn)自己的 showOff()

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")

    override fun showOff() { // 如果同樣的繼承成員有不止一個實現(xiàn),你必須提供一個顯式實現(xiàn)。
super<Clickable>.showOff()
super<Focusable>.showOff() // 使用尖括號加上父類型名字的 "super" 表明了你想要調(diào)用哪一個父類的方法
    }
}

Java 中你可以把基類的名字放在 super 關(guān)鍵字的前面,就像 Clickable.super.showOff() 這樣,在 Kotlin 中需要把基類的名字放在尖括號中:super<Clickable>.showOff()

繼承

如果你想允許創(chuàng)建一個類的子類,你需要使用 open 修飾符來標(biāo)示這個類。此外,你需要給每一個可以被重寫的屬性或方法添加 open 修飾符。

open class RichButton : Clickable { // 這個類是open的:其他類可以繼承它。
    fun disable() {} // 這個函數(shù)是final的:你不能在子類中重寫它。
    open fun animate() {} // 這個函數(shù)是open的:你可以在子類中重寫它。
    override fun click() {} // 這個函數(shù)重寫了一個開放函數(shù)并且它本身同樣是open的。
}

注意,如果你重寫了一個基類或者接口的成員,重寫了的成員同樣默認(rèn)是 open。如果你想改變這一行為,阻止你的類的子類重寫你的實現(xiàn),你可以顯式將重寫的成員標(biāo)注為 final

open class RichButton : Clickable {
    final override fun click() {}
}

在 Kotlin 中,同 Java 一樣,你可以將一個類聲明為 abstract ,抽象類始終是開放的,所以你不需要顯式使用 open 修飾符。

abstract class Animated { // 這個類是抽象的:你不能創(chuàng)建它的實例。
    abstract fun animate() // 這個函數(shù)是抽象的:它沒有實現(xiàn)必須被子類重寫。
    open fun stopAnimating() {} // 抽象類中的非抽象函數(shù)并不是默認(rèn)開放的,但是可以標(biāo)注為開放的。
    fun animateTwice() {}
}
可見性

Kotlin 中的可見性修飾符與 Java 中的類似。你同樣可以使用 public, protectedprivate 修飾符。

  • 但是默認(rèn)的可見性是不一樣的:如果你省略了修飾符,聲明就是 public 的。

  • Java 中的默認(rèn)可見性——包私有,在 Kotlin 中并沒有使用。

  • 在 Java 中,你可以從同一個包中訪問一個 protected 的成員,但 Kotlin 中protected 成員只在類和它的子類中可見。

  • 要注意的是類的擴展函數(shù)不能訪問它的 privateprotected 成員。

嵌套類

  • 默認(rèn)情況下Kotlin 的嵌套類不能訪問外部類的實例,而 Java 中可以

  • Kotlin 中沒有顯式修飾符的嵌套類與 Java 中的 static 嵌套類是一樣的

  • 要把它變成一個內(nèi)部類來持有一個外部類的引用的話你需要使用 inner 修飾符

    類 A 在另一個類 B 中聲明 在 Java 中 在 Kotlin 中
    嵌套類(不存儲外部類的引用) static class A class A
    內(nèi)部類(存儲外部類的引用) class A inner class A

在 Kotlin 中引用外部類實例的語法也與 Java 不同。你需要使用 this@OuterInner 類去訪問 Outer 類:

class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}

密封類(sealed)

父類添加一個 sealed 修飾符,對可能創(chuàng)建的子類做出嚴(yán)格的限制。所有的直接子類必須嵌套在父類中。

sealed class Expr { // 將基類標(biāo)記為密封的
    class Num(val value: Int) : Expr() // 將所有可能的子類作為密封類列出
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when (e) { // "when" 表達(dá)式涵蓋了所有可能的情況,所以不再需要 "else" 分支
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
  • 如果你在 when 表達(dá)式里面處理所有 sealed 類的子類,你就不再需要提供默認(rèn)分支。注意 sealed 修飾符隱含了這個類是一個開放類;你不再需要顯式添加 open 修飾符。

  • 當(dāng)你在 when 中使用 sealed類并且添加一個新的子類的時候,有返回值的 when 表達(dá)式會導(dǎo)致編譯失敗,它會告訴你哪里的代碼必須要修改。

  • Expr 類有一個只能在類內(nèi)部調(diào)用的 private 構(gòu)造方法。你也不能聲明一個 sealed接口。為什么?如果你能這樣做的話,Kotlin編譯器不能保證任何人都不能在 Java 代碼中實現(xiàn)這個接口。

構(gòu)造函數(shù)

主構(gòu)造函數(shù)和初始化語句塊

class User constructor(_nickname: String) { // 帶一個參數(shù)的主構(gòu)造方法
    val nickname: String

    init { // 初始化語句塊
        nickname = _nickname
    }
}

簡化后

class User(val nickname: String) // "val" 意味著相應(yīng)的屬性會用構(gòu)造方法的參數(shù)來初始化

如果你的類具有一個父類,主構(gòu)造方法同樣需要初始化父類。你可以通過在基類列表的父類引用中提供父類構(gòu)造方法參數(shù)的方式來做到這一點:

open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

如果你沒有給一個類聲明任何的構(gòu)造方法,將會生成一個不做任何事情的默認(rèn)構(gòu)造方法:

open class Button // 將會生成一個不帶任何參數(shù)的默認(rèn)構(gòu)造方法

如果繼承了 Button 類并且沒有提供任何的構(gòu)造方法,你必須顯式調(diào)用父類的構(gòu)造方法即使它沒有任何的參數(shù):

class RadioButton: Button()

這就是為什么在父類名稱后面還需要一個空的括號

注意與接口的區(qū)別:接口沒有構(gòu)造方法,所以在你實現(xiàn)一個接口的時候,你不需要在父類型列表中它名稱后面再加上括號。

數(shù)據(jù)類

如果你為你的類添加 data 修飾符,必要的方法將會自動生成toStringequalshashCode。

data class Client(val name: String, val postalCode: Int)

請注意雖然數(shù)據(jù)類的屬性并沒有必須是 val —— 你同樣可以使用 var —— 但還是強烈推薦只使用只讀屬性,讓數(shù)據(jù)類的實例不可變

為了讓使用不可變對象的數(shù)據(jù)類變得更容易,Kotlin 編譯器為他們多生成了一個方法copy():一個允許拷貝類的實例的方法,并在拷貝的同時修改某些屬性的值。

類委托:"by" 關(guān)鍵字

下面這段代碼,為了實現(xiàn) Collection借口,你必須實現(xiàn)所有的方法

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()
    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean =
       innerList.containsAll(elements)
}

而如果使用類委托,就可以變成

class DelegatingCollection<T>(
        innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}

類中所有的方法實現(xiàn)都消失了,DelegatingCollection默認(rèn)會使用ArrayList的行為,如果你需要修改某一個函數(shù)的行為,只需要重寫這一個函數(shù)即可。

"object"關(guān)鍵字

Tips: 這是我在學(xué)習(xí) Koltin 是感受到最需要注意的地方,objectKotlin中的用法和重要。

Kotlin 中 object 關(guān)鍵字在多種情況下出現(xiàn),但是它們都遵循同樣的核心理念:這個關(guān)鍵字定義一個類并同時創(chuàng)建一個實例(換句話說就是一個對象)。讓我們來看看使用它的不同場景:

  • 對象聲明是定義單例的一種方式。
  • 伴生對象可以持有工廠方法和其他與這個類相關(guān),但在調(diào)用時并不依賴類實例的方法。它們的成員可以通過類名來訪問。
  • 對象表達(dá)式用來替代 Java 的匿名內(nèi)部類。
對象聲明

對象聲明將 類聲明 與該類的 單一實例 聲明結(jié)合到了一起。

object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}

與變量一樣,對象聲明允許你使用對象名加 . 字符的方式來調(diào)用方法和訪問屬性:

Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()

你同樣可以在類中聲明對象。這樣的對象同樣只有一個單一實例,它們在每個容器類的實例中并不具有不同的實例

data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}

在 Java 中使用 Kotlin 對象

Kotlin 中的對象聲明被編譯成了通過靜態(tài)字段來持有它的單一實例的類,這個字段名字始終都是 INSTANCE。如果你在 Java 中實現(xiàn)單例模式,你也許也會順手做同樣地事。因此,要從 Java 代碼使用 Kotlin 對象,可以通過訪問靜態(tài)的 INSTANCE 字段:

/* Java */
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);

在這個例子中,INSTANCE 字段的類型是 CaseInsensitiveFileComparator。

伴生對象companion

Kotlin 中的類不能擁有靜態(tài)成員;Java 的 static 關(guān)鍵字并不是 Kotlin 語言的一部分。使用伴生對象,會讓我們的方法調(diào)用看起來很像static方法。

class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}
>>> A.bar()
Companion object called

重要用途,實現(xiàn)工廠方法模式

class User private constructor(val nickname: String) { // 將主構(gòu)造方法標(biāo)記為私有
    companion object { // 聲明伴生對象
        fun newSubscribingUser(email: String) = // 聲明一個命名的伴生對象
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) = // 用工廠方法通過 Facebook 賬號來創(chuàng)建一個新用戶
            User(getFacebookName(accountId))
    }
}

你可以通過類名來調(diào)用 companion object 的方法:

>>> val subscribingUser = User.newSubscribingUser("bob@gmail.com")
>>> val facebookUser = User.newFacebookUser(4)

>>> println(subscribingUser.nickname)
bob
作為普通對象使用的伴生對象
class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}
>>> person = Person.Loader.fromJSON("{name: 'Dmitry'}") // 你可以通過兩種方式來調(diào)用 fromJSON
>>> person.name
Dmitry
>>> person2 = Person.fromJSON("{name: 'Brent'}") // 你可以通過兩種方式來調(diào)用 fromJSON
>>> person2.name
Brent

如果你省略了伴生對象的名字,默認(rèn)的名字將會分配為 Companion。

在伴生對象中實現(xiàn)接口

interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
       override fun fromJSON(jsonText: String): Person = ... // 實現(xiàn)接口的伴生對象
    }
}

這時,如果你有一個函數(shù)使用抽象方法來加載實體,你可以將 Person 對象傳進(jìn)去。

fun loadFromJSON<T>(factory: JSONFactory<T>): T {
    ...
}

loadFromJSON(Person) // 將伴生對象實例傳入函數(shù)中

注意,Person 類的名字被用作 JSONFactory 的實例。

另外,我們還可以為半生對象定義擴展函數(shù)。

class Person(val firstName: String, val lastName: String) {
    companion object { // 聲明一個空的伴生對象
    }
}

// client/server communication module
fun Person.Companion.fromJSON(json: String): Person { // 聲明一個擴展函數(shù)
    ...
}

val p = Person.fromJSON(json)
匿名內(nèi)部類
fun countClicks(window: Window) {
    var clickCount = 0 // 聲明局部變量

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++ // 更新變量的值
        }
    })
    // ...
}

與 Java 的匿名類一樣,在對象表達(dá)式中的代碼可以訪問創(chuàng)建它的函數(shù)中的變量。但是與 Java 不同,訪問并沒有被限制在 final 變量;你還可以在對象表達(dá)式中修改變量的值。例如,我們來看看你可以怎樣使用監(jiān)聽器對窗口點擊計數(shù)。

fun countClicks(window: Window) {
    var clickCount = 0 // 聲明局部變量

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++ // 更新變量的值
        }
    })
    // ...
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容