转载

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

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

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知识归纳(四) —— 接口和类

接口的实现

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 {
}
复制代码
原文  https://juejin.im/post/5cecbb79f265da1bb564d860
正文到此结束
Loading...