Kotlin知識概括(四) —— 接口和類

前序

    Kotlin的類和接口與Java的類和接口存在較大區別,本次主要概括Kotlin的接口和類如何定義、繼承以及其一些具體細節,同時查看其對應的Java層實現。android

帶默認方法的接口

    Kotlin接口能夠包含抽象方法以及非抽象方法的實現(相似Java 8的默認方法)編程

interface MyInterface {
    //抽象方法
    fun daqi()
    //非抽象方法(即提供默認實現方法)
    fun defaultMethod() {
    }
}
複製代碼

    接口也能夠定義屬性。聲明的屬性能夠是抽象的,也能夠是提供具體訪問器實現的(即不算抽象的)。安全

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

    //抽象方法
    fun daqi()
    //非抽象方法(即提供默認實現方法)
    fun defaultMethod() {
    }
}
複製代碼

    接口中聲明的屬性不能有幕後字段。由於接口是無狀態的,所以接口中聲明的訪問器不能引用它們。(簡單說就是接口沒有具體的屬性,不能用幕後字段對屬性進行賦值)bash

接口的實現

    Kotlin使用 : 替代Java中的extends 和 implements 關鍵字。Kotlin和Java同樣,一個類能夠實現任意多個接口,可是隻能繼承一個類。ide

    接口中抽象的方法和抽象屬性,實現接口的類必須對其提供具體的實現。函數

    對於在接口中提供默認實現的接口方法和提供具體訪問器的屬性,能夠對其進行覆蓋,從新實現方法和提供新的訪問器實現。post

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修飾符進行修飾。由於這樣能夠避免先寫出實現方法,再添加抽象方法形成的意外重寫學習

接口的繼承

    接口也能夠從其餘接口中派生出來,從而既提供基類成員的實現,也能夠聲明新的方法和屬性。ui

interface Name {
    val name:String
}

interface Person :Name{
    fun learn()
}

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

覆蓋衝突

    在C++中,存在菱形繼承的問題,即一個類同時繼承具備相同函數簽名的兩個方法,到底該選擇哪個實現呢?因爲Kotlin的接口支持默認方法,當一個類實現多個接口,同時擁有兩個具備相同函數簽名的默認方法時,到底選擇哪個實現呢?this

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

    一、類中帶override修飾的方法優先級最高。 類或者父類中帶override修飾的方法的優先級高於任何聲明爲默認方法的優先級。(Kotlin編譯器強制要求,當類中存在和父類或實現的接口有相同函數簽名的方法存在時,須要在前面添加override關鍵字修飾。)

    二、當第一條沒法判斷時,子接口的優先級更高。優先選擇擁有最具體實現的默認方法的接口,由於從繼承角度理解,能夠認爲子接口的默認方法覆蓋重寫了父接口的默認方法,子接口比父接口具體。

    三、最後仍是沒法判斷時,繼承多個接口的類須要顯示覆蓋重寫該方法,並選擇調用指望的默認方法。

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

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接口時,此時就會產生覆蓋衝突的問題,這個時候編譯器會強制要求你提供本身的實現:

惟一的解決辦法就是顯示覆蓋該方法,若是想沿用接口的默認實現,能夠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")
    }
}
複製代碼

    若是一個類擁有父類,但沒有主構造方法時,每一個從構造方法都應該初始化父類(即調用父類的構造方法),不然將其委託給會初始化父類的構造方法(即便用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和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修飾符。

內部類須要外部類引用時,須要使用 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知識概括(十四) —— 反射

相關文章
相關標籤/搜索