Kotlin 抽象与接口

上篇文章我们一起进入了 Kotlin 面向对象的世界,从这篇文章开始将继续探讨 Kotlin 面向对象中的继承与接口。其实在 Kotlin 中继承、接口大部分和 Java 是一样的,但是在语法层面支持是不一样。因为 Kotlin 会有一层语法糖可以很方便高效地声明某个语法,从而让你把更多精力专注在业务逻辑上,而不是语法代码模板上。然后我们还会一起来聊下 Kotlin 多继承的实现,Kotlin 和 Java 一样都是单继承,这一点是毋庸置疑的,但是我们也会需要多继承场景,那么 Kotlin 是怎么解决这样场景的呢?大家肯定想到的是接口多继承,具体怎么一起来看看吧。

1. 抽象与接口

与 Java 一样的是 Kotlin 也是使用 abstractinterface 来分别声明抽象类和接口,除此之外 Kotlin 的接口内部还支持非抽象方法的实现 (这一点和 Java8 中 default 方法很类似),但是需要注意内部不能包含任何的状态 (纯函数的形式)。

1.1 抽象类声明

在 Kotlin 中抽象类的声明使用 abstract 关键字,抽象类中方法使用 abstract 声明抽象方法。

//以Random.kt源码为例
public abstract class Random {//使用abstract关键声明一个抽象类Random
    public abstract fun nextBits(bitCount: Int): Int //与Java一样使用abstract声明一个抽象类中抽象方法,所以子类必须要实现该方法
    
    public open fun nextInt(): Int = nextBits(32)//open表示这个类可以被子类重写

    public fun nextInt(until: Int): Int = nextInt(0, until)//由于Kotlin默认是final且没有显式open,所以该方法不能被子类重写
    ...
}

1.2 接口声明

在 Kotlin 中接口的声明使用 interface 关键字:

interface OnClickListener {//使用interface关键字声明一个接口
    fun onClick() //声明了一个接口抽象方法,所有实现这个接口的非抽象类都需要实现这个方法
}

在 Kotlin 中实现一个简单的接口:

class Button: OnClickListener {
   override fun onClick() = println("Button is Clicked") //与Java不同的是在Kotlin中override必须是强制要求的
}

2. Kotlin 中带默认方法的接口

我们都知道在 Java8 以下版本中,接口中不能存在带实现方法的。直到 Java8 出现 default 方法,那么在 Java8 中可以声明带实现的方法。

//java8实现
public interface OnClickListener {
    public void onClick();
    
    default public void onClickLog() {//使用default关键字声明接口中一个带实现的方法,这个Java8以下版本是无法做到
       System.out.println("clicked!");
    }
}

我们看了 Java8 中是如何实现带默认方法的接口的,那么在 Kotlin 中是如何做到的呢?其实在 Kotlin 语法中天然就支持带默认实现的方法,不需要添加任何的关键字或修饰符。

//kotlin实现
interface OnClickListener {
    fun onClick()
    fun onClickLog() = println("clicked!")//不需要声明任何关键字,直接支持带默认实现的方法
}

那么问题就来了,我们都知道 Kotlin 是完全兼容到 Java6 的,然而 Java8 以下是不支持这种带默认实现的接口方法,那么 Kotlin 它是怎么做到 Java8 以下版本完全兼容这种语法特性呢?一起来反编译它的 Kotlin 代码就一目了然了。

//反编译后java代码
public interface OnClickListener {
   void onClick();

   void onClickLog();

   @Metadata(
      mv = {1, 1, 16},
      bv = {1, 0, 3},
      k = 3
   )
   public static final class DefaultImpls {//可以看到这边自动生成一个DefaultImpls静态类
      public static void onClickLog(OnClickListener $this) {//默认实现方法onClickLog被声明成一个静态方法
         String var1 = "Clicked!";
         boolean var2 = false;
         System.out.println(var1);
      }
   }
}

可能你看到上面代码还是不是很直观,不知道它是如何触发 DefaultImpls 调用的,所以我们可以把上面代码添加一个实现类,就能看到如何调用的了。

package com.imooc.test

interface OnClickListener {
    fun onClick()

    fun onClickLog() = println("Clicked!")
}

class Button : OnClickListener {//Button实现类
    override fun onClickLog() {//重写onClickLog方法
        super.onClickLog()//默认通过super调用父类默认实现的方法
    }

    override fun onClick() {

    }
}

反编译后的 Java 代码:

// OnClickListener.java
public interface OnClickListener {
   void onClick();

   void onClickLog();

   @Metadata(
      mv = {1, 1, 16},
      bv = {1, 0, 3},
      k = 3
   )
   public static final class DefaultImpls {
      public static void onClickLog(OnClickListener $this) {
         String var1 = "Clicked!";
         boolean var2 = false;
         System.out.println(var1);
      }
   }
}
// Button.java
public final class Button implements OnClickListener {
   public void onClickLog() {
      OnClickListener.DefaultImpls.onClickLog(this);//现在可以看到实际上通过接口类名调用它内部静态类DefaultImpls,再通过静态类DefaultImpls调用它的静态方法onClickLog
   }

   public void onClick() {
   }
}

所以总结一下,Kotlin 中接口默认实现方法是如何兼容 Java8 以下版本的它实际上就是在接口内部生成了一个静态类 DefaultImpls ,并在静态类内部生成对应默认实现静态方法。然后调用的时候只需要通过接口名。静态类 DefaultImpls . 默认实现静态方法名调用即可。

3. Kotlin 中接口的多继承

我们都知道在 Java 中是不支持多继承的,然而 Kotlin 也一样不支持类的多继承。可是为什么要这么设计呢,但是相信很多小伙伴应该有过这样的感受平时开发中依然遇到类似多继承的场景。

3.1 为什么不支持类的多继承

我相信大家都知道经典的多继承问题,俗称 “钻石继承问题”。我们用反证法,假设 Java/Kotlin 中支持类的多继承,一起来看个例子,对于 A 类中有一个 invoke 方法,B,C 两个类都去继承 A 类,然后 D 类去分别去继承 B,C 类。

abstract class A {
   abstract fun invoke()
}

class B: A {
   override fun invoke() = println("B invoke")
}

class C: A {
   override fun invoke() = println("C invoke")
}

class D: B,C {//假设支持类的多继承
    override fun invoke() = println("C invoke")// B ? C
}

图片描述

那么问题就来了 D 类应该是继承 B 类 invoke 方法,还是 C 类 invoke 方法呢?所以这样类的多继承很容易带来歧义。

但是我们知道在开发过程中还是可能遇到多继承的问题,我们一般常用的方法是采用接口多继承方式来解决,因为我们知道在 Java 和 Kotlin 中是支持接口的多继承的。

3.2 Kotlin 中接口的多继承

在 Java 中接口多继承是支持的,Kotlin 依然也支持。那么一起来看下在 Kotlin 对于上述多继承问题是如何解决的呢?

package com.imooc.test

interface A {
    fun invoke()
}

interface B : A {
    override fun invoke() {
        println("B invoke")
    }
}

interface C : A {
    override fun invoke() {
        println("C invoke")
    }
}

class D : B, C {
    //override fun invoke() = super<B>.invoke()//通过super中泛型类型指定继承B接口的方法,所以最后输出"B invoke"
    override fun invoke() = super<C>.invoke()//通过super中泛型类型指定继承C接口的方法,所以最后输出"C invoke"
}

fun main() {
    val d = D()
    d.invoke()
}

4. 总结

到这里有关 Kotlin 中抽象与接口就结束,其实 Kotlin 中抽象和接口与 Java 中基本是相似的,只需要注意文章提到那几点不一样地方即可。多多对比多多体会,下一篇文章我们将继续探讨 Kotlin 面向对象中一些比较特殊的类,比如数据类、枚举类、密封类等。