Kotlin教程(三)類、對(duì)象和接口

寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kotlin的同學(xué)。系列文章的知識(shí)點(diǎn)會(huì)以《Kotlin實(shí)戰(zhàn)》這本書中順序編寫,在將書中知識(shí)點(diǎn)展示出來(lái)同時(shí),我也會(huì)添加對(duì)應(yīng)的Java代碼用于對(duì)比學(xué)習(xí)和更好的理解。

Kotlin教程(一)基礎(chǔ)
Kotlin教程(二)函數(shù)
Kotlin教程(三)類、對(duì)象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運(yùn)算符重載及其他約定
Kotlin教程(八)高階函數(shù)
Kotlin教程(九)泛型


定義類繼承結(jié)構(gòu)

Kotlin中的接口

Kotlin的接口與Java 8 中的相似:它們可以包含抽象方法(方法=函數(shù))的定義以及非抽象方法的實(shí)現(xiàn)(與Java 8 中的默認(rèn)方法類似),但它們不能包含任何狀態(tài)。
使用interface 關(guān)鍵字定義接口:

interface Clickable {
    fun click()
}

我們聲明了一個(gè)擁有名為click的但抽象方法的接口。所有實(shí)現(xiàn)這個(gè)接口的非抽象類都需要提供這個(gè)方法的一個(gè)實(shí)現(xiàn)。我們來(lái)實(shí)現(xiàn)以下這個(gè)接口:

class Button : Clickable {
    override fun click() = println("i was clicked")
}

Kotlin在類名后面使用冒號(hào)來(lái)代替了Java中的extendsimplements 關(guān)鍵字。和Java一樣,一個(gè)類可以實(shí)現(xiàn)任意多個(gè)接口,但是只能繼承一個(gè)類。
與Java中的@Override 注解類似,Kotlin中使用override 修飾符來(lái)標(biāo)注被重寫的父類或者接口的方法和屬性,使用override 修飾符是強(qiáng)制要求的,不標(biāo)注將不能編譯, 這會(huì)避免先寫出實(shí)現(xiàn)方法在添加抽象方法造成的意外重寫。
接口的方法可以有一個(gè)默認(rèn)實(shí)現(xiàn)。Java 8中需要你在這樣的實(shí)現(xiàn)上標(biāo)注default 關(guān)鍵字。而Kotlin不需要特殊的標(biāo)識(shí),只需要提供一個(gè)方法體:

interface Clickable {
    fun click()
    fun showOff() = println("i'm Clickable!") //默認(rèn)實(shí)現(xiàn)的方法
}

class Button : Clickable {
    override fun click() = println("i was clicked")
}

在Kotlin中實(shí)現(xiàn)這個(gè)接口時(shí),有默認(rèn)實(shí)現(xiàn)的方法就不一定要實(shí)現(xiàn)了。
但是注意了,如果你在Java代碼中實(shí)現(xiàn)這個(gè)Kotlin接口時(shí),所有的方法都要實(shí)現(xiàn),并沒(méi)有默認(rèn)實(shí)現(xiàn)的說(shuō)法。

class Abc implements Clickable {

    @Override
    public void click() {
    }

    @Override
    public void showOff() { //必須實(shí)現(xiàn)
    }
}

這和Kotlin默認(rèn)方法實(shí)現(xiàn)的方式有關(guān)系,先來(lái)看下實(shí)現(xiàn)方式就知道為什么在Java中所有方法都要實(shí)現(xiàn)了。我們將上面的接口和實(shí)現(xiàn)類轉(zhuǎn)換成Java代碼:

public interface Clickable {
   void click();

   void showOff();

   public static final class DefaultImpls {
      public static void showOff(Clickable $this) {
         String var1 = "i'm Clickable!";
         System.out.println(var1);
      }
   }
}

public final class Button implements Clickable {
   public void click() {
      String var1 = "i was clicked";
      System.out.println(var1);
   }

   public void showOff() {
      Clickable.DefaultImpls.showOff(this);
   }
}

可以看到Kotlin實(shí)現(xiàn)接口默認(rèn)方法的方式是:定義了一個(gè)靜態(tài)內(nèi)部類DefaultImpls,在這個(gè)類中實(shí)現(xiàn)了默認(rèn)方法,并且參數(shù)是Clickable對(duì)象,然后給每個(gè)實(shí)現(xiàn)類(Button)默認(rèn)加上了實(shí)現(xiàn)和調(diào)用Clickable.DefaultImpls.showOff(this); 。Kotlin需要兼容到Java 6,因此并沒(méi)有使用Java 8的接口特性。
有沒(méi)有發(fā)現(xiàn)這種實(shí)現(xiàn)方式其實(shí)與上一章的擴(kuò)展函數(shù)非常類似?

有一種特殊情況:如果你的類實(shí)現(xiàn)了兩個(gè)接口,并且這兩個(gè)接口中分別定了同名的默認(rèn)實(shí)現(xiàn)的方法,那這個(gè)時(shí)候這個(gè)類會(huì)采用那個(gè)接口的默認(rèn)實(shí)現(xiàn)那?
答案是:任何一個(gè)都不會(huì)使用。取而代之的時(shí),如果你沒(méi)有顯示實(shí)現(xiàn)這個(gè)同名接口,會(huì)得到編譯錯(cuò)誤的提示。

interface Clickable {
    fun click()
    fun showOff() = println("i'm Clickable!")
}

interface Focusable {
    fun showOff() = println("i'm Focusable!")
}

class Button : Clickable, Focusable {
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }

    override fun click() = println("i was clicked")
}

這里我們實(shí)現(xiàn)同名的showOff ,并且調(diào)用父類型的實(shí)現(xiàn)。我們使用了與Java相同的關(guān)鍵字super 。但是語(yǔ)法略有不同,Java中可以把基類的名字放在super關(guān)鍵字的前面,就像Clickable.super.showOff() ,在Kotlin中需要把基類的名字放在尖括號(hào)中:super<Clickable>.showOff()

open、final和abstract修飾符:默認(rèn)為final

Java中默認(rèn)類都是可以被繼承和復(fù)寫方法的,除非顯示地使用final 關(guān)鍵字,這通常很方便,單頁(yè)造成了一些問(wèn)題。對(duì)基類進(jìn)行修改胡導(dǎo)致自樂(lè)不正確的行為,這就是所謂的脆弱的基類問(wèn)題。《Effective Java》中也建議:要么為繼承做好設(shè)計(jì)并記錄文檔,要么禁止這么做。所以Kotlin采用了這樣的思想,默認(rèn)都是final的。如果你想允許創(chuàng)建一個(gè)類的子類,需要使用open 修飾符來(lái)標(biāo)識(shí)這個(gè)類,還要給每一個(gè)可以被重寫的屬性或方法添加open 修飾符。

open class RichButton : Clickable { //open修飾表示可以有子類
    fun disable() {} //這個(gè)函數(shù)是final的,不能被子類重寫
    
    open fun animate() {} //函數(shù)是open的,可以被子類重寫
    
    override fun click() {} //這個(gè)函數(shù)是重寫了一個(gè)open函數(shù),因此也是open的
}

如果你重寫一個(gè)基類或者接口的成員,重寫的成員同樣默認(rèn)是open的,如果你想改變這一行為,阻止子類繼續(xù)重寫,可以顯示地將重寫的成員標(biāo)注為final:

open class RichButton : Clickable { 
    final override fun click() {} //顯示標(biāo)記final,阻止子類重寫
}

在Kotlin中也有abstract 類,除了默認(rèn)是final以外基本與Java相同:

abstract class Animated { //抽象類,不能創(chuàng)建實(shí)例
    abstract fun animate()//抽象方法,必須被子類重寫

    open fun stopAnimating() {}//顯示修飾open

    fun animateTwice() {}//普通方法默認(rèn)還是final
}

個(gè)人建議雖然接口可以默認(rèn)實(shí)現(xiàn),但我們還是按照J(rèn)ava的習(xí)慣來(lái)使用,不在接口中定義默認(rèn)實(shí)現(xiàn),有默認(rèn)實(shí)現(xiàn)的定義成abstract 類即可。

類中范文修飾符的意義

修飾符 相關(guān)成員 評(píng)注
final 不能被重寫 類中成員默認(rèn)使用
open 可以被重寫 需要明確地表明
abstract 必須被重 只能在抽象類中使用,抽象成員不能有實(shí)現(xiàn)
override 重寫父類或接口中成員 如果沒(méi)有使用final表明,重寫的成員默認(rèn)是open的

可見性修飾符:默認(rèn)為public

總體來(lái)說(shuō)Kotlin中的可見性修飾符與Java中類似。同樣可以使用public 、protectedprivate 修飾符。但是默認(rèn)的可見性是不一樣的,如果省略了修飾符,聲明就是public 的。
Java中默認(rèn)可見性——包私有。在Kotlin中并沒(méi)有使用。Kotlin只把包作為在命名空間里組織代碼的一種方式使用,并沒(méi)有將其用作可見性控制。
作為替代方案,Kotlin提供了一個(gè)新的修飾符:internal ,表示只在模塊內(nèi)部可見。一個(gè)模塊就是一組一起編譯的Kotlin文件,這可能是一個(gè)Intellij IDEA模塊、一個(gè)Eclipse項(xiàng)目、一個(gè)Maven或Gradle項(xiàng)目或者一組使用調(diào)用Ant任務(wù)進(jìn)行編譯的文件。
internal 可見性的優(yōu)勢(shì)在于它提供了對(duì)模塊實(shí)現(xiàn)細(xì)節(jié)的真正封裝。使用Java時(shí),這種封裝很容易被破壞,因?yàn)橥獠看a可以將類定義到與你代碼相同的包中,從而得到訪問(wèn)你包私有聲明的權(quán)限。
Kotlin中有特有的頂層聲明,如果在頂層聲明中使用private 可見性,包括類、函數(shù)和屬性,那么這些聲明是會(huì)在聲明他們的文件中可見。

Kotlin的可見性修飾符

修飾符 類成員 頂層聲明
public(默認(rèn)) 所有地方可見 所有地方可見
internal 模塊中可見 模塊中可見
protected 子類中可見 -
private 類中可見 文件中可見

注意,protected 修飾符在Java和Kotlin中不同的行為。在Java中,可以從同一個(gè)包中訪問(wèn)一個(gè)protected 成員,但是在Kotlin中protected 成員只在類和它的子類中可見,即同一個(gè)包是不可見的。
同時(shí)還要注意類的擴(kuò)展函數(shù)不能訪問(wèn)類的protectedprotected 成員。

Kotlin中的public、protected和private修飾符在編譯成Java字節(jié)碼時(shí)會(huì)被保留。你從Java代碼使用這些Kotlin聲明時(shí)就如同他們?cè)贘ava中聲明了同樣的可見性。唯一的例外是private類會(huì)被編譯成包私有聲明(在Java中你不能把類聲明為private)。
但是你可能會(huì)問(wèn),internal修飾符會(huì)發(fā)生什么?Java中并沒(méi)有直接與之類似的東西。包私有可見性是一個(gè)完全不同的東西,一個(gè)模塊通常會(huì)由多個(gè)包組成,并且不同模塊可能會(huì)包含來(lái)自同一個(gè)包的聲明。因此internal修飾符在字節(jié)碼中會(huì)變成public。
這些Kotlin聲明和它們Java翻版(或者說(shuō)它們的字節(jié)碼呈現(xiàn))的對(duì)應(yīng)關(guān)系解釋了為什么有時(shí)你能從Java代碼中訪問(wèn)internal類或頂層聲明,抑或從同一個(gè)包的Java代碼中訪問(wèn)一個(gè)protected的成員(與你在Java中做的相似)。但是你應(yīng)該盡量避免這種情況的出現(xiàn)來(lái)打破可見性的約束。

此外,Kotlin與Java之間可見性規(guī)則的另一個(gè)區(qū)別:Kotlin中的一個(gè)外部類不能看到其內(nèi)部(或嵌套)類中private成員。

內(nèi)部類和嵌套類:默認(rèn)是嵌套類

如果你對(duì)Java的內(nèi)部類和嵌套類的定義不是很清楚,或者忘了細(xì)節(jié),可以看下這篇博客:深入理解java嵌套類和內(nèi)部類、匿名類

class Outer {

    class Inner {
        //內(nèi)部類,持有外部類的應(yīng)用
    }

    static class Nested {
        //嵌套類,不持有外部類
    }
}

Java中內(nèi)部類會(huì)持有外部類引用,這層引用關(guān)系通常很容易忽略而造成內(nèi)存泄露和意料之外的問(wèn)題。因此Kotlin中默認(rèn)是嵌套類,如果想聲明成內(nèi)部類,需要使用inner 修飾符。

嵌套類和內(nèi)部類在Java與Kotlin中的對(duì)應(yīng)關(guān)系

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

在Java中內(nèi)部類通過(guò)Outer.this 來(lái)獲取外部類的對(duì)象,而在Kotlin中則是通過(guò)this@Outer 獲得外部類對(duì)象。

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

密封類:定義受限的類繼承結(jié)構(gòu)

Kotlin提供了一個(gè)sealed 修飾符用于修飾類,來(lái)限制子類必須嵌套在父類中。

sealed class Father {
    class ChildA : Father()

    class ChildB : Father()
}

sealed 修飾符隱含這個(gè)類是一個(gè)open 的類,你不再需要顯示得添加open 修飾符了。

這有什么好處那?當(dāng)你在when 表達(dá)式處理所有sealed 類的子類時(shí),你就不再需要提供默認(rèn)分支了:

fun a(c: Father): Int =
            when (c) {
                is ChildA -> 1
                is ChildB -> 2
//                else -> 3  //覆蓋了所有可能的情況,所以不再需要了
            }

聲明了sealed 修飾符的類只能在內(nèi)部調(diào)用private構(gòu)造方法,也不能聲明一個(gè)sealed 的接口。為什么呢?還記得轉(zhuǎn)換成Java字節(jié)碼時(shí)可見性的規(guī)則嗎?如果不這樣做,Kotlin編譯器不能保證在Java代碼中實(shí)現(xiàn)這個(gè)接口。

在Kotlin 1.0 中,sealed功能是相當(dāng)嚴(yán)格的。所有子類必須是嵌套的,并且子類不能創(chuàng)建為data類(后面會(huì)提到)。Kotlin 1.1 解除了這些限制并允許在同一文件的任何位置定義sealed類的子類。

聲明一個(gè)帶非默認(rèn)構(gòu)造方法或?qū)傩缘念?/h2>

Java中可以聲明一個(gè)或多個(gè)構(gòu)造方法,Kotlin也是類似的,只是做了一點(diǎn)修改:區(qū)分了主構(gòu)造方法(通常是主要而簡(jiǎn)潔的初始化類的方法,并且在類體外部聲明)和從構(gòu)造方法(在類體內(nèi)部聲明)。同樣也允許在初始化語(yǔ)句塊中添加額外的初始化邏輯。

初始化類:主構(gòu)造方法和初始化語(yǔ)句塊

在這之前我們已經(jīng)見過(guò)怎么聲明一個(gè)簡(jiǎn)單的類了:

class User (val nickName: String)

這里括號(hào)圍起來(lái)的語(yǔ)句塊(val nickName: String) 叫做主構(gòu)造方法。主要有兩個(gè)目的:標(biāo)明構(gòu)造方法的參數(shù),以及定義使用這些參數(shù)初始化的屬性。查看轉(zhuǎn)換后的Java代碼可以了解它的工作機(jī)制:

public final class User {
   @NotNull
   private final String nickName;

   @NotNull
   public final String getNickName() {
      return this.nickName;
   }

   public User(@NotNull String nickName) {
      this.nickName = nickName;
   }
}

我們也可以按照J(rèn)ava的這種邏輯在Kotlin中實(shí)現(xiàn)(事實(shí)上完全沒(méi)有必要,僅僅是學(xué)習(xí)關(guān)鍵字的例子,這樣寫上面完全相同):

class User constructor(_nickName: String) {
    val nickName: String

    init {
        nickName = _nickName
    }
}

這里出現(xiàn)了兩個(gè)新的關(guān)鍵字:constructor 用來(lái)開始一個(gè)主構(gòu)造方法或者從構(gòu)造方法的聲明(與類名一起定義主構(gòu)造方法時(shí)可以省略);init 關(guān)鍵字用來(lái)引入一個(gè)初始化語(yǔ)句塊,與Java中的構(gòu)造代碼塊非常類似。
這種寫法與class User (val nickName: String) 完全一致,有沒(méi)有注意到簡(jiǎn)單的寫法中多了val 關(guān)鍵字,這意味著相應(yīng)的屬性會(huì)使用構(gòu)造方法的參數(shù)來(lái)初始化。

構(gòu)造方法也可以像函數(shù)參數(shù)一樣設(shè)置默認(rèn)值:

class User @JvmOverloads constructor(val nickName: String, val isSubscribed: Boolean = true)

默認(rèn)參數(shù)有效減少了定義重載構(gòu)造,@JvmOverloads 支持Java代碼創(chuàng)建實(shí)例時(shí)也能享受默認(rèn)參數(shù)。

如果你的類具有與一個(gè)父類,主構(gòu)造方法同樣需要初始化父類??梢酝ㄟ^(guò)在基類列表的父類引用中提供父類構(gòu)造方法參數(shù)的方式做到這一點(diǎn):

open class User(val nickName: String)

class TwitterUser(nickName: String) : User(nickName)

如果沒(méi)有給一個(gè)類聲明任何的構(gòu)造方法,將會(huì)生成一個(gè)不做任何事情的默認(rèn)構(gòu)造方法,繼承了該類的的類也必須顯示的調(diào)用的父類的構(gòu)造方法:

open class Button

class RadioButton : Button()

注意到Button() 后面的() 了嗎?這也是與接口的區(qū)別,接口沒(méi)有構(gòu)造方法,因此接口后面沒(méi)有()

interface Clickable

class RadioButton : Button(), Clickable

如果你想要確保類不被其他代碼實(shí)例化,那就加上private

class Secretive private constrauctor()

在Java中可以通過(guò)使用private構(gòu)造方法禁止實(shí)例化這個(gè)類來(lái)表示一個(gè)更通用的意思:這個(gè)類是一個(gè)靜態(tài)實(shí)用工具類的容器或者單例的。Kotlin針對(duì)這種目的具有內(nèi)建的語(yǔ)言級(jí)別的功能??梢允褂庙攲雍瘮?shù)作為靜態(tài)實(shí)用工具。想要表示單例,可以使用對(duì)象聲明,將會(huì)在之后的章節(jié)中見到。

構(gòu)造方法:用不同的方式來(lái)初始化父類

默認(rèn)參數(shù)已經(jīng)可以避免構(gòu)造方法的重載了。但是如果你一定要聲明多個(gè)構(gòu)造參數(shù),也是可以的。

open class View {
    constructor(context: Context)

    constructor(context: Context, attributes: Attributes)
} 

這個(gè)類沒(méi)有聲明主構(gòu)造方法,但是聲明了兩個(gè)從構(gòu)造方法,從構(gòu)造方法必須使用constructor 關(guān)鍵字引出。
如果想要擴(kuò)展這個(gè)類,可以聲明同樣的構(gòu)造方法,使用super 關(guān)鍵字調(diào)用對(duì)應(yīng)的父類構(gòu)造方法:

class Button : View {
    constructor(context: Context) : super(context)

    constructor(context: Context, attributes: Attributes) : super(context, attributes)
}

就像在Java中一樣,也可以使用this 關(guān)鍵字,從一個(gè)構(gòu)造方法調(diào)用類中另一個(gè)構(gòu)造方法。

class Button : View {
    constructor(context: Context) : super(context)

    constructor(context: Context, attributes: Attributes) : this(context)
}

注意,如果定義了主構(gòu)造方法,所有的從構(gòu)造方法都必須直接或者間接的調(diào)用主構(gòu)造方法:

open class View() {
    constructor(context: Context) : this()

    constructor(context: Context, attributes: Attributes) : this(context)
}

實(shí)現(xiàn)在接口中聲明的屬性

在Kotlin中,接口可以包含抽象屬性聲明:

interface User {
    val nickName: String
}

其實(shí)這里的屬性,并不是變量(字段),而是val 代表了getter方法,相應(yīng)的Java代碼:

public interface User {
   @NotNull
   String getNickName();
}

我們用幾種方式來(lái)實(shí)現(xiàn)這個(gè)接口:

class PrivateUser(override val nickName: String) : User

class SubscribingUser(val email: String) : User {
    override val nickName: String 
        get() = email.substringBefore("@")   //只有g(shù)etter方法
}

class FacebookUser(val accountId: Int) : User {
    override val nickName = getFacebookName(accountId) //字段支持
}

PrivateUser類使用了簡(jiǎn)潔的語(yǔ)法在主構(gòu)造方法中聲明了一個(gè)屬性,這個(gè)屬性實(shí)現(xiàn)了來(lái)自于User的抽象屬性,所以需要標(biāo)記override。
SubscribingUser類,nickName屬性通過(guò)一個(gè)自定義getter實(shí)現(xiàn),這個(gè)屬性沒(méi)有一個(gè)支持存儲(chǔ)它的值,它只有一個(gè)getter在每次調(diào)用時(shí)從email中得到昵稱。
FacebookUser類在初始化時(shí)將nickName屬性與值關(guān)聯(lián)。getFacebookName 方法通過(guò)與Facebook關(guān)聯(lián)獲取用戶信息,代價(jià)較大,因此只在初始化階段調(diào)用一次。
除了抽象屬性聲明外,接口還可以包含具有g(shù)etter和setter的屬性,只要它們沒(méi)有引用一個(gè)支持字段(支持字段需要在接口中存儲(chǔ)狀態(tài),這是不允許的):

interface User {
    val email: String
    val nickName: String 
          get() = email.substringBefore("@")
}

通過(guò)getter或setter訪問(wèn)支持字段

之前說(shuō)的屬性其實(shí)有兩種:一種是字段或者說(shuō)變量,Kotlin中聲明這種字段會(huì)生成默認(rèn)的getter和setter方法。而另一個(gè)種即沒(méi)有字段,僅僅只有g(shù)etter和setter方法,因?yàn)樵贙otlin的表現(xiàn)形式相同,因此都叫做屬性。而相應(yīng)的Java代碼可以較清楚地表現(xiàn)兩者的區(qū)別:

class Student {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return name.length() > 0 ? name.substring(0, 1) : "";
    }
}

name 屬性是字段支持的,而Surname 屬性僅僅只有g(shù)et方法,這兩個(gè)屬性定義在Kotlin中是這樣的:

class Student {
    var name: String = ""
    val surname: String
        get() = if (name.isNotEmpty()) name.substring(0, 1) else ""
}

Kotlin中聲明的字段屬性會(huì)生成默認(rèn)的getter和setter方法,也可以改變這種默認(rèn)的生成:

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
                Address was changed for $name: "$field" -> "$value".
            """.trimIndent())
            field = value
        }
}

在字段的下方也可以像定義自定義訪問(wèn)器那樣定義getter和setter方法,在方法中使用field 標(biāo)識(shí)符來(lái)表示支持字段。是否發(fā)現(xiàn)在Kotlin中這兩種屬性的區(qū)別很小:是否初始化:= "unspecified" ,是否使用field 字段。

修改訪問(wèn)器的可見性

訪問(wèn)器的可見性與屬性的可見性相同。但是如果需要可以通過(guò)在get和set關(guān)鍵字前放置可見性修飾符的來(lái)修改它:

class LengthCounter {
    var counter: Int = 0
        private set

    private var other: Int = 0
}

直接在屬性前放置private 和在set或者get訪問(wèn)器前放置有什么區(qū)別那?看看轉(zhuǎn)換后的Java代碼:

public final class LengthCounter {
   private int counter;
   private int other;

   public final int getCounter() {
      return this.counter;
   }

   private final void setCounter(int var1) {
      this.counter = var1;
   }
}

private直接修飾屬性將不會(huì)生成getter和setter方法。而修飾set會(huì)生成private的setter方法。

編譯器生成的方法:數(shù)據(jù)類和類委托

通用對(duì)象方法

我們先來(lái)看看Java中常見的toStringequalshashCode 方法在Kotlin中是如何復(fù)寫的。
toString()

class Client(val name: String, val postalCode: Int) {
    override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
}

equals()
在Java中== 運(yùn)算符,如果應(yīng)用在基本數(shù)據(jù)類型上比較的是值,而在引用類型上比較的是引用。因此,在Java中通??偸钦{(diào)用equals。
而在Kotlin中== 就是Java中的equals ,如果在Kotlin中想要比較引用,可以使用=== 運(yùn)算符。


class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client) { //檢查是不是一個(gè)Client
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }
}

Any 是java.lang.Object的模擬:Kotlin中所有類的父類??煽疹愋?code>Any? 意味著other有可能為null。在Kotlin中所有可能為null的情況都需要顯示標(biāo)明,即在類型后面加上 ,后續(xù)章節(jié)會(huì)詳細(xì)說(shuō)明。

hashCode()
hashCode方法通常與equals一起被重寫,因?yàn)橥ㄓ玫膆ashCode契約:如果兩個(gè)對(duì)象相等,他們必須有著相同的hash值。

class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

這三個(gè)方法在數(shù)據(jù)容器bean通常都是被重寫的,并且基本都是工具自動(dòng)生成的,而現(xiàn)在Kotlin編譯器就可以幫我們做這些工作。

數(shù)據(jù)類:自動(dòng)生成通用方法的實(shí)踐

只需要在class 前加上data 關(guān)鍵字就能定義一個(gè)實(shí)現(xiàn)了toStringequalshashCode 方法的類——數(shù)據(jù)類:

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

雖然數(shù)據(jù)類的屬性并沒(méi)有要求是val ,但還是強(qiáng)烈推薦只使用只讀屬性,讓數(shù)據(jù)類的實(shí)例不可變。為了讓不可變對(duì)象的數(shù)據(jù)類的使用變得更容易,Kotlin編譯器為它們多生成了一個(gè)方法,一個(gè)允許copy類的實(shí)例的方法,并在copy的同時(shí)修改某些屬性的值。下面是手動(dòng)實(shí)現(xiàn)copy方法后看起來(lái)是的樣子:

data class Client(val name: String, val postalCode: Int) {
    fun copy(name:String = this, postalCode:Int = this.postalCode) = Client(name, postalCode)
}

類委托:使用“by”關(guān)鍵字

Java中通常采用裝飾器模式來(lái)向其他類添加一些行為。這種模式的本質(zhì)就是創(chuàng)建一個(gè)新類,實(shí)現(xiàn)與原始類一樣的接口并將原來(lái)的類的實(shí)例作為一個(gè)字段保存,與原始類擁有同樣行為的方法不用修改,只需要直接轉(zhuǎn)發(fā)到原始類的實(shí)例。
這種方式的一個(gè)缺點(diǎn)是需要相當(dāng)多的模板代碼。例如我們來(lái)實(shí)現(xiàn)一個(gè)Collection的接口的裝飾器,即使你不需要修改任何的行為:

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()

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

現(xiàn)在Kotlin將委托作為一個(gè)語(yǔ)言級(jí)別的功能做了頭等支持。無(wú)論什么時(shí)候?qū)崿F(xiàn)一個(gè)接口,你都可以使用by 關(guān)鍵字將接口的實(shí)現(xiàn)委托到另一個(gè)對(duì)象。下面就是怎樣通過(guò)推薦的方式來(lái)重寫前面的例子:

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

類中所有的方法實(shí)現(xiàn)都消失了,編譯器會(huì)生成它們,并實(shí)現(xiàn)與DelegatingCollection的例子是相似的。這樣的話僅僅只需要重寫我們需要改變行為的方法就可以了:

class CountingSet<T>(
        val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
    var objectAdded = 0

    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded++
        return innerSet.addAll(elements)
    }
}

這個(gè)例子通過(guò)重寫add和addAll方法計(jì)數(shù),并將MutableCollection接口剩下的實(shí)現(xiàn)委托給被包裝的容器。

object關(guān)鍵字:將聲明一個(gè)類與創(chuàng)建一個(gè)實(shí)例結(jié)合起來(lái)

Kotlin中object關(guān)鍵字在多種情況下出現(xiàn),但是他們都遵循同樣的核心理念:這個(gè)關(guān)鍵字定義一個(gè)類并同時(shí)創(chuàng)建一個(gè)實(shí)例(對(duì)象)。讓我們來(lái)看看使用它的不同場(chǎng)景:

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

對(duì)象聲明:創(chuàng)建單例易如反掌

單例模式時(shí)Java鐘最常用的一種設(shè)計(jì)模式。Kotlin通過(guò)使用對(duì)象聲明功能為這一切提供了最高級(jí)的語(yǔ)言支持。對(duì)象聲明將類聲明與該類的單一實(shí)例聲明結(jié)合到了一起。

object Payroll {
    val allEmployees = arrayListOf<Person>()

    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}

與類一樣,一個(gè)對(duì)象聲明也可以包含屬性、方法、初始化語(yǔ)句塊等的聲明。唯一不允許的就是構(gòu)造方法。與普通類的實(shí)例不同,對(duì)象聲明在定義的時(shí)候就立即創(chuàng)建了,不需要再代碼的其他地方調(diào)用構(gòu)造方法。
與變量一樣,對(duì)象聲明允許你使用對(duì)象名.字符 的方式來(lái)調(diào)用方法和訪問(wèn)屬性:

Payroll.allEmployees.add(Person(...))
Payroll.calculateSallary()

想知道它是如何工作的?同樣來(lái)看轉(zhuǎn)換后的Java代碼吧:

public final class Payroll {
   @NotNull
   private static final ArrayList allEmployees;
   public static final Payroll INSTANCE;

    private Payroll(){
    }

   @NotNull
   public final ArrayList getAllEmployees() {
      return allEmployees;
   }

   public final void calculateSalary() {
       ...
   }

   static {
      Payroll var0 = new Payroll();
      INSTANCE = var0;
      allEmployees = new ArrayList();
   }
}

可以看到私有化了構(gòu)造方法,并且通過(guò)靜態(tài)代碼塊初始化了Payroll實(shí)例,保存在INSTANCE字段,這也是為什么在Java中是使用需要這種方式:

Payroll.INSTANCE.calculateSalary()

該INSTANCE是在Payroll類加載進(jìn)內(nèi)存中就會(huì)創(chuàng)建的實(shí)例,因此,不建議將依賴太多或者開銷太大的類使用object聲明成單例。

同樣可以在類使用對(duì)象聲明創(chuàng)建單例,并且該對(duì)象聲明可以訪問(wèn)外部類中的private屬性:

data class Person(val name: String) {
    //定義
    object NameComparator : Comparator<Person> {
        override fun compare(o1: Person, o2: Person): Int = o1.name.compareTo(o2.name)
    }
}

val persons = listOf(Person("Bob"), Person("Alice"))
persons.sortedWith(Person.NameComparator) //調(diào)用

伴生對(duì)象:工廠方法和靜態(tài)成員的地盤

Kotlin中的類不能擁有靜態(tài)成員:Java的static關(guān)鍵字并不是Kotlin語(yǔ)言的一部分。作為代替,Kotlin依賴包級(jí)別函數(shù)(在大多數(shù)情況下能夠替代Java的靜態(tài)方法)和對(duì)象聲明(在其他情況下替代Java的靜態(tài)方法,同時(shí)還包括靜態(tài)字段)。在大多數(shù)情況下,還是推薦使用頂層函數(shù),但是頂層函數(shù)不能訪問(wèn)類的private成員。
特別是Java中常見的工廠方法和類中需要使用的static成員該如何定義那?就像這樣的:

static class B {
        public static final String tag = "tag";
        
        private B() {
        }

        public static B newInstance() {
            return new B();
        }
    }

這時(shí)候就要使用伴生對(duì)象了。伴生對(duì)象是在類中定義的對(duì)象前添加一個(gè)特殊的關(guān)鍵字來(lái)標(biāo)記:companion 。這樣做,就獲得了直接通過(guò)容器類名稱來(lái)訪問(wèn)這個(gè)這個(gè)對(duì)象的方法和屬性的能力,不再需要顯示得指明對(duì)象的名稱,最終的語(yǔ)法看起來(lái)非常像Java中的靜態(tài)方法調(diào)用:

class A private constructor() {
    companion object {
        fun newInstance() = A()
        val tag = "tag"
    }
}

A.newInstance()
A.tag

作為普通對(duì)象使用的伴生對(duì)象

伴生對(duì)象本質(zhì)也是一個(gè)普通對(duì)象,普通對(duì)象可以做的一切伴生對(duì)象都可以,例如實(shí)現(xiàn)接口。
之所以看上去奇怪,是因?yàn)橹拔覀冎皇鞘÷运念惷部梢越o它加上類名:

class A private constructor() {
    companion object C{
        val tag = "tag"
        fun newInstance() = A()
    }
}

A.C.newInstance() //兩種使用方式效果相同
A.newInstance()

如果省略了伴生對(duì)象的名字,默認(rèn)的名字將會(huì)是Companion。這點(diǎn)在將代碼轉(zhuǎn)換成Java代碼后就出現(xiàn)了:

public final class A {
   @NotNull
   private static final String tag = "tag";
   public static final A.Companion Companion = new A.Companion();

   private A() {
   }

   public static final class Companion {
      @NotNull
      public final String getTag() {
         return A.tag;
      }

      @NotNull
      public final A newInstance() {
         return new A((DefaultConstructorMarker)null);
      }

      private Companion() {
      }
   }
}

所以,你應(yīng)該理解在Java中調(diào)用伴生對(duì)象的屬性是這樣的了:A. Companion.newInstance() 。
為了讓Java中調(diào)用也有一致的體驗(yàn),可以在對(duì)應(yīng)的成員上使用@JvmStatic注解來(lái)達(dá)到這個(gè)目的。如果你想聲明一個(gè)static字段,可以在一個(gè)頂層屬性或者聲明在object中的屬性上使用@JvmField注解。

class A private constructor() {
    companion object{
        @JvmField
        val tag = "tag"
        @JvmStatic
        fun newInstance() = A()
    }
}

既然伴生對(duì)象就是一個(gè)普通類,當(dāng)然也是可以聲明擴(kuò)展函數(shù):

fun A.Companion.getFlag() = "flag"

A.getFlag()

對(duì)象表達(dá)式:改變寫法的匿名內(nèi)部類

object關(guān)鍵字不僅僅能用來(lái)聲明單例式的對(duì)象,還能用來(lái)聲明匿名對(duì)象。我們來(lái)翻寫下Java中如下使用匿名內(nèi)部類的代碼:

public static void main(String[] args) {
        new B().setListener(new Listener() {
            @Override
            public void onClick() {

            }
        });
    }

    interface Listener {
        void onClick();
    }

    static class B {

        private Listener listener;

        public void setListener(Listener listener) {
            this.listener = listener;
        }
    }

Kotlin中使用匿名內(nèi)部類:

fun main(args: Array<String>) {
    B().setListener(object : Listener {
        override fun onClick() {
        }
    })
}

除了去掉了對(duì)象的名字外,語(yǔ)法時(shí)與對(duì)象聲明相同的。對(duì)象表達(dá)式聲明了一個(gè)類并創(chuàng)建了該類的一個(gè)實(shí)例,但是并沒(méi)有給這個(gè)類或是實(shí)例分配一個(gè)名字。通常來(lái)說(shuō)它們都不需要名字,應(yīng)為你會(huì)將這個(gè)對(duì)象用作一個(gè)函數(shù)調(diào)用的參數(shù)。如果你需要給對(duì)象分配一個(gè)名字,可以將其存儲(chǔ)到一個(gè)變量中。
與Java匿名內(nèi)部類只能擴(kuò)展一個(gè)類或?qū)崿F(xiàn)一個(gè)接口不同,Kotlin的匿名對(duì)象可以實(shí)現(xiàn)多個(gè)接口。并且訪問(wèn)創(chuàng)建匿名內(nèi)部類的函數(shù)中的變量是沒(méi)有限制在final變量,還可以在對(duì)象表達(dá)式中修改變量的值:

fun main(args: Array<String>) {
    var clickCount = 0 
    B().setListener(object : Listener {
        override fun onClick() {
            clickCount++ //修改變量
        }
    })
}

同樣的,我們通過(guò)查看轉(zhuǎn)換的Java代碼還研究為什么可以做到這些區(qū)別:

public static final void main(@NotNull String[] args) {
      final IntRef clickCount = new IntRef();
      clickCount.element = 0;
      (new B()).setListener((Listener)(new Listener() {
         public void onClick() {
            int var1 = clickCount.element++;
         }
      }));
   }

可以看到這里通過(guò)IntRef包裝了我們定義的clickCount,因此,final屬性聲明在了包裝類上。
那Kotlin的匿名對(duì)象可以實(shí)現(xiàn)多個(gè)接口,又是如何做的那?我又新定義了一個(gè)接口,讓匿名內(nèi)部類同時(shí)實(shí)現(xiàn)兩個(gè)接口:

fun main(args: Array<String>) {
    var clickCount = 0
    val niming = object : Listener, OnLongClickListener {
        override fun onLongClick() {
        }

        override fun onClick() {
            clickCount++
        }
    }
    B().setListener(niming)
    View().onLongClickListener = niming
}

interface OnLongClickListener {
    fun onLongClick()
}

class View {
    var onLongClickListener: OnLongClickListener? = null
}
public static final void main(@NotNull String[] args) {
      final IntRef clickCount = new IntRef();
      clickCount.element = 0;
      <undefinedtype> niming = new Listener() {
         public void onLongClick() {
         }

         public void onClick() {
            int var1 = clickCount.element++;
         }
      };
      (new B()).setListener((Listener)niming);
      (new View()).setOnLongClickListener((OnLongClickListener)niming);
   }

出現(xiàn)了一個(gè)新東西<undefinedtype> 根據(jù)字面理解應(yīng)該是一個(gè)未確定的類型,并且可以強(qiáng)轉(zhuǎn)成對(duì)應(yīng)的接口,這個(gè)可能就不是Java的內(nèi)容了,不清楚具體的實(shí)現(xiàn)是怎樣的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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