阅读 703

Kotlin知识归纳(四) —— 接口和类

前序

    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使用 : 替代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接口时,此时就会产生覆盖冲突的问题,这个时候编译器会强制要求你提供自己的实现:

唯一的解决办法就是显示覆盖该方法,如果想沿用接口的默认实现,可以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知识归纳(十四) —— 反射

关注下面的标签,发现更多相似文章
评论