Skip to content

Latest commit

 

History

History
199 lines (144 loc) · 7.55 KB

6.Kotlin_多继承问题.md

File metadata and controls

199 lines (144 loc) · 7.55 KB

6.Kotlin_多继承问题

继承和实现是面向对象程序设计中不变的主题。Java是不支持类的多继承的,Kotlin亦是如此。他们为什么要这样设计呢? 现实中,其实多继承的需求经常会出现,然而类的多继承方式会导致进程关系上语义的混淆。

骡子的多继承困惑

如果你了解C++,应该知道C++中的类是支持多重继承机制的。然而,C++中存在一个经典的钻石问题--骡子的多继承困惑。我们假设Java的类也支持多继承,然后模仿C++中类似的语法,来看看它到底会导致什么问题:

abstract class Animal {
    abstract public void run();
}
class Horse extends Animal {
    @Override
    public void run() {
        System.out.println("I am run very fast");
    }
}
class Donkey extends Animal {
     @Override
    public void run() {
        System.out.println("I am run very slow");
    }
}
class Mule extends Horse, Donkey {
    // 骡子
    ...
}

这是一段伪代码,这段代码的含义是:

  • 马和驴都继承了Animal类,并实现了Animal中的run抽象方法。
  • 骡子是马和驴的杂交产物,它拥有两者的特性,于是Mule利用多继承同时继承了Horse和Donkey

目前看起来没有问题,然而当我们打算在Mule中实现run方法的时候,问题就产生了:Mule到底是继承Horse的run方法还是Donkey的run方法?这个就是经典的钻石问题。

image-20210325104002353

所以钻石问题也被称为棱形继承问题。可以发现,类的多继承如果使用不当,就会在继承关系上产生歧义。而且,多继承还会给代码维护带来很多困扰:一来代码的耦合度会提高,二来各种类之间的关系令人眼花缭乱。

于是Kotlin和Java一样只支持类的单继承。那么面对多继承的需求,在Kotlin中该如何解决呢?

接口实现多继承

接口支持多实现,所以一个类可以实现多个接口,这是Java经常干的事。Kotlin的接口与Java和类似,但它除了可以定义带默认实现的方法之外,还可以声明抽象的属性。下面就是用Kotlin中的接口来实现多继承:

interface Flyer {
    fun fly()
    fun kind() = "flying animals"
}
interface Animal {
    val name: String 
    fun eat()
    fun kind() = "flying animals"
}

class Bird(override val name: String) : Flyer, Animal {
    override fun eat() {
        println("I can eat")
    }
    override fun fly() {
        println("I can fly")
    }
    override fun kind() = super<Flyer>.kind()
}

fun main(args: Array<String) {
    val bird = Bird("sparrow")
    println(bird.kind())
}
// 运行结果
flying animals

上面Bird类同时实现了Flyer和Animal两个接口,但由于它们都拥有默认的kind方法,同样会引起上面所说的钻石问题。而Kotlin提供了对应的方式来解决这个问题,那就是super关键字,我们可以利用它来指定继承哪个父类接口的方法,比如上面代码中的super.kind()。当然我们也可以主动实现方法,覆盖父接口的方法。如:

override fun kind() = "a flying ${this.name}"
// 最终的执行结果就是
a flying sparrow

通过这个例子,可以看出实现接口的语法:

  • 在Kotlin中实现一个接口时,需要实现接口中没有默认实现的方法及未初始化的属性,若同时实现多个接口,而接口间又有相同方法名的默认实现时,则需要主动指定使用哪个接口的方法或者重写方法。
  • 如果是默认的接口方法,你可以在实现类中通过super这种方式调用它,其中T为拥有该方法的接口名。
  • 在实现接口的属性和方法时,都必须带上override关键字,不能省略。

内部类解决多继承问题

在Java中可以将一个类的定义放在另一个类的定义内部,这就是内部类。由于内部类可以继承一个与外部无关的类,所以这保证了内部类的独立性,可以用它这个特性来尝试解决多继承的问题。

open class Horse {
    fun runFast() {
        println("I can run fast")
    }
}
open class Donkey {
    fun doLongTimeThing() {
        println("I can do some thing long time")
    }
}
class Mule {
    fun runFast() {
        HorseC().runFast()
    }
    fun doLongTimeThing() {
        DonkeyC().doLongTimeThing()
    }
    
    private inner class HorseC : Horse()
    private inner class DonkeyC : Donkey()
}

上面的例子可以看到:

  • 可以在一个类的内部定义多个内部类,每个内部类的实例都有自己的独立状态,它们与外部对象的信息相互独立。
  • 通过让内部类HorseC、DonkeyC分别继承Horse和Donkey这两个外部类,我们可以在Mule类中定义它们的实例对象,从而获得了Horse和Donkey两者不同的状态和行为。
  • 可以利用private修饰内部类,使得其他类都不能访问内部类,这样可以具有非常良好的封闭性。

所以在某些场合下,内部类确实是一种解决多继承非常好的思路。

使用委托代替多继承

委托是一种特殊的类型,用于方法事件委托,比如你调用A类的methodA方法,其实背后是B类的methodA去执行。

印象中,要实现委托并不是一件非常自然直观的事情。但庆幸的是,Kotlin简化了这种语法,我们只需要通过by关键字就可以实现委托的效果。比如之前提过的by lazy语法,其实就是利用委托实现的延迟初始化语法。

val laziness: String by lazy {
    println("I will hava a value")
    "I an a lazy initialized string"
}

下面通过委托来替代多继承实现需求:

interface CanFly {
    fun fly()
}
interface CanEat {
    fun eat()
}

open class Flyer : CanFly {
    override fun fly() {
        println("I can fly")
    }
}
open class Animal : CanEat {
    override fun eat() {
        println("I can eat")
    }
}
class Bird(flyer: Flyer, animal: Animal) : CanFly by flyer, CanEat by animal {}

fun main(args: Array<String>) {
    val flyer = Flyer()
    val animal = Animal()
    val b = Bird(flyer, animal)
    b.fly()
    b.eat()
}

有人可能会有疑问: 首先,委托方式怎么跟接口实现多继承如此相似,而且好像也并没有简单多少。其次,这种方式好像跟组合也很像,那么它到底有什么优势? 主要有以下两点:

  • 前面说到接口是无状态的,所以即使它提供了默认方法实现也是很简单的,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。我们可以利用上面委托的这种方式,虽然它也是接口委托,但它是用一个具体的类去实现方法逻辑,可以拥有更强大的能力。

  • 假设我们需要继承的类是A,委托对象是B、C,我们在具体调用的时候并不是像组合一样A.B.method,而是可以直接调用A.method,这更能表达A拥有该method的能力,更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的。

  • 上一篇:5.Kotlin_内部类&密封类&枚举&委托

  • 下一篇:7.Kotlin_注解&反射&扩展