继承和实现是面向对象程序设计中不变的主题。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方法?这个就是经典的钻石问题。
所以钻石问题也被称为棱形继承问题。可以发现,类的多继承如果使用不当,就会在继承关系上产生歧义。而且,多继承还会给代码维护带来很多困扰:一来代码的耦合度会提高,二来各种类之间的关系令人眼花缭乱。
于是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的能力,更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的。
- 邮箱 :charon.chui@gmail.com
- Good Luck!