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

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

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


定義類繼承結構

Kotlin中的接口

Kotlin的接口與Java 8 中的類似:它們能夠包含抽象方法(方法=函數)的定義以及非抽象方法的實現(與Java 8 中的默認方法相似),但它們不能包含任何狀態。 使用interface 關鍵字定義接口:設計模式

interface Clickable {
    fun click()
}
複製代碼

咱們聲明瞭一個擁有名爲click的抽象方法的接口。全部實現這個接口的非抽象類都須要提供這個方法的一個實現。咱們來實現如下這個接口:bash

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

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

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的接口特性。 有沒有發現這種實現方式其實與上一章的擴展函數很是相似?post

有一種特殊狀況:若是你的類實現了兩個接口,而且這兩個接口中分別定了同名的默認實現的方法,那這個時候這個類會採用那個接口的默認實現那? 答案是:任何一個都不會使用。取而代之的時,若是你沒有顯示實現這個同名接口,會獲得編譯錯誤的提示。學習

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()ui

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

相關文章
相關標籤/搜索