目录

Kotlin 接口(类)委托

Kotlin 为什么默认是 final class?

面对对象的三大思想: 封装 继承 多态

对于继承类, 我们为了让子类访问而防止父类访问, 我们会使用protected关键字, 但是这样事实上是破坏了封装性, 子类很多方法要依赖于父类的实现, 一旦父类实现发生改变, 子类都会受影响, 举个例子, 我们想要给HashSet添加插入计数功能:

就这么简单的功能? 简单! 看我继承大法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class CountingSet<T> : HashSet<T>() {
    var count = 0
    override fun add(element: T): Boolean {
        count++
        return super.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        count += elements.size
        return super.addAll(elements)
    }
}
val countingSet = CountingSet<Int>()
countingSet.addAll(listOf(1, 2, 3))
println(countingSet.count) // 6

输出6, 为什么会产生这样的原因呢? 其实很简单, HashSet在使用addAll添加元素的时候会多次调用add方法, 导致数量被双倍记录了. 这时候你可能想喷我了: 直接add里面写就行不就行了? 你何必在addAll里面重写??

原因我先不说, 我们来看下一个例子, 给List添加增加计数方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class CountingList<T> : ArrayList<T>() {
    var count = 0
    override fun add(element: T): Boolean {
        count++
        return super.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        count += elements.size
        return super.addAll(elements)
    }
}
val countingList = CountingList<Int>()
countingList.addAll(listOf(1, 2, 3))
println(countingList.count) // 3

输出3, 你再看看上面HashSet那个例子, 明白为什么了吗? 什么? 不明白? 事实上, 不同于HashSet, ArrayList内部addAll其实是使用arrayCopy来实现的, 而不是反复调用add.

从上面的例子我们能发现什么? 继承是不可靠的, 在你不够完全了解父类的情况下, 这种继承是危险的, 可能产生不可预期的后果, 因为继承后的子类在向上转型调用父类方法的时候产生多态调用的父类的方法如果是子类重写过的方法就会调用子类重写过的方法, 而父类如果我们要完全了解甚至要知道父类的父类的实现…. 而且一旦父类更改实现, 子类的重写过的方法很可能又会出问题.

所以在Kotlin中, 所有类都默认不可继承, 因为继承并不优雅, 且不可靠.

不用继承? 那我们用什么?

在上面的例子中, 你也看到了继承的不可靠性, 事实上, 我们应该使用组合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class CountingSet2(val innerSet: HashSet<Int> = HashSet()) {
    var count = 0
    fun add(e: Int): Boolean {
        count++
        return innerSet.add(e)
    }

    fun addAll(es: Collection<Int>): Boolean {
        count += es.size
        return innerSet.addAll(es)
    }

    fun contains(e: Int) { innerSet.contains(e) }
    // ...
}

没错, 通过上面的方法, 我们做到了保证可靠的addaddAll方法, 但是我们想让CountingSet拥有HashSet的那些方法(比如说contains, clear…), 我们需要写一遍HashSet里面的所有方法, 而我们只是想重写addaddAll, 这样更谈不上优雅!

那…我们怎么办? 事实上, 如果父类我们可以进行操作, 我们可以让父类实现一个添加元素计数的接口, 然后我们去实现接口, 但是如果我们无力改变父类的实现, 我们应该怎么办?

Java中, 毫无办法, 你要么详细了解父类具体实现然后继承, 要么用组合重写全部方法, 这样都是不小的工作量! 如果你有办法, 欢迎邮箱给我, 我想学习一下!

Kotlin 可以更优雅吗

我们可以使用Kotlin接口委托, 并重写其中的add, addAll方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class CountingSet3(val innerSet: HashSet<Int> = HashSet()): 
	MutableCollection<Int> by innerSet {
    var count = 0
    override fun add(element: Int): Boolean {
        count++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<Int>): Boolean {
        count += elements.size
        return innerSet.addAll(elements)
    }
}

你可能会想问, MutableCollection是个什么东西? 对于Javaer, 你可以将它看作java.util.Collection, 所有实现了java.util.Collection接口的相当于实现了kotlinMutableCollection接口

java.util.HashSet就实现了java.util.Collection这个接口, 也就相当于java.util.HashSet实现了MutableCollection接口

在上面的代码中, CountingSet3实现了HashSet的父接口, 并使用HashSet替他实现了大部分的方法, 这种实现就是通过上面的组合那样实现的(你可以通过Kotlin Bytecode看到), 但是免去了我们大量定义原实现类的方法! 看起来相当简洁但又不像继承那样耦合严重.

限制

  1. 这东西是用来委托实现接口的, 不是委托实现一个类, 因此要求用来委托的对象实现了这个接口
  2. Java内置类写的是很规范的面向接口编程, 但你写的类就不一定了
  3. 事实上还是使用了组合思想, 实际字节码量并没有减少, 只是对于我们开发人员来说更简单了, 不过编程语言就应该设计的人性化才对

其他

  1. 对于自定义类型要实现的每一个接口你都可以委托实现了这个接口的对象, 而且这些接口的委托实现的对象可以是同一个, 就像下面这样:

    1
    2
    
    class ListedSet3(val innerSet: HashSet<Int> = HashSet()) :
        MutableCollection<Int> by innerSet, Cloneable by innerSet
    

    也可以继承类, 实现接口, 也可以是数据类, 泛型类等等… 普通类能做的这些都没问题

    1
    2
    
    private data class ListedSet3<T>(val innerSet: HashSet<T> = HashSet()) :
        Any(), MutableCollection<T> by innerSet, Cloneable by innerSet
    

    因此我们也能看得出来: Kotlin“类委托” 实际上是 接口委托, 网上有些文章都写的是类委托, 我觉得这是不对的, Kotlin的这个"类委托"本质上是接口委托, 实际上他也只能委托实现接口

  2. 以这种方式重写的成员不会在委托对象的成员中调用 ,委托对象的成员只能访问其自身对接口成员实现

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    interface Base {
        val message: String
        fun print()
    }
    
    class BaseImpl(val x: Int) : Base {
        override val message = "BaseImpl: x = $x"
        override fun print() { println(message) }
    }
    
    class Derived(b: Base) : Base by b {
        // 在 b 的 `print` 实现中不会访问到这个属性
        override val message = "Message of Derived"
    }
    
    fun main() {
        val b = BaseImpl(10)
        val derived = Derived(b)
        derived.print()
        println(derived.message)
    }
    


从上面我们也能看得出来, 这并不是一种继承, 而是将其中的方法进行了转发, 这也就是Kotlin委托的本质: 代理接口实现并进行方法转发


ref: Delegation | Kotlin (kotlinlang.org)
code: learn-kt/210417.kt at master · zsqw123/learn-kt (github.com)