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

寫在開頭:本人打算開始寫一個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 成員只在類和它的子類中可見,即同一個包是不可見的。
同時還要注意類的擴展函數不能訪問類的protectedprotected 成員。

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的內容了,不清楚具體的實現是怎樣的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。