Kotlin教程(三)類、物件和介面

胡奚冰發表於2018-03-23

寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學習Kotlin的同學。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展示出來同時,我也會新增對應的Java程式碼用於對比學習和更好的理解。

Kotlin教程(一)基礎
Kotlin教程(二)函式
Kotlin教程(三)類、物件和介面
Kotlin教程(四)可空性
Kotlin教程(五)型別
Kotlin教程(六)Lambda程式設計
Kotlin教程(七)運算子過載及其他約定
Kotlin教程(八)高階函式
Kotlin教程(九)泛型


定義類繼承結構

Kotlin中的介面

Kotlin的介面與Java 8 中的相似:它們可以包含抽象方法(方法=函式)的定義以及非抽象方法的實現(與Java 8 中的預設方法類似),但它們不能包含任何狀態。 使用interface 關鍵字定義介面:

interface Clickable {
    fun click()
}
複製程式碼

我們宣告瞭一個擁有名為click的抽象方法的介面。所有實現這個介面的非抽象類都需要提供這個方法的一個實現。我們來實現以下這個介面:

class Button : Clickable {
    override fun click() = println("i was clicked")
}
複製程式碼

Kotlin在類名後面使用冒號來代替了Java中的extendsimplements 關鍵字。和Java一樣,一個類可以實現任意多個介面,但是隻能繼承一個類。 與Java中的@Override 註解類似,Kotlin中使用override 修飾符來標註被重寫的父類或者介面的方法和屬性,使用override 修飾符是強制要求的,不標註將不能編譯, 這會避免先寫出實現方法在新增抽象方法造成的意外重寫。 介面的方法可以有一個預設實現。Java 8中需要你在這樣的實現上標註default 關鍵字。而Kotlin不需要特殊的標識,只需要提供一個方法體:

interface Clickable {
    fun click()
    fun showOff() = println("i'm Clickable!") //預設實現的方法
}

class Button : Clickable {
    override fun click() = println("i was clicked")
}
複製程式碼

在Kotlin中實現這個介面時,有預設實現的方法就不一定要實現了。 但是注意了,如果你在Java程式碼中實現這個Kotlin介面時,所有的方法都要實現,並沒有預設實現的說法。

class Abc implements Clickable {

    @Override
    public void click() {
    }

    @Override
    public void showOff() { //必須實現
    }
}
複製程式碼

這和Kotlin預設方法實現的方式有關係,先來看下實現方式就知道為什麼在Java中所有方法都要實現了。我們將上面的介面和實現類轉換成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實現介面預設方法的方式是:定義了一個靜態內部類DefaultImpls,在這個類中實現了預設方法,並且引數是Clickable物件,然後給每個實現類(Button)預設加上了實現和呼叫Clickable.DefaultImpls.showOff(this); 。Kotlin需要相容到Java 6,因此並沒有使用Java 8的介面特性。 有沒有發現這種實現方式其實與上一章的擴充套件函式非常類似?

有一種特殊情況:如果你的類實現了兩個介面,並且這兩個介面中分別定了同名的預設實現的方法,那這個時候這個類會採用那個介面的預設實現那? 答案是:任何一個都不會使用。取而代之的時,如果你沒有顯示實現這個同名介面,會得到編譯錯誤的提示。

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")
}
複製程式碼

這裡我們實現同名的showOff ,並且呼叫父型別的實現。我們使用了與Java相同的關鍵字super 。但是語法略有不同,Java中可以把基類的名字放在super關鍵字的前面,就像Clickable.super.showOff() ,在Kotlin中需要把基類的名字放在尖括號中:super<Clickable>.showOff()

open、final和abstract修飾符:預設為final

Java中預設類都是可以被繼承和複寫方法的,除非顯示地使用final 關鍵字,這通常很方便,但也造成了一些問題。對基類進行修改會導致不正確的行為,這就是所謂的脆弱的基類問題。《Effective Java》中也建議:要麼為繼承做好設計並記錄文件,要麼禁止這麼做。所以Kotlin採用了這樣的思想,預設都是final的。如果你想允許建立一個類的子類,需要使用open 修飾符來標識這個類,還要給每一個可以被重寫的屬性或方法新增open 修飾符。

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

如果你重寫一個基類或者介面的成員,重寫的成員同樣預設是open的,如果你想改變這一行為,阻止子類繼續重寫,可以顯示地將重寫的成員標註為final:

open class RichButton : Clickable { 
    final override fun click() {} //顯示標記final,阻止子類重寫
}
複製程式碼

在Kotlin中也有abstract 類,除了預設是final以外基本與Java相同:

abstract class Animated { //抽象類,不能建立例項
    abstract fun animate()//抽象方法,必須被子類重寫

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

    fun animateTwice() {}//普通方法預設還是final
}
複製程式碼

個人建議雖然介面可以預設實現,但我們還是按照Java的習慣來使用,不在介面中定義預設實現,有預設實現的定義成abstract 類即可。

類中範文修飾符的意義

修飾符 相關成員 評註
final 不能被重寫 類中成員預設使用
open 可以被重寫 需要明確地表明
abstract 必須被重 只能在抽象類中使用,抽象成員不能有實現
override 重寫父類或介面中成員 如果沒有使用final表明,重寫的成員預設是open的

可見性修飾符:預設為public

總體來說Kotlin中的可見性修飾符與Java中類似。同樣可以使用publicprotectedprivate 修飾符。但是預設的可見性是不一樣的,如果省略了修飾符,宣告就是public 的。 Java中預設可見性——包私有。在Kotlin中並沒有使用。Kotlin只把包作為在名稱空間裡組織程式碼的一種方式使用,並沒有將其用作可見性控制。 作為替代方案,Kotlin提供了一個新的修飾符:internal ,表示只在模組內部可見。一個模組就是一組一起編譯的Kotlin檔案,這可能是一個Intellij IDEA模組、一個Eclipse專案、一個Maven或Gradle專案或者一組使用呼叫Ant任務進行編譯的檔案。 internal 可見性的優勢在於它提供了對模組實現細節的真正封裝。使用Java時,這種封裝很容易被破壞,因為外部程式碼可以將類定義到與你程式碼相同的包中,從而得到訪問你包私有宣告的許可權。 Kotlin中有特有的頂層宣告,如果在頂層宣告中使用private 可見性,包括類、函式和屬性,那麼這些宣告是會在宣告他們的檔案中可見。

Kotlin的可見性修飾符

修飾符 類成員 頂層宣告
public(預設) 所有地方可見 所有地方可見
internal 模組中可見 模組中可見
protected 子類中可見 -
private 類中可見 檔案中可見

注意,protected 修飾符在Java和Kotlin中不同的行為。在Java中,可以從同一個包中訪問一個protected 成員,但是在Kotlin中protected 成員只在類和它的子類中可見,即同一個包是不可見的。 同時還要注意類的擴充套件函式不能訪問類的protected 成員。

Kotlin中的public、protected和private修飾符在編譯成Java位元組碼時會被保留。你從Java程式碼使用這些Kotlin宣告時就如同他們在Java中宣告瞭同樣的可見性。唯一的例外是private類會被編譯成包私有宣告(在Java中你不能把類宣告為private)。 但是你可能會問,internal修飾符會發生什麼?Java中並沒有直接與之類似的東西。包私有可見性是一個完全不同的東西,一個模組通常會由多個包組成,並且不同模組可能會包含來自同一個包的宣告。因此internal修飾符在位元組碼中會變成public。 這些Kotlin宣告和它們Java翻版(或者說它們的位元組碼呈現)的對應關係解釋了為什麼有時你能從Java程式碼中訪問internal類或頂層宣告,抑或從同一個包的Java程式碼中訪問一個protected的成員(與你在Java中做的相似)。但是你應該儘量避免這種情況的出現來打破可見性的約束。

此外,Kotlin與Java之間可見性規則的另一個區別:Kotlin中的一個外部類不能看到其內部(或巢狀)類中private成員。

內部類和巢狀類:預設是巢狀類

如果你對Java的內部類和巢狀類的定義不是很清楚,或者忘了細節,可以看下這篇部落格:深入理解java巢狀類和內部類、匿名類

class Outer {

    class Inner {
        //內部類,持有外部類的應用
    }

    static class Nested {
        //巢狀類,不持有外部類
    }
}
複製程式碼

Java中內部類會持有外部類引用,這層引用關係通常很容易忽略而造成記憶體洩露和意料之外的問題。因此Kotlin中預設是巢狀類,如果想宣告成內部類,需要使用inner 修飾符。

巢狀類和內部類在Java與Kotlin中的對應關係

類A在另一個類B中的宣告 在Java中 在Kotlin中
巢狀類(不儲存外部類的引用) static class A class A
內部類(儲存外部類的引用) class A inner class A

在Java中內部類通過Outer.this 來獲取外部類的物件,而在Kotlin中則是通過this@Outer 獲得外部類物件。

class Outer {
    inner class Inner {
        fun getOuter(): Outer = this@Outer
    }
}
複製程式碼

密封類:定義受限的類繼承結構

Kotlin提供了一個sealed 修飾符用於修飾類,來限制子類必須巢狀在父類中。

sealed class Father {
    class ChildA : Father()

    class ChildB : Father()
}
複製程式碼

sealed 修飾符隱含這個類是一個open 的類,你不再需要顯示得新增open 修飾符了。

這有什麼好處那?當你在when 表示式處理所有sealed 類的子類時,你就不再需要提供預設分支了:

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

宣告瞭sealed 修飾符的類只能在內部呼叫private構造方法,也不能宣告一個sealed 的介面。為什麼呢?還記得轉換成Java位元組碼時可見性的規則嗎?如果不這樣做,Kotlin編譯器不能保證在Java程式碼中實現這個介面。

在Kotlin 1.0 中,sealed功能是相當嚴格的。所有子類必須是巢狀的,並且子類不能建立為data類(後面會提到)。Kotlin 1.1 解除了這些限制並允許在同一檔案的任何位置定義sealed類的子類。

宣告一個帶非預設構造方法或屬性的類

Java中可以宣告一個或多個構造方法,Kotlin也是類似的,只是做了一點修改:區分了主構造方法(通常是主要而簡潔的初始化類的方法,並且在類體外部宣告)和從構造方法(在類體內部宣告)。同樣也允許在初始化語句塊中新增額外的初始化邏輯。

初始化類:主構造方法和初始化語句塊

在這之前我們已經見過怎麼宣告一個簡單的類了:

class User (val nickName: String)
複製程式碼

這裡括號圍起來的語句塊(val nickName: String) 叫做主構造方法。主要有兩個目的:標明構造方法的引數,以及定義使用這些引數初始化的屬性。檢視轉換後的Java程式碼可以瞭解它的工作機制:

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;
   }
}
複製程式碼

我們也可以按照Java的這種邏輯在Kotlin中實現(事實上完全沒有必要,僅僅是學習關鍵字的例子,這樣寫與上面完全相同):

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

    init {
        nickName = _nickName
    }
}
複製程式碼

這裡出現了兩個新的關鍵字:constructor 用來開始一個主構造方法或者從構造方法的宣告(與類名一起定義主構造方法時可以省略);init 關鍵字用來引入一個初始化語句塊,與Java中的構造程式碼塊非常類似。 這種寫法與class User (val nickName: String) 完全一致,有沒有注意到簡單的寫法中多了val 關鍵字,這意味著相應的屬性會使用構造方法的引數來初始化。

構造方法也可以像函式引數一樣設定預設值:

class User @JvmOverloads constructor(val nickName: String, val isSubscribed: Boolean = true)
複製程式碼

預設引數有效減少了定義過載構造,@JvmOverloads 支援Java程式碼建立例項時也能享受預設引數。

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

open class User(val nickName: String)

class TwitterUser(nickName: String) : User(nickName)
複製程式碼

如果沒有給一個類宣告任何的構造方法,將會生成一個不做任何事情的預設構造方法,繼承了該類的的類也必須顯示的呼叫的父類的構造方法:

open class Button

class RadioButton : Button()
複製程式碼

注意到Button() 後面的() 了嗎?這也是與介面的區別,介面沒有構造方法,因此介面後面沒有()

interface Clickable

class RadioButton : Button(), Clickable
複製程式碼

如果你想要確保類不被其他程式碼例項化,那就加上private

class Secretive private constrauctor()
複製程式碼

在Java中可以通過使用private構造方法禁止例項化這個類來表示一個更通用的意思:這個類是一個靜態實用工具類的容器或者單例的。Kotlin針對這種目的具有內建的語言級別的功能。可以使用頂層函式作為靜態實用工具。想要表示單例,可以使用物件宣告,將會在之後的章節中見到。

構造方法:用不同的方式來初始化父類

預設引數已經可以避免構造方法的過載了。但是如果你一定要宣告多個構造引數,也是可以的。

open class View {
    constructor(context: Context)

    constructor(context: Context, attributes: Attributes)
} 
複製程式碼

這個類沒有宣告主構造方法,但是宣告瞭兩個從構造方法,從構造方法必須使用constructor 關鍵字引出。 如果想要擴充套件這個類,可以宣告同樣的構造方法,使用super 關鍵字呼叫對應的父類構造方法:

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

    constructor(context: Context, attributes: Attributes) : super(context, attributes)
}
複製程式碼

就像在Java中一樣,也可以使用this 關鍵字,從一個構造方法呼叫類中另一個構造方法。

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

    constructor(context: Context, attributes: Attributes) : this(context)
}
複製程式碼

注意,如果定義了主構造方法,所有的從構造方法都必須直接或者間接的呼叫主構造方法:

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

    constructor(context: Context, attributes: Attributes) : this(context)
}
複製程式碼

實現在介面中宣告的屬性

在Kotlin中,介面可以包含抽象屬性宣告:

interface User {
    val nickName: String
}
複製程式碼

其實這裡的屬性,並不是變數(欄位),而是val 代表了getter方法,相應的Java程式碼:

public interface User {
   @NotNull
   String getNickName();
}
複製程式碼

我們用幾種方式來實現這個介面:

class PrivateUser(override val nickName: String) : User

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

class FacebookUser(val accountId: Int) : User {
    override val nickName = getFacebookName(accountId) //欄位支援
}
複製程式碼

PrivateUser類使用了簡潔的語法在主構造方法中宣告瞭一個屬性,這個屬性實現了來自於User的抽象屬性,所以需要標記override。 SubscribingUser類,nickName屬性通過一個自定義getter實現,這個屬性沒有一個支援儲存它的值,它只有一個getter在每次呼叫時從email中得到暱稱。 FacebookUser類在初始化時將nickName屬性與值關聯。getFacebookName 方法通過與Facebook關聯獲取使用者資訊,代價較大,因此只在初始化階段呼叫一次。 除了抽象屬性宣告外,介面還可以包含具有getter和setter的屬性,只要它們沒有引用一個支援欄位(支援欄位需要在介面中儲存狀態,這是不允許的):

interface User {
    val email: String
    val nickName: String 
          get() = email.substringBefore("@")
}
複製程式碼

通過getter或setter訪問支援欄位

之前說的屬性其實有兩種:一種是欄位或者說變數,Kotlin中宣告這種欄位會生成預設的getter和setter方法。而另一個種即沒有欄位,僅僅只有getter和setter方法,因為在Kotlin的表現形式相同,因此都叫做屬性。而相應的Java程式碼可以較清楚地表現兩者的區別:

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 屬性僅僅只有get方法,這兩個屬性定義在Kotlin中是這樣的:

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

Kotlin中宣告的欄位屬性會生成預設的getter和setter方法,也可以改變這種預設的生成:

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

在欄位的下方也可以像定義自定義訪問器那樣定義getter和setter方法,在方法中使用field 識別符號來表示支援欄位。是否發現在Kotlin中這兩種屬性的區別很小:是否初始化:= "unspecified" ,是否使用field 欄位。

修改訪問器的可見性

訪問器的可見性與屬性的可見性相同。但是如果需要可以通過在get和set關鍵字前放置可見性修飾符的來修改它:

class LengthCounter {
    var counter: Int = 0
        private set

    private var other: Int = 0
}
複製程式碼

直接在屬性前放置private 和在set或者get訪問器前放置有什麼區別那?看看轉換後的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直接修飾屬性將不會生成getter和setter方法。而修飾set會生成private的setter方法。

編譯器生成的方法:資料類和類委託

通用物件方法

我們先來看看Java中常見的toStringequalshashCode 方法在Kotlin中是如何複寫的。

toString()

class Client(val name: String, val postalCode: Int) {
    override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
}
複製程式碼

equals()

在Java中== 運算子,如果應用在基本資料型別上比較的是值,而在引用型別上比較的是引用。因此,在Java中通常總是呼叫equals。 而在Kotlin中== 就是Java中的equals ,如果在Kotlin中想要比較引用,可以使用=== 運算子。


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

Any 是java.lang.Object的模擬:Kotlin中所有類的父類。可空型別Any? 意味著other有可能為null。在Kotlin中所有可能為null的情況都需要顯示標明,即在型別後面加上 ,後續章節會詳細說明。

hashCode()

hashCode方法通常與equals一起被重寫,因為通用的hashCode契約:如果兩個物件相等,他們必須有著相同的hash值。

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

這三個方法在資料容器bean通常都是被重寫的,並且基本都是工具自動生成的,而現在Kotlin編譯器就可以幫我們做這些工作。

資料類:自動生成通用方法的實踐

只需要在class 前加上data 關鍵字就能定義一個實現了toStringequalshashCode 方法的類——資料類:

data class Client(val name: String, val postalCode: Int) 
複製程式碼

雖然資料類的屬性並沒有要求是val ,但還是強烈推薦只使用只讀屬性,讓資料類的例項不可變。為了讓不可變物件的資料類的使用變得更容易,Kotlin編譯器為它們多生成了一個方法,一個允許copy類的例項的方法,並在copy的同時修改某些屬性的值。下面是手動實現copy方法後看起來是的樣子:

data class Client(val name: String, val postalCode: Int) {
    fun copy(name:String = this, postalCode:Int = this.postalCode) = Client(name, postalCode)
}
複製程式碼

類委託:使用“by”關鍵字

Java中通常採用裝飾器模式來向其他類新增一些行為。這種模式的本質就是建立一個新類,實現與原始類一樣的介面並將原來的類的例項作為一個欄位儲存,與原始類擁有同樣行為的方法不用修改,只需要直接轉發到原始類的例項。 這種方式的一個缺點是需要相當多的模板程式碼。例如我們來實現一個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()
}
複製程式碼

現在Kotlin將委託作為一個語言級別的功能做了頭等支援。無論什麼時候實現一個介面,你都可以使用by 關鍵字將介面的實現委託到另一個物件。下面就是怎樣通過推薦的方式來重寫前面的例子:

class DelegatingCollection<T>(val innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList
複製程式碼

類中所有的方法實現都消失了,編譯器會生成它們,並實現與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)
    }
}
複製程式碼

這個例子通過重寫add和addAll方法計數,並將MutableCollection介面剩下的實現委託給被包裝的容器。

object關鍵字:將宣告一個類與建立一個例項結合起來

Kotlin中object關鍵字在多種情況下出現,但是他們都遵循同樣的核心理念:這個關鍵字定義一個類並同時建立一個例項(物件)。讓我們來看看使用它的不同場景:

  • 物件宣告是定義單例的一種方式。
  • 伴生物件可以持有工廠方法和其他與這個類相關,但在呼叫時並不依賴類例項的方法。他們的成員可以通過類名來訪問。
  • 物件表示式用來替代Java的匿名內部類。

物件宣告:建立單例易如反掌

單例模式是Java中最常用的一種設計模式。Kotlin通過使用物件宣告功能為這一切提供了最高階的語言支援。物件宣告將類宣告與該類的單一例項宣告結合到了一起。

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

    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}
複製程式碼

與類一樣,一個物件宣告也可以包含屬性、方法、初始化語句塊等的宣告。唯一不允許的就是構造方法。與普通類的例項不同,物件宣告在定義的時候就立即建立了,不需要再程式碼的其他地方呼叫構造方法。 與變數一樣,物件宣告允許你使用物件名.字元 的方式來呼叫方法和訪問屬性:

Payroll.allEmployees.add(Person(...))
Payroll.calculateSallary()
複製程式碼

想知道它是如何工作的?同樣來看轉換後的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();
   }
}
複製程式碼

可以看到私有化了構造方法,並且通過靜態程式碼塊初始化了Payroll例項,儲存在INSTANCE欄位,這也是為什麼在Java中是使用需要這種方式:

Payroll.INSTANCE.calculateSalary()
複製程式碼

該INSTANCE是在Payroll類載入進記憶體中就會建立的例項,因此,不建議將依賴太多或者開銷太大的類使用object宣告成單例。

同樣可以在類使用物件宣告建立單例,並且該物件宣告可以訪問外部類中的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) //呼叫
複製程式碼

伴生物件:工廠方法和靜態成員的地盤

Kotlin中的類不能擁有靜態成員:Java的static關鍵字並不是Kotlin語言的一部分。作為代替,Kotlin依賴包級別函式(在大多數情況下能夠替代Java的靜態方法)和物件宣告(在其他情況下替代Java的靜態方法,同時還包括靜態欄位)。在大多數情況下,還是推薦使用頂層函式,但是頂層函式不能訪問類的private成員。 特別是Java中常見的工廠方法和類中需要使用的static成員該如何定義那?就像這樣的:

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

        public static B newInstance() {
            return new B();
        }
    }
複製程式碼

這時候就要使用伴生物件了。伴生物件是在類中定義的物件前新增一個特殊的關鍵字來標記:companion 。這樣做,就獲得了直接通過容器類名稱來訪問這個這個物件的方法和屬性的能力,不再需要顯示得指明物件的名稱,最終的語法看起來非常像Java中的靜態方法呼叫:

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

A.newInstance()
A.tag
複製程式碼

作為普通物件使用的伴生物件

伴生物件本質也是一個普通物件,普通物件可以做的一切伴生物件都可以,例如實現介面。 之所以看上去奇怪,是因為之前我們只是省略它的類名,也可以給它加上類名:

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

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

如果省略了伴生物件的名字,預設的名字將會是Companion。這點在將程式碼轉換成Java程式碼後就出現了:

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() {
      }
   }
}
複製程式碼

所以,你應該理解在Java中呼叫伴生物件的屬性是這樣的了:A. Companion.newInstance() 。 為了讓Java中呼叫也有一致的體驗,可以在對應的成員上使用@JvmStatic註解來達到這個目的。如果你想宣告一個static欄位,可以在一個頂層屬性或者宣告在object中的屬性上使用@JvmField註解。

class A private constructor() {
    companion object{
        @JvmField
        val tag = "tag"
        @JvmStatic
        fun newInstance() = A()
    }
}
複製程式碼

既然伴生物件就是一個普通類,當然也是可以宣告擴充套件函式:

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

A.getFlag()
複製程式碼

物件表示式:改變寫法的匿名內部類

object關鍵字不僅僅能用來宣告單例式的物件,還能用來宣告匿名物件。我們來翻寫下Java中如下使用匿名內部類的程式碼:

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中使用匿名內部類:

fun main(args: Array<String>) {
    B().setListener(object : Listener {
        override fun onClick() {
        }
    })
}
複製程式碼

除了去掉了物件的名字外,語法時與物件宣告相同的。物件表示式宣告瞭一個類並建立了該類的一個例項,但是並沒有給這個類或是例項分配一個名字。通常來說它們都不需要名字,應為你會將這個物件用作一個函式呼叫的引數。如果你需要給物件分配一個名字,可以將其儲存到一個變數中。 與Java匿名內部類只能擴充套件一個類或實現一個介面不同,Kotlin的匿名物件可以實現多個介面。並且訪問建立匿名內部類的函式中的變數是沒有限制在final變數,還可以在物件表示式中修改變數的值:

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

同樣的,我們通過檢視轉換的Java程式碼還研究為什麼可以做到這些區別:

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++;
         }
      }));
   }
複製程式碼

可以看到這裡通過IntRef包裝了我們定義的clickCount,因此,final屬性宣告在了包裝類上。 那Kotlin的匿名物件可以實現多個介面,又是如何做的那?我又新定義了一個介面,讓匿名內部類同時實現兩個介面:

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);
   }
複製程式碼

出現了一個新東西<undefinedtype> 根據字面理解應該是一個未確定的型別,並且可以強轉成對應的介面,這個可能就不是Java的內容了,不清楚具體的實現是怎樣的。

相關文章