Kotlin知識歸納(四) —— 介面和類

大棋發表於2019-05-30

前序

    Kotlin的類和介面與Java的類和介面存在較大區別,本次主要歸納Kotlin的介面和類如何定義、繼承以及其一些具體細節,同時檢視其對應的Java層實現。

帶預設方法的介面

    Kotlin介面可以包含抽象方法以及非抽象方法的實現(類似Java 8的預設方法)

interface MyInterface {
    //抽象方法
    fun daqi()
    //非抽象方法(即提供預設實現方法)
    fun defaultMethod() {
    }
}
複製程式碼

    介面也可以定義屬性。宣告的屬性可以是抽象的,也可以是提供具體訪問器實現的(即不算抽象的)。

interface MyInterface {
    //抽象屬性
    var length:Int
	//提供訪問器的屬性
    val name:String
        get() = ""

    //抽象方法
    fun daqi()
    //非抽象方法(即提供預設實現方法)
    fun defaultMethod() {
    }
}
複製程式碼

    介面中宣告的屬性不能有幕後欄位。因為介面是無狀態的,因此介面中宣告的訪問器不能引用它們。(簡單說就是介面沒有具體的屬性,不能用幕後欄位對屬性進行賦值)

Kotlin知識歸納(四) —— 介面和類

介面的實現

    Kotlin使用 : 替代Java中的extends 和 implements 關鍵字。Kotlin和Java一樣,一個類可以實現任意多個介面,但是隻能繼承一個類。

    介面中抽象的方法和抽象屬性,實現介面的類必須對其提供具體的實現。

    對於在介面中提供預設實現的介面方法和提供具體訪問器的屬性,可以對其進行覆蓋,重新實現方法和提供新的訪問器實現。

class MyClass:MyInterface{
    //原抽象屬性,提供具體訪問器
    //不提供具體訪問器,提供初始值,使用預設訪問器也是沒有問題的
    override var length: Int = 0
    /*override var length: Int
        get() = 0
        set(value) {}*/
    
    //覆蓋提供好訪問器的介面屬性
    override val name: String
        //super.name 其實是呼叫介面中定義的訪問器
        get() = super.name
    
    //原抽象方法,提供具體實現
    override fun daqi() {
    }

    //覆蓋預設方法
    override fun defaultMethod() {
        super.defaultMethod()
    }
}
複製程式碼

    無論是從介面中獲取的屬性還是方法,前面都帶有一個override關鍵字。該關鍵字與Java的@Override註解類似,重寫父類或介面的方法屬性時,都 強制 需要用override修飾符進行修飾。因為這樣可以避免先寫出實現方法,再新增抽象方法造成的意外重寫

介面的繼承

    介面也可以從其他介面中派生出來,從而既提供基類成員的實現,也可以宣告新的方法和屬性。

interface Name {
    val name:String
}

interface Person :Name{
    fun learn()
}

class daqi:Person{
    //為父介面的屬性提供具體的訪問器
    override val name: String
        get() = "daqi"
    
    //為子介面的方法提供具體的實現
    override fun learn() {
    }
}
複製程式碼

覆蓋衝突

    在C++中,存在菱形繼承的問題,即一個類同時繼承具有相同函式簽名的兩個方法,到底該選擇哪一個實現呢?由於Kotlin的介面支援預設方法,當一個類實現多個介面,同時擁有兩個具有相同函式簽名的預設方法時,到底選擇哪一個實現呢?

主要根據以下3條規則進行判斷:

    1、類中帶override修飾的方法優先順序最高。 類或者父類中帶override修飾的方法的優先順序高於任何宣告為預設方法的優先順序。(Kotlin編譯器強制要求,當類中存在和父類或實現的介面有相同函式簽名的方法存在時,需要在前面新增override關鍵字修飾。)

    2、當第一條無法判斷時,子介面的優先順序更高。優先選擇擁有最具體實現的預設方法的介面,因為從繼承角度理解,可以認為子介面的預設方法覆蓋重寫了父介面的預設方法,子介面比父介面具體。

    3、最後還是無法判斷時,繼承多個介面的類需要顯示覆蓋重寫該方法,並選擇呼叫期望的預設方法。

  • 如何理解第二條規則?先看看一下例子:

Java繼承自Language,兩者都對use方法提供了預設實現。而Java比Language更具體。

interface Language{
    fun use() = println("使用語言")
}

interface Java:Language{
    override fun use() = println("使用Java語言程式設計")
}
複製程式碼

而實現這兩個介面的類中,並無覆蓋重寫該方法,只能選擇更具體的預設方法作為其方法實現。

class Person:Java,Language{
}

//執行結果是輸出:使用Java語言程式設計
val daqi = Person()
daqi.use()
複製程式碼
  • 如何理解第三條規則?繼續看例子:

介面Java和Kotlin都提供對learn方法提供了具體的預設實現,且兩者並無明確的繼承關係。

interface Java {
    fun learn() = println("學習Java")
}

interface Kotlin{
    fun learn() = println("學習Kotlin")
}
複製程式碼

當某類都實現Java和Kotlin介面時,此時就會產生覆蓋衝突的問題,這個時候編譯器會強制要求你提供自己的實現:

Kotlin知識歸納(四) —— 介面和類

唯一的解決辦法就是顯示覆蓋該方法,如果想沿用介面的預設實現,可以super關鍵字,並將具體的介面名放在super的尖括號中進行呼叫。

class Person:Java,Kotlin{
    override fun learn() {
        super<Java>.learn()
        super<Kotlin>.learn()
    }
}
複製程式碼

對比Java 8的介面

    Java 8中也一樣可以為介面提供預設實現,但需要使用default關鍵字進行標識。(Kotlin只需要提供具體的方法實現,即提供函式體)

public interface Java8 {
    default void defaultMethod() {
        System.out.println("我是Java8的預設方法"); 
    }
} 
複製程式碼

    面對覆蓋衝突,Java8的和處理和Kotlin的基本相似,在語法上顯示呼叫介面的預設方法時有些不同:

//Java8 顯示呼叫覆蓋衝突的方法
Java8.super.defaultMethod()
    
//Kotlin 顯示呼叫覆蓋衝突的方法
super<Kotlin>.learn()
複製程式碼

Kotlin 與 Java 間介面的互動

    眾所周知,Java8之前介面沒有預設方法,Kotlin是如何相容的呢?定義如下兩個介面,再檢視看一下反編譯的結果:

interface Language{
    //預設方法
    fun use() = println("使用語言程式設計")
}


interface Java:Language{
    //抽象屬性
    var className:String

    //提供訪問器的屬性
    val field:String
        get() = ""

    //預設方法
    override fun use() = println("使用Java語言程式設計")

    //抽象方法
    fun absMethod()
}
複製程式碼

先檢視父介面的原始碼:

public interface Language {
   void use();

   public static final class DefaultImpls {
      public static void use(Language $this) {
         String var1 = "使用語言程式設計";
         System.out.println(var1);
      }
   }
}
複製程式碼

    Language介面中的預設方法轉換為抽象方法保留在介面中。其內部定義了一個名為DefaultImpls的靜態內部類,該內部類中擁有和預設方法相同名稱的靜態方法,而該靜態方法的實現就是其同名預設函式的具體實現。也就是說,Kotlin的預設方法轉換為靜態內部類DefaultImpls的同名靜態函式。

所以,如果想在Java中呼叫Kotlin介面的預設方法,需要加多一層DefaultImpls

public class daqiJava implements Language {
    @Override
    public void use() {
        Language.DefaultImpls.use(this);
    }
}
複製程式碼

再繼續檢視子介面的原始碼

public interface Java extends Language {
   //抽象屬性的訪問器
   @NotNull 
   String getClassName();
   void setClassName(@NotNull String var1);

   //提供具體訪問器的屬性
   @NotNull 
   String getField();

    //預設方法
   void use();
    
    //抽象方法
   void absMethod();
    
   public static final class DefaultImpls {
      @NotNull
      public static String getField(Java $this) {
         return "";
      }

      public static void use(Java $this) {
         String var1 = "使用Java語言程式設計";
         System.out.println(var1);
      }
   }
}
複製程式碼

    通過原始碼觀察到,無論是抽象屬性還是擁有具體訪問器的屬性,都沒有在介面中定義任何屬性,只是宣告瞭對應的訪問器方法。(和擴充套件屬性相似)

抽象屬性和提供具體訪問器的屬性區別是:

  • 抽象屬性的訪問器均為抽象方法。
  • 擁有具體訪問器的屬性,其訪問器實現和預設方法一樣,外部宣告一個同名抽象方法,具體實現被儲存在靜態內部類DefaultImpls的同名靜態函式中。

Java定義的介面,Kotlin繼承後能為其父介面的方法提供預設實現嗎?當然是可以啦:

//Java介面
public interface daqiInterface {
    String name = "";
    
    void absMethod();
}

//Kotlin介面
interface daqi: daqiInterface {
    override fun absMethod() {

    }
}
複製程式碼

    Java介面中定義的屬性都是預設public static final,對於Java的靜態屬性,在Kotlin中可以像頂層屬性一樣,直接對其進行使用:

fun main(args: Array<String>) {
    println("Java介面中的靜態屬性name = $name")
}
複製程式碼

    Kotlin的類可以有一個主建構函式以及一個或多個 從建構函式。主建構函式是類頭的一部分,即在類體外部宣告。

主構造方法

constructor關鍵字可以用來宣告 主構造方法 或 從構造方法。

class Person(val name:String)
//其等價於
class Person constructor(val name:String)
複製程式碼

    主建構函式不能包含任何的程式碼。初始化的程式碼可以放到以 init 關鍵字作為字首的初始化塊中。

class Person constructor(val name:String){
    init {
        println("name = $name")
    }
}

複製程式碼

    構造方法的引數也可以設定為預設引數,當所有構造方法的引數都是預設引數時,編譯器會生成一個額外的不帶引數的構造方法來使用所有的預設值

class Person constructor(val name:String = "daqi"){
    init {
        println("name = $name")
    }
}

//輸出為:name = daqi
fun main(args: Array<String>) {
    Person()
}
複製程式碼

    主構造方法同時需要初始化父類,子類可以在其列表引數中索取父類構造方法所需的引數,以便為父類構造方法提供引數。

open class Person constructor(name:String){
}

class daqi(name:String):Person(name){
}
複製程式碼

    當沒有給一個類宣告任何構造方法,編譯器將生成一個不做任何事情的預設構造方法。對於只有預設構造方法的類,其子類必須顯式地呼叫父類的預設構造方法,即使他沒有引數。

open class View
    
class Button:View()
複製程式碼

而介面沒有構造方法,所以介面名後不加括號。

//實現介面
class Button:ClickListener
複製程式碼

當 主構造方法 有註解或可見性修飾符時,constructor 關鍵字不可忽略,並且constructor 在這些修飾符和註解的後面。

class Person public @Inject constructor(val name:String)
複製程式碼

構造方法的可見性是 public,如果想將構造方法設定為私有,可以使用private修飾符。

class Person private constructor()
複製程式碼

從構造方法

從構造方法使用constructor關鍵字進行宣告

open class View{
    //從構造方法1
    constructor(context:Context){
    }
	
    //從構造方法2
    constructor(context:Context,attr:AttributeSet){
    }
}
複製程式碼

    使用this關鍵字,從一個構造方法中呼叫該類另一個構造方法,同時也能使用super()關鍵字呼叫父類構造方法。

    如果一個類有 主構造方法,每個 從構造方法 都應該顯式呼叫 主構造方法,否則將其委派給會呼叫主構造方法的從構造方法。

class Person constructor(){
    //從構造方法1,顯式呼叫主構造方法
    constructor(string: String) : this() {
        println("從構造方法1")
    }
	
    //從構造方法2,顯式呼叫構造方法1,間接呼叫主構造方法。
    constructor(data: Int) : this("daqi") {
        println("從構造方法2")
    }
}
複製程式碼

注意

    初始化塊中的程式碼實際上會成為主建構函式的一部分。顯式呼叫主構造方法會作為次建構函式的第一條語句,因此所有初始化塊中的程式碼都會在次建構函式體之前執行。

即使該類沒有主建構函式,這種呼叫仍會隱式發生,並且仍會執行初始化塊。

//沒有主構造方法的類
class Person{
    init {
        println("主構造方法 init 1")
    }
	
    //從構造方法預設會執行所有初始化塊
    constructor(string: String) {
        println("從構造方法1")
    }

    init {
        println("主構造方法 init 2")
    }
}
複製程式碼

Kotlin知識歸納(四) —— 介面和類

    如果一個類擁有父類,但沒有主構造方法時,每個從構造方法都應該初始化父類(即呼叫父類的構造方法),否則將其委託給會初始化父類的構造方法(即使用this呼叫其他會初始化父類的構造方法)。

class MyButton:View{
    //呼叫自身的另外一個從構造方法,間接呼叫父類的構造方法。
    constructor(context:Context):this(context,MY_STYLE){
    }
	//呼叫父類的構造方法,初始化父類。
    constructor(context:Context,attr:AttributeSet):super(context,attr){
    }
}
複製程式碼

脆弱的基類

    Java中允許建立任意類的子類並重寫任意方法,除非顯式地使用final關鍵字。對基類進行修改導致子類不正確的行為,就是所謂的脆弱的基類。所以Kotlin中類和方法預設是final,Java類和方法預設是open的

    當你允許一個類存在子類時,需要使用open修飾符修改這個類。如果想一個方法能被子類重寫,也需要使用open修飾符修飾。

open class Person{
    //該方法時final 子類不能對它進行重寫
    fun getName(){}
    
    //子類可以對其進行重寫
    open fun getAge(){}
}
複製程式碼

對基類或介面的成員進行重寫後,重寫的成員同樣預設為open。(儘管其為override修飾)

如果想改變重寫成員預設為open的行為,可以顯式的將重寫成員標註為final

open class daqi:Person(){
    final override fun getAge() {
        super.getAge()
    }
}
複製程式碼

抽象類的成員和介面的成員始終是open的,不需要顯式地使用open修飾符。

Kotlin知識歸納(四) —— 介面和類

可見性修飾符

    Kotlin和Java的可見性修飾符相似,同樣可以使用public、protected和private修飾符。但Kotlin預設可見性是public,而Java預設可見性是包私有

    Kotlin中並沒有包私有這種可見性,Kotlin提供了一個新的修飾符:internal,表示“只在模組內部可見”。模組是指一組一起編譯的Kotlin檔案。可能是一個Gradle專案,可能是一個Idea模組。internal可見性的優勢在於它提供了對模組實現細節的封裝。

    Kotlin允許在頂層宣告中使用private修飾符,其中包括類宣告,方法宣告和屬性宣告,但這些宣告只能在宣告它們的檔案中可見。

注意

  • 覆蓋一個 protected 成員並且沒有顯式指定其可見性,該成員的可見性還是 protected 。
  • 與Java不同,Kotlin的外部類(巢狀類)不能看到其內部類中的private成員。
  • internal修飾符編譯成位元組碼轉Java後,會變成public。
  • private類轉換為Java時,會變成包私有宣告,因為Java中類不能宣告為private。

內部類和巢狀類

    Kotlin像Java一樣,允許在一個類中宣告另一個類。但Kotlin的巢狀類預設不能訪問外部類的例項,和Java的靜態內部類一樣。

    如果想讓Kotlin內部類像Java內部類一樣,持有一個外部類的引用的話,需要使用inner修飾符。

Kotlin知識歸納(四) —— 介面和類

內部類需要外部類引用時,需要使用 this@外部類名 來獲取。

class Person{
    private val name  = "daqi"
    
    inner class MyInner{
        fun getPersonInfo(){
            println("name = ${this@Person.name}")
        }
    }
}
複製程式碼

object關鍵字

物件宣告

    在Java中建立單例往往需要定義一個private的構造方法,並建立一個靜態屬性來持有這個類的單例。

    Kotlin通過物件宣告將類宣告和類的單一例項結合在一起。物件宣告在定義的時候就立即建立,而這個初始化過程是執行緒安全的。

    物件宣告中可以包含屬性、方法、初始化語句等,也支援繼承類和實現介面,唯一不允許的是不能定義構造方法(包括主構造方法和從構造方法)。

    物件宣告不能定義在方法和內部類中,但可以定義在其他的物件宣告和非內部類(例如:巢狀類)。如果需要引用該物件,直接使用其名稱即可。

//定義物件宣告
class Book private constructor(val name:String){

    object Factory {
        val name = "印書廠"

        fun createAppleBooK():Book{
            return Book("Apple")
        }

        fun createAndroidBooK():Book{
            return Book("Android")
        }
    }
}
複製程式碼

呼叫物件宣告的屬性和方法:

Book.Factory.name
Book.Factory.createAndroidBooK()
複製程式碼

    將物件宣告反編譯成Java程式碼,其內部實現也是定義一個private的構造方法,並始終建立一個名為INSTANCE的靜態屬性來持有這個類的單例,而該類的初始化放在靜態程式碼塊中。

public final class Book {
   //....

   public Book(String name, DefaultConstructorMarker $constructor_marker) {
      this(name);
   }

   public static final class Factory {
      @NotNull
      private static final String name = "印書廠";
      public static final Book.Factory INSTANCE;

      //...

      @NotNull
      public final Book createAppleBooK() {
         return new Book("Apple", (DefaultConstructorMarker)null);
      }

      @NotNull
      public final Book createAndroidBooK() {
         return new Book("Android", (DefaultConstructorMarker)null);
      }

      private Factory() {
      }

      static {
         Book.Factory var0 = new Book.Factory();
         INSTANCE = var0;
         name = "印書廠";
      }
   }
}
複製程式碼

用Java呼叫物件宣告的方法:

//Java呼叫物件宣告
Book.Factory.INSTANCE.createAndroidBooK();
複製程式碼

伴生物件

    一般情況下,使用頂層函式可以很好的替代Java中的靜態函式,但頂層函式無法訪問類的private成員。

    當需要定義一個方法,該方法能在沒有類例項的情況下,呼叫該類的內部方法。可以定義一個該類的物件宣告,並在該物件宣告中定義該方法。類內部的物件宣告可以用 companion 關鍵字標記,這種物件叫伴生物件。

    可以直接通過類名來訪問該伴生物件的方法和屬性,不用再顯式的指明物件宣告的名稱,再訪問該物件宣告物件的方法和屬性。可以像呼叫該類的靜態函式和屬性一樣,不需要再關心物件宣告的名稱。

//將構造方法私有化
class Book private constructor(val name:String){
    //伴生物件的名稱可定義也可以不定義。
    companion object {
        //伴生物件呼叫其內部私有構造方法
        fun createAppleBooK():Book{
            return Book("Apple")
        }

        fun createAndroidBooK():Book{
            return Book("Android")
        }
    }
}
複製程式碼

呼叫伴生物件的方法:

Book.createAndroidBooK()
複製程式碼

    伴生物件的實現和物件宣告類似,定義一個private的構造方法,並始終建立一個名為Companion的靜態屬性來持有這個類的單例,並直接對Companion靜態屬性進行初始化。

public final class Book {
   //..
   public static final Book.Companion Companion = new Book.Companion((DefaultConstructorMarker)null);

    //...

   public static final class Companion {
     //...
      @NotNull
      public final Book createAppleBooK() {
         return new Book("Apple", (DefaultConstructorMarker)null);
      }

      @NotNull
      public final Book createAndroidBooK() {
         return new Book("Android", (DefaultConstructorMarker)null);
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
複製程式碼

伴生物件的擴充套件

    擴充套件方法機制允許在任何地方定義某類的擴充套件方法,但需要該類的例項進行呼叫。當需要擴充套件一個通過類自身呼叫的方法時,如果該類擁有伴生物件,可以通過對伴生物件定義擴充套件方法

//對伴生物件定義擴充套件方法
fun Book.Companion.sellBooks(){
}
複製程式碼

當對該擴充套件方法進行呼叫時,可以直接通過類自身進行呼叫:

Book.sellBooks()
複製程式碼

匿名內部類

作為android開發者,在設定監聽時,建立匿名物件的情況再常見不過了。

mButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        
    }
});
複製程式碼

    object關鍵字除了能用來宣告單例式物件外,還可以宣告匿名物件。和物件宣告不同,匿名物件不是單例,每次都會建立一個新的物件例項。

mRecyclerView.setOnClickListener(object :View.OnClickListener{
    override fun onClick(v: View?) {
        
    }
});
複製程式碼

    當該匿名類擁有兩個以上抽象方法時,才需要使用object建立匿名類。否則儘量使用lambda表示式。

mButton.setOnClickListener {
}
複製程式碼

參考資料:

android Kotlin系列:

Kotlin知識歸納(一) —— 基礎語法

Kotlin知識歸納(二) —— 讓函式更好呼叫

Kotlin知識歸納(三) —— 頂層成員與擴充套件

Kotlin知識歸納(四) —— 介面和類

Kotlin知識歸納(五) —— Lambda

Kotlin知識歸納(六) —— 型別系統

Kotlin知識歸納(七) —— 集合

Kotlin知識歸納(八) —— 序列

Kotlin知識歸納(九) —— 約定

Kotlin知識歸納(十) —— 委託

Kotlin知識歸納(十一) —— 高階函式

Kotlin知識歸納(十二) —— 泛型

Kotlin知識歸納(十三) —— 註解

Kotlin知識歸納(十四) —— 反射

相關文章