• Kotlin读书总结之面向对象


    一. 对象与类

    1.1. 对象和单例

    Kotlin直接提供了对单例的支持,消除了实现模式的负担以及出错的风险。

    1.1.1. 带有对象表达式的匿名对象

    kotlin的对象表达式类似于js对象和c#中的匿名类型;kotlin中对象表达式是后跟一块{}的关键字object。

    fun draw() {
        val temp = object {
            val x = 10
            val y = 20
            val radius = 50
        }
        println("x: ${temp.x}; y:${temp.y}; radius:${temp.radius}")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    匿名对象使用限制:

    • 匿名对象的内部类型不能作为函数或方法的返回类型
    • 匿名对象的内部类型不能用作函数或方法的参数类型
    • 如果它们作为属性存储在类中,它们将被视作Any类型,它们的任何属性或者方法都将无法直接访问

    匿名对象可以作为接口的实现者,这里和Java类型,匿名内部类通常动态的实现接口。

    fun createRunnable(): Runnable {
        val runnable = object: Runnable {
            override fun run() {
                println("hello world")
            }
        }
        return runnable
    }
    
    // 简写,如果匿名内部类实现单个抽象方法接口,可以直接实现可不需要置顶方法名
    fun createRunnable(): Runnable = Runnable { println("hello world") }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    匿名内部类实现多个接口,就必须指定实例在返回时应该表示的类型,如下:

    fun createRunnable(): Runnable = object: Runnable, AutoCloseable {
        override fun run() {
            TODO("Not yet implemented")
        }
        override fun close() {
            TODO("Not yet implemented")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1.1.2. 带有对象声明的单例

    如果object关键字和块{}之间放置一个名称,那么kotlin将认为定义是语句或声明,而不是表达式。

    使用一个对象表达式来创建一个匿名内部类的实例,使用一个对象声明来创建一个单例(一个只有单例实例的类)。

    Unit是kotlin中一个单例,也可以通过object关键字创建一个单例

    object Util {
        // 获取核心线程数 
        fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
    }
    
    // 调用
    Util.numberOfProcessors()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    单例并不局限于拥有方法,也可以有属性(val/var)。对象声明可以实现接口,也可以从已有的类扩展,就像对象表达式;另外单例具有基类或接口,那么该单例实例可以赋给引用或者传递给基类的参数。

    object Sun: Runnable {
        const val radiusInKm = 696000
        val coreTemperatureInC = 15000000
        var a = true
        override fun run() {
            println("spin......")
        }
    }
    
    fun moveIt(runnable: Runnable) {
        runnable.run()
    }
    
    fun main() {
        println(Sun.radiusInKm) // 696000
        println(Sun.a) // true
        Sun.a = false
        println(Sun.a) // false
        moveIt(Sun) // spin......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1.1.3. 顶级函数于单例

    单例使用还可以运用于在包中函数组合和导出。例如一些函数之间关系比其他函数更为密切,,可以将其放入单例中;此外还有一些函数存在依赖关系,最好也放在一个单例中。

    1.2. 创建类

    kotlin将很多模版放到了Jvm中,减少Java中很多模板代码。

    类最简单的语法 – class关键字后面跟着类名:

    class Person
    
    • 1

    这种类没有提供任何属性、状态或行为,所以这种类不会做任何事情。

    kotlin的只读属性相当于是Java中final描述的属性,只支持读取无法修改。

    // year是只读属性
    class Person(val year: Int, var name: String)
    
    • 1
    • 2

    kotlin创建对象不需要Java使用new的

    val person = Person(10, "张三")
    
    • 1
    1.2.1. 构造函数

    kotlin的构造函数分为:主构造函数和次级构造函数。主构造函数可以有一个,次级构造函数可以有一个或者多个。

    主构造函数用于初始化类,在类标题中声明,例子:

    class Person(val name: String, val age: Int)
    // 当constructor关键字没有注解和可见性修饰符作用于它时,constructor关键字可以省略。
    class Person constructor(val name: String, val age: Int)
    
    • 1
    • 2
    • 3

    次级构造函数可以创建一个或者多个,并且使用construction关键字创建次级构造函数。例子:

    class Person constructor(val name: String) {
        var age: Int = 0
        constructor(name: String, age: Int) : this(name) {
            this.age = age
        }
        constructor(sex: Boolean) : this("Tom", if (sex) 10 else 20)
        override fun toString() = "$name => $age"
    }
    
    fun main() {
        val person1 = Person("Tom", 9)
        val person2 = Person(true)
        println(person1) // Tom => 9
        println(person2) // Tom => 10
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1.2.2. 控制对属性的修改

    kotlin在Jvm中会自动添加属性的getter和setter方法,如果需要自动set和get方法可以如下这样:

    class Person(val year: Int, name: String) {
        var name = name
            set(value) {
                if (value.isBlank()) {
                    throw RuntimeException("not empty, please")
                }
                field = value
            }
            get() {
                return "名字: $field"
            }
    }
    
    fun main() {
        val person = Person(10, "张三")
        println(person.name)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1.2.3. 访问修饰符

    kotlin中类的属性和方法默认是publish,其他访问修饰符还有:private、protected和internal;public和private与Java中的一致,protected是允许派生类的方法访问该属性,internal允许同一个模块的任何代码访问属性或者方法,值得注意的是internal修饰符没有直接的字节码表示,它有kotin编译器使用一些命名约定来处理,而不会带来任何运行时开销。

    下面看一下如何将类属性的setter/getter方法设置为private

    class Car(val name: String, age: Int) {
        var age = age
            private set
    }
    
    fun main() {
        val car = Car("BM", 10)
        car.age = 10 // Cannot assign to 'age': the setter is private in 'Car'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1.2.4. 初始化代码

    主构造函数声明是第一行的一部分。参数和属性在构造函数的参数列表中定义;不通过构造函数参数传递的属性也可以在类中定义;如果初始化对象的代码比只设置值复杂,就需要用到init块。

    kotlin中一个类可以有零个或多个init块,这些块作为主构造函数的一部分来执行。init块的执行顺序是自上而下的。例子:

    class Person() {
        private var age: Int = 10
        constructor(name: String, age: Int):this() {
            println("constructor")
            this.age = age
        }
        init { println("Person init 2,gender:${age}") }
        init { println("Person init 1") }
    }
    
    
    
    fun main() {
        val person = Person("Tom", 9)
        println(person)
    }
    
    // 输出
    Person init 2,gender:10
    Person init 1
    constructor
    com.example.one.Person@4f023edb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    Kotlin中的init代码块就相当于Java中的普通代码块,在创建对象的时候代码块会先执行。注意是每次创建都会执行一遍。

    1.3. 伴生对象和类成员

    伴生对象是在类中定义的单例,它们是类的单例伙伴,伴生对象可以实现接口,也可以从基类扩展,因此在代码重用方面也很有用。

    注意:伴生对象并不是我们所想象的Java中的静态对象那样。

    1.3.1. 类级别成员

    下面演示下如何在类上使用伴生对象添加一个属性和方法:

    class MachineOperator(val name: String) {
        
        fun checkin() = checkedIn++
        fun checkout() = checkedIn--
        
        companion object {
            var checkedIn = 0
            fun minimumBreak() = "15 minutes every 2 hours"
        }
    }
    
    fun main() {
        MachineOperator("Hello").checkin()
        println(MachineOperator.minimumBreak()) // 15 minutes every 2 hours
        println(MachineOperator.checkedIn) // 1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意:在伴生对象中放置可变属性可能会导致多线程环境中线程安全问题

    1.3.2. 访问同伴

    有时候需要一个对伴生对象的引用,而不是它的成员。例如在希望将该单例实例传递给需要接口实现的函数或方法。

    val companion = MachineOperator.Companion
    
    • 1

    或者也可以给伴生对象添加一个有意义的名称再去获取

    class MachineOperator(val name: String) {
    
        fun checkin() = checkedIn++
        fun checkout() = checkedIn--
    
        companion object MachineOperatorFactory {
            var checkedIn = 0
            fun minimumBreak() = "15 minutes every 2 hours"
        }
    }
    
    val companion = MachineOperator.MachineOperatorFactory
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1.3.3. Companion作为Factory

    类的伴生对象可以充当工厂,可以提供一个私用的构造方法,具体如下:

    class MachineOperator private constructor(val name: String) {
    
        fun checkin() = checkedIn++
        fun checkout() = checkedIn--
        
        companion object {
            var checkedIn = 0
            fun create(name: String): MachineOperator {
                val operator = MachineOperator(name)
                operator.checkin()
                return operator
            }
        }
    }
    
    fun main() {
        val operator = MachineOperator.create("哈哈哈")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这里还有一个注意点,伴生对象的init方法和对象中init执行顺序是怎么样?例子:

    class Person() {
        private var age: Int = 10
        constructor(name: String, age: Int):this() {
            println("constructor")
            this.age = age
        }
        init { println("Person init 2,gender:${age}") }
        init { println("Person init 1") }
        companion object {
            val instance by lazy { Person() }
          	// val instance = Person()
            init { println("companion init 1") }
            init { println("companion init 2") }
        }
    }
    
    fun main() {
        val person = Person.instance
        println(person)
    }
    
    // 输出
    companion init 1
    companion init 2
    Person init 2,gender:10
    Person init 1
    com.example.one.Person@eed1f14
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在使用lazy和不使用lazy初始化顺序是不一致的。这里只需要记住,拥有伴生对象是优先执行伴生对象的代码的,lazy是惰性加载的(后面会介绍)只有在使用的时候才加载。

    1.4. 泛型类

    泛型可以很好的抽象一个事物模型,在kotlin中创建一个泛型类很简单,下面看一个例子:

    class PriorityPair<T: Comparable<T>>(member1: T, member2: T) {
        val first: T
        val second: T
    
        init {
            if (member1 >= member2) {
                first = member1
                second = member2
            } else {
                first = member2
                second = member1
            }
        }
    
        override fun toString() = "$first $second"
    }
    
    fun main() {
        val priorityPair = PriorityPair(2, 1)
        val priorityPair1 = PriorityPair('A', 'B')
        println(priorityPair) // 2 1
        println(priorityPair1) // B A
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    1.5. 数据类

    kotlin的数据类是专用类,主要用于承载数据而不是行为。

    主构造函数需要使用val或者var定义至少一个属性,这里不允许Non-val或var参数,如果需要可以在body{}中向类添加其他的属性或方法。

    kotlin将自动创建equals/hashCode/toString方法,此外它还提供了一个copy方法来复制实例,同时为select属性提供更新的值。

    kotlin还创建了以单词component开头的特殊方法,用来访问通过主构造函数定义的属性。

    // 定义数据类
    data class Example(val id: Int, val name: String, val completed: Boolean)
    // 使用
    val example = Example(1, "Tom", true)
    println(example.toString())
    val example1 = example.copy(id = 2)
    println(example.toString())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    数据类重写相关方法

    data class Example(val id: Int, val name: String, val completed: Boolean) {
        override fun toString(): String {
            return "id:$id, name:$name, completed: $completed"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    属性解构

    // 给数据类赋值    
    val example = Example(1, "Tom", true)
    // 普通获取属性
    val id = example.id
    val name = example.name
    val completed = example.completed
    // 类用componentN方法根据构造方法参数顺序进行解构,参数名称可以重新定义,不需要的参数可以使用下划线进行忽略
    val (ids, _, completed) = example
    println("id: $ids, completed: $completed")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意:对应数据类而言构造函数参数顺序和解构顺序高度依赖,所以增减属性对于解构顺序都有影响

    推荐使用数据类的场景:

    • 在给数据建模,而不是行为
    • 希望生成equals/hashCode/toString/copy
    • 主构造函数至少接受一个属性是有意义的,数据类不允许使用无参数的构造函数
    • 主构造函数只接受属性是有意义的
    • 希望使用解构工具轻松的从对象中提取数据

    二. 类层次结构和继承

    2.1. 接口

    kotlin的接口和Java的接口类似,但是语义语法上有很大的不同:

    • kotlin的接口也可以有静态方法,但是只能通过在接口中编写的伴生对象实现
    • kotlin的接口中可以有方法
    • kotlin的接口可以有属性

    下面看一下具体的例子:

    interface Remote {
        fun up()
        fun down()
        // 默认方法
        fun doubleUp() {
            up()
            up()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接下来我们创建一个类去实现接口

    class TV {
        var volume = 0
    }
    
    class TVRemote(val tv: TV): Remote {
        override fun up() { tv.volume++ }
        override fun down() { tv.volume-- }
    }
    
    fun main() {
        val tvRemote = TVRemote(TV())
        tvRemote.up()
        println(tvRemote.tv.volume) // 1
        tvRemote.doubleUp()
        println(tvRemote.tv.volume) // 3
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    最后看一下接口中的静态方法:

    interface Remote {
        fun up()
        fun down()
        fun doubleUp() {
            up()
            up()
        }
        companion object {
            fun combine(first: Remote, second: Remote): Remote = object: Remote {
                override fun up() {
                    first.up()
                    second.up()
                }
                override fun down() {
                    first.down()
                    second.down()
                }
            }
        }
    }
    
    fun main() {
        val fists = TVRemote(TV())
        val second = TVRemote(TV())
        val remote = Remote.combine(fists, second)
        remote.doubleUp()
        println("${fists.tv.volume} ${second.tv.volume}") // 2 2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    最看看一下接口中的属性

    interface People {
        val email: String
        val nickName: String
            get() = email.substringBefore("@")
        fun send(): String
    }
    
    class Superman(override val email: String, override val nickName: String) : People {
        override fun send() = "send email: $nickName -> $email"
    }
    
    class SuperWoman(override val email: String): People {
        override val nickName: String
            get() = email.substringBefore("@") + "->"
    
        override fun send() = "send email: $nickName -> $email"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.2. 抽象类

    kotlin的抽象类和Java一样也是通过关键字abstract标注,抽象方法必须在抽象类中标注为abstract

    abstract class Musician(val name: String, val activeForm: Int) {
        abstract fun instrumentType(): String
    }
    
    class Cellist(name: String, activeForm: Int): Musician(name, activeForm) {
        override fun instrumentType() = "Hello world"
    }
    
    fun main() {
        val cellist = Cellist("Tom", 2022)
        println(cellist.instrumentType())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    接口和抽象类的区别:

    • 在接口中定义的属性没有幕后字段,它们必须依赖抽象方法从实现类中得到属性,并传递给基类
    • 可以实现多个接口,但最多可以从一个类(抽象和非抽象)扩展

    2.3. 嵌套类和内部类

    将上面的那个Remote接口相关的实现用内部类实现

    interface Remote {
        fun up()
        fun down()
        fun doubleUp() {
            up()
            up()
        }
    }
    
    class Tv {
        private var volume = 0
    
        val remote: Remote
            get() = TvRemote()
    
        override fun toString() = "Volume: $volume"
    
        inner class TvRemote: Remote {
            override fun up() { volume++ }
            override fun down() { volume-- }
            // this@Tv可以将内部指向TvRemote指向Tv
            // override fun toString() = "Remote: ${this@Tv.toString()}"
            override fun toString() = "Remote: ${super@Tv.toString()}"
        }
    }
    
    fun main() {
        val remote = Tv().remote
        remote.doubleUp()
        println(remote) // Remote: Volume: 2
        remote.doubleUp()
        println(remote) // Remote: Volume: 4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    内部类使用关键字inner标识就可以,这里需要主要两个语法:

    • this@Tv:如果内部类中的属性或方法隐藏了外部类中的对应成员,那么就可以使用this@Tv从内部类的方法访问外部类的成员
    • super@Tv:如果内部类的方法想访问Any,也就是绕过类访问它的基类,但是像这种方法破坏了多态性和方法重写。

    上面的内部类可以用匿名内部类实现:

    class Tv {
        private var volume = 0
    
        val remote: Remote
            get() = object: Remote {
                override fun up() { volume++ }
                override fun down() { volume-- }
                override fun toString() = "Remote: ${this@Tv}"
            }
    
        override fun toString() = "Volume: $volume"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.4. 继承

    kotlin中类默认是final,它不支持继承,只能标记为open,只有开放类的开放方法可以在派生类中重写,并且需要标记为override,重写方法可以标记为final override,以防字类进行进一步重写该方法。

    注意:基类的val属性可以用派生类的val/var重写,但是基类的var属性只能使用派生类中的var重写。这个限制是因为val只有一个getter,可以通过重写var的派生类中添加一个setter。

    先看一个例子:

    // year是不能被任何派生类中重写
    // color的open是允许派生类重写
    open class Vehicle(val year: Int, open var color: String) {
        // km被open标识是可以被重写
        open val km = 0
        // 	toString和repaint也不允许被重写,因为没有被open标识
        final override fun toString() = "year: $year, color: $color, km: $km"
        fun repaint(newColor: String) { color = newColor }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接着实现一个派生类

    class Car(year: Int, color: String): Vehicle(year, color) {
        override var km: Int = 0
            set(value) {
                if (value < 1) {
                    throw RuntimeException("can not set negative value")
                }
                field = value
            }
        override var color: String
            get() = super.color
            set(value) {
                if (value.isEmpty()) {
                    throw RuntimeException("color required")
                }
                super.color = value
            }
        fun drive(distance: Int) { km += distance }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这里的kotlin不区分implements和extends,只有继承。

    2.5. 密封类

    sealed类是一种同时拥有枚举类 enum 和 普通类 class 特性的类,叫做密封类。

    密封类在Java层面是一个抽象类,派生类继承这个抽象个类,密封类的构造函数是私有的,不能被外部外放;这样就通过继承这个抽象类,达到限制类型的做法。这和Java中使用接口来限定参数类型的做法类似。下面看例子:

    sealed class Result
    class Success(val code: Int): Result()
    class Exception(val code: Int, val message: String): Result()
    fun handleResult(result: Result) = when(result) {
        is Success -> "success"
        is Exception -> "exception"
    }
    
    fun main() {
        val result = handleResult(Success(200))
        println(result)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    密封类的派生类可以有任意数量的实例。一个特殊的情况是枚举,它将每个子类的实例数量限制为一个。

    2.6. 枚举

    kotlin的枚举和Java的枚举类似,先看一个例子:

    enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
    sealed class Card(val suit: Suit)
    class Ace(suit: Suit): Card(suit)
    class Queen(suit: Suit): Card(suit)
    class Pip(suit: Suit, val number: Int): Card(suit)
    class King(suit: Suit): Card(suit) {
        override fun toString() = "king if $suit"
    }
    fun process(card: Card) = when (card) {
        is Ace -> "${card.javaClass.name} of ${card.suit}"
        is King, is Queen -> "$card"
        is Pip -> "${card.number} of ${card.suit}"
    }
    fun main() {
        println(process(Ace(Suit.CLUBS)))  // com.example.one.Ace of CLUBS
        println(process(Queen(Suit.SPADES))) // com.example.one.Queen@85ede7b
        println(process(Pip(Suit.DIAMONDS, 10)) // 10 of DIAMONDS
        )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    接着看一下枚举的一些常用操作:

    enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
    // 获取某一个
    var suit = Suit.valueOf("CLUBS")
    // 遍历, name:实例的名称 ordinal:索引
    for (suit in Suit.values()) { println("${suit.name} -> ${suit.ordinal}") }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    枚举类也可以有属性和方法:

    enum class Suit(val symbols: Char) {
        CLUBS('\u2663'),
        DIAMONDS('\u2666'),
        HEARTS('\u2665') {
            override fun display() = "${super.display()} $symbols"
        },
        SPADES('\u2660');
        
        open fun display() = "$symbols $name"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    三. 通过委托进行扩展

    kotlin同时支持继承和委托,如何选择:

    • 如果你想用一个类的对象来替代另一个类的对象,请使用继承
    • 如果你想让一个类的对象只使用另一个类的对象,请使用委托

    先看一个委托的实例:

    interface Worker {
        fun work()
        fun takeVacation()
    }
    class JavaProgrammer: Worker {
        override fun work() = println("...write Java...")
        override fun takeVacation() = println("...code at the beach...")
    }
    class PhpProgrammer: Worker {
        override fun work() = println("...write PHP...")
        override fun takeVacation() = println("...code at the beach...")
    }
    class Manager(val worker: Worker) {
        fun work() = worker.work()
        fun takeVacation() = worker.takeVacation()
    }
    fun main() {
        val manager = Manager(JavaProgrammer())
        manager.work()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    看着比较啰嗦,但是kotlin提供了支持。

    3.1. 使用by来进行委托

    上面例子是手工实现委托,将方法调用从Manager路由到Worker委托。在kotlin可以让编译器为我们生成路由代码,简化我们的使用。

    class Manager(): Worker by JavaProgrammer()
    fun main() {
        val manager = Manager()
        manager.work() // ...write Java...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    例子中可以看到Manager没有实现任何方法,只是通过By关键字,编译器在字节码层面实现了属于Worker的方法。

    这是最简单的委托使用

    3.2. 委托给一个参数

    在3.1的例子中属于最简单的委托例子,但是看一下就会发现,委托不具备扩展性,只能使用JavaProgrammer,另外Manager的实例不具备访问委托的权限。那么怎么改进呢?可以Wroker委托给参数,如下:

    class Manager(val staff: Worker): Worker by staff {
        fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
    }
    fun main() {
        val manager = Manager(JavaProgrammer())
        manager.meeting() // organizing meeting with JavaProgrammer
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时还有一个疑问,如果Manager的方法和Worker中的冲突,怎么解决?重写当前冲突的方法

    class Manager(val staff: Worker): Worker by staff {
        fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
        override fun takeVacation() = println("of course")
    }
    
    fun main() {
        val manager = Manager(JavaProgrammer())
        manager.takeVacation() // of course
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接着还有一个问题就是委托多个接口怎么处理?例子:

    interface Assistant {
        fun doChores()
        fun fileTimeSheet() = println("No escape from that")
    }
    class DepartmentAssistant: Assistant {
        override fun doChores() = println("routing stuff")
    }
    // 多个接口委托
    class Manager(val worker: Worker, val assistant: Assistant): 
    		Worker by worker, Assistant by assistant {
        fun meeting() = println("organizing meeting with ${worker.javaClass.simpleName}")
        override fun takeVacation() = println("of course")
        override fun fileTimeSheet() {
            println("manually forwarding this...")
            assistant.fileTimeSheet()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.3. 委托的一些注意点

    kotlin的委托是编译器帮我们处理一部分字节码,但是并不能将委托等价于继承。还是借用开始那个例子:

    interface Worker {
        fun work()
        fun takeVacation()
    }
    class JavaProgrammer: Worker {
        override fun work() = println("...write Java...")
        override fun takeVacation() = println("...code at the beach...")
    }
    class PhpProgrammer: Worker {
        override fun work() = println("...write PHP...")
        override fun takeVacation() = println("...code at the beach...")
    }
    class Manager(val worker: Worker): Worker by worker
    
    fun main() {
    //  val manager: JavaProgrammer = Manager(PhpProgrammer()) // ERROR 类型不匹配
        val manager: Worker = Manager(JavaProgrammer())  // 这样是可以的
        manager.work() // ...write Java...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里也可以发现委托的一个副作用,Manager可以被当作Worker来对待。

    还有一个需要的注意的,这里Manager的参数是val,但是如果将val变为var时,会出现什么现象。例子:

    class Manager(var worker: Worker): Worker by worker
    
    fun main() {
        val manager = Manager(JavaProgrammer())
        manager.work() // ...write Java...
        manager.worker = PhpProgrammer()
        manager.work() // ...write Java...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    出现例子中的现象其实也不奇怪,在想下上面Java的委托实现,可以推测出,kotlin在by关键下,也会保存一个幕后字段作为JavaProgrammer的实现引用,接着将this.worker = worker赋值引用,manager.worker = PhpProgrammer()只是改变了worker参数引用的指向,但是幕后字段的引用并没有改变,修改不生效。

    并且在manager.worker = PhpProgrammer()中,涉及将JavaProgrammer改为PhpProgrammer,但是JavaProgrammer并没有垃圾回收,因为委托持有它。因此委托的声明周期和对象的声明周期相同。

    3.4. 委托变量和属性

    Kotlin的委托还可以使用在get/set对象属性的访问权限以及局部变量的访问权限。

    get/set会委托给被委托对象setValue/getValue方法,因此被委托类需要提供setValue/getValue这两个方法。如果是val 属性,只需提供getValue。如果是var 属性,则setValue/getValue都需要提供。

    3.4.1. 委托变量

    局部变量也可以声明委托,例子:

    class PoliteString(var content: String) {
        operator fun getValue(thisRef: Any?, property: KProperty<*>) = content.replace("stupid", "s******")
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { content = value }
    }
    
    fun main() {
        var comment: String by PoliteString("Some nice message")
        println(comment)  // Some nice message
        comment = "This is stupid"
        println(comment)  // This is s******
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    3.4.2. 委托属性

    属性委托和变量委托其实差不多,例子:

    import kotlin.reflect.KProperty
    
    class Delegate {
        private var content = "==> "
        operator fun getValue(thisRef: Any?, property: KProperty<*>) = content
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { content = "<== $value" }
    }
    
    class NewString {
        var prop: String by Delegate()
    }
    
    fun main() {
        val newString = NewString()
        println(newString.prop)    // ==> 
        newString.prop = "hello world"
        println(newString.prop)    // <== hello world
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    thisRef —— 必须与属性所有者类型相同或者是它的超类型

    property —— 必须是类型 KProperty<*> 或其超类型

    value —— 必须和属性同类型或者是它的超类型

    从上面的例子可以看到属性委托需要实现getter/setter方法,kotlin标准库中声明了2个含所需 operator方法的 ReadOnlyProperty / ReadWriteProperty 接口。

    interface ReadOnlyProperty<in R, out T> {
        operator fun getValue(thisRef: R, property: KProperty<*>): T
    }
    
    interface ReadWriteProperty<in R, T> {
        operator fun getValue(thisRef: R, property: KProperty<*>): T
        operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么看一下上面的例子该如何重写:

    class Delegate: ReadWriteProperty<Any, String> {
        private var content = "==> "
        override operator fun getValue(thisRef: Any, property: KProperty<*>) = content
        override operator fun setValue(thisRef: Any, property: KProperty<*>, value: String) { content = "<== $value" }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这一下就简单很多了。

    3.5. Lazy(惰性)委托

    惰性委托可以在使用的才会执行,这个特性和短路求值一样。例子:

    fun getTemperature(city: String): Double {
        println("fetch from webservice for $city")
        return 30.0
    }
    
    fun main() {
        val showTemperature = false
        val city = "Boulder"
        if (showTemperature && getTemperature(city) > 20) { // 短路求值
            println("Warm")
        } else {
            println("Nothing to report")  // 只输出这一句
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    接着重构下上面的代码:

    fun main() {
        val showTemperature = false
        val city = "Boulder"
        val temperature = getTemperature(city)
        if (showTemperature && temperature > 20) {
            println("Warm")
        } else {
            println("Nothing to report")
        }
    }
    // fetch from webservice for Boulder
    // Nothing to report
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这样的输出并不是我们希望的,那么如何优化呢?可以惰性委托:

    fun main() {
        val showTemperature = false
        val city = "Boulder"
        val temperature by lazy { getTemperature(city) }
        if (showTemperature && temperature > 20) {
            println("Warm")
        } else {
            println("Nothing to report")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里的通过by关键字,将变量temperature变成了一个委托属性。lazy函数接受一个lambda表达式作为参数,该表达式将执行计算,但仅按需执行,而不是急于或立即执行。lambda表达式中的计算将在请求变量的值时进行计算。

    一旦对lambda表达式求值,委托将记住结果,以后对该值的请求将接受报错,不会重新计算lambda表达式。

    默认情况下,lazy函数同步lambda表达式的执行,因此最多只有一个线程执行它。

    3.6. Observable委托

    observable委托来监视对象中局部变量或属性的变化。它可以用于监视和调用。例子:

    import kotlin.properties.Delegates.observable
    
    fun main() {
        var count by observable(0) {property, oldValue, newValue ->
            println("Property: $property, old: $oldValue, new: $newValue")
        }
        println("count: $count") // count: 0
        count++ // Property: property count (Kotlin reflection is not available), old: 0, new: 1
        println("count: $count") // count: 1
        count-- // Property: property count (Kotlin reflection is not available), old: 1, new: 0
        println("count: $count") // count: 0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里提示Kotlin reflection is not available,是因为缺少依赖

    implementation(“org.jetbrains.kotlin:kotlin-reflect:1.6.10”)

    这里还需要了解Vetoable函数,这个和observable不同,使用vetoable注册的处理程序返回布尔结果。返回值true表示接受修改,false表示拒绝,如果拒绝修改将被放弃。例子:

    var count by vetoable(10) {_, oldValue, newValue -> newValue > oldValue}
    println(count) // 10
    count++
    println(count) // 11
    count--
    println(count) // 11
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    本文来源:《Kotlin编程实战:第二部分总结》

  • 相关阅读:
    docker 安装rabbitmq并配置hyperf使用
    高纬度矩阵乘法的意义
    RHCE之路配置本地DNS服务器的正反向解析
    js的indexOf方法
    自动化项目实战 [个人博客系统]
    基于SSM的进存销管理系统
    计算机网络的故事——确认访问用户身份的认证
    MySQL索引失效原理是什么?
    CentOS7下安装ClickHouse详解
    69. x 的平方根
  • 原文地址:https://blog.csdn.net/yhflyl/article/details/127708062