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知识概括(十四) —— 反射

相关文章
相关标签/搜索