Kotlin基础-有趣的面向对象(下)

258 阅读11分钟

11、委托

类的委托

在Kotlin中,委托其实就是代理模式,只不过Kotlin天生支持委托,它可以通过关键字by实现。

委托模式其实是实现继承的一种很好的方案

被委托对象和委托对象一般有一个公共接口,它们都要实现这个共同的接口,下面示例来自Kotlin官方文档

 interface Base {
     fun print()
 }
 ​
 class BaseImpl(private val x: Int) : Base {
     override fun print() { println(x) }
 }
 ​
 class Derived(b: Base) : Base by b
 ​
 fun main() {
     val b = BaseImpl(10)
     Derived(b).print() // 10
 }

如上示例,委托对象为Derived,被委托对象是BaseImpl,由于Derived和BaseImpl实现了Base接口,所以在初始化Derived对象时主构造函数传入一个Base类型的对象(多态)就可以委托此对象去实现print()函数,因此我们调用Derived对象的print()函数就会去请求代理对象(被委托对象)BaseImpl去做实现

需要注意的是,如果对于公共接口中的方法,委托对象类中有具体实现的话,它不会去请求代理对象完成,而是调用本类中的具体实现,对上述代码改造如下

 interface Base {
     fun print()
     fun printMessage()
 }
 ​
 class BaseImpl(private val x: Int) : Base {
     override fun print() { println(x) }
     override fun printMessage() { println(x) }
 }
 ​
 class Derived(b: Base) : Base by b {
     override fun printMessage() {
         println("Derived printMessage")
     }
 }
 ​
 fun main() {
     val b = BaseImpl(10)
     Derived(b).print() // 10
     Derived(b).printMessage() // Derived printMessage
 }

下图可以更好的理解委托的原理

21.png

属性委托

基本的属性委托形式它长下面这样

 class Example {
     var p: String by Delegate()
 }

也是通过关键字by来实现,然后后面这个Delegate它是一个类,这里不去看什么语法形式,我们看官方文档给出的这个类的实现的例子

 import kotlin.reflect.KProperty
 ​
 class Delegate {
     operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
         return "$thisRef, thank you for delegating '${property.name}' to me!"
     }
 ​
     operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
         println("$value has been assigned to '${property.name}' in $thisRef.")
     }
 }

这个类是我们自己定义的,可见我们用operator关键字来声明了两个方法,分别是getValue and setValue。接下来我们来简单解析一下这两个方法。

首先我们在main函数中去初始化一个Example实例,然后分别去读取p的值以及更改p的值看看会发生什么

 fun main() {
     val example = Example()
     println(example.p)
     example.p = "NEW"
 }

输出如下

 Example@33a17727, thank you for delegating 'p' to me!
 NEW has been assigned to 'p' in Example@33a17727.

我们观察以上输出,其实thisRef就是我们上面实例化的那个Example对象,只不过这边我们用了多态去接收,我们当然可以把getValue和setValue方法中第一个参数类型改为Example。

那第二个参数其实就是Kotlin中的一个内置类型,我们发现它在反射包下,它肯定是用反射读到了Example类以及p属性(反射的内容我们后面会提及),二这第二个参数property身上有一个name属性就是我们读取或者操作的那个属性的名称,比如说我们这边读取和修改的是p属性,如果我们操作的是别的属性,那么property参数的name值就是我们操作的那个属性名

最后一个是setValue方法中的第三个参数value,我们发现这个参数刚好就是我们修改的值。

那这么说应该是很通透了吧,那既然这样我们就可以自己去定义一个委托属性类去玩一下,接下来我们自己写一个案例来理解

 import kotlin.reflect.KProperty
 ​
 class NameDelegate(private val name: String) {
     operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
         println("Read the $thisRef's ${property.name} property")
         return this.name
     }
 }
 ​
 class AgeDelegate(private var age: Int) {
     operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
         println("Read the $thisRef's ${property.name} property")
         return this.age
     }
     operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
         println("Modified the $thisRef's ${property.name} to $value")
         this.age = value
     }
 }
 ​
 class Person(name: String, age: Int) {
     val name: String by NameDelegate(name)
     var age: Int by AgeDelegate(age)
 }
 ​
 fun main() {
     val person = Person("Jack", 18)
     val name = person.name
     println(name)
 ​
     val age = person.age
     println(age)
     person.age = 20
 }

输出如下

 Read the demo.Person@1b2c6ec2's name property
 Jack
 Read the demo.Person@1b2c6ec2's age property
 18
 Modified the demo.Person@1b2c6ec2's age to 20

解释一下上面代码的含义,我们定义了两个委托类NameDelegateAgeDelegate,分别用来代理Person类中的name(不可变)和age(可变)属性,在NameDelegateAgeDelegate中分别封装了name和age两个私有属性,用这两个私有属性来代理Person类中的name和age属性,而这两个委托类中的getValuesetValue方法就用来执行相应的getter和setter操作,其实整个代码还是蛮好理解的。

然后介绍一下,在Kotlin标准库中有两个接口,分别是ReadOnlyPropertyReadWriteProperty,为了让我们的委托类实现的更为规范,我们可以去实现这两个接口(而且实现接口后在IDEA中还可以一键自动生成哦)。

 public fun interface ReadOnlyProperty<in T, out V> {
     /**
      * Returns the value of the property for the given object.
      * @param thisRef the object for which the value is requested.
      * @param property the metadata for the property.
      * @return the property value.
      */
     public operator fun getValue(thisRef: T, property: KProperty<*>): V
 }
 public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
     /**
      * Returns the value of the property for the given object.
      * @param thisRef the object for which the value is requested.
      * @param property the metadata for the property.
      * @return the property value.
      */
     public override operator fun getValue(thisRef: T, property: KProperty<*>): V
 ​
     /**
      * Sets the value of the property for the given object.
      * @param thisRef the object for which the value is requested.
      * @param property the metadata for the property.
      * @param value the value to set.
      */
     public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
 }

其实就是定义了两套接口规范而已

接下来我们就改造一下上面的例子

 import kotlin.properties.ReadOnlyProperty
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
 ​
 class NameDelegate(private val name: String) : ReadOnlyProperty<Any, String> {
     override fun getValue(thisRef: Any, property: KProperty<*>): String {
         println("Read the $thisRef's ${property.name} property")
         return this.name
     }
 }
 ​
 class AgeDelegate(private var age: Int) : ReadWriteProperty<Any, Int> {
     override fun getValue(thisRef: Any, property: KProperty<*>): Int {
         println("Read the $thisRef's ${property.name} property")
         return this.age
     }
     override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
         println("Modified the $thisRef's ${property.name} to $value")
         this.age = value
     }
 }
 ​
 class Person(name: String, age: Int) {
     val name: String by NameDelegate(name)
     var age: Int by AgeDelegate(age)
 }
 ​
 fun main() {
     val person = Person("Jack", 18)
     val name = person.name
     println(name)
 ​
     val age = person.age
     println(age)
     person.age = 20
 }

输出还是一样的,只不过我们实现了这两个接口,更规范了,可读性也会更好

但是到这里您可能会有一个疑问,就是不管是类的委托还是属性的委托,我直接用类去实现或者用getter和setter方法去实现功能不就好了吗,为什么要委托给其他的代理类呢,这难道不是多此一举吗?

其实这也涉及到代理模式解决的其中一个关键问题,就是解耦合,其实很多时候我们要求我们的业务代码的耦合不要太高,如果完全没有了解过这方面的东西也没有关系,我们现阶段只需要简单学习即可,比较简单的例子就是我们的一个类要实现一个功能,我们直接去在类中写,如果某一天需要更改类的功能,我们需要去重写大部分业务代码,碰到复杂的业务这会很头疼,不亚于重构。而代理模式往往能够帮我们解决这类问题,我们只需要委托给另一个委托类去做,将来有了别的功能,我们只需要将委托的类去改变一下就好,即实现了一个*“可插拔”的效果。而且代理类往往会有增强功能*的作用,这个我们后面详解。

lazy(懒惰属性)

懒惰属性,简单理解就是可以实现属性的懒加载,注意,这里不要与lateinit关键字搞混,如果忘记可以跳回去看一下lateinit

通过lazy关键字修饰的属性可以实现懒加载,属性懒加载是一种优化的手段,它是在需要时才去初始化对象的属性,而不是实例化的时候同时初始化属性,并且它只会初始化一次,这样做可以减少内存占用和提高应用性能

lazy关键字可以应用于类、接口和枚举类的属性。我们看如下的简单示例

 class People(private val firstname: String, private val lastname: String) {
     private val fullName: String by lazy {
         println("fullName init")
         "$firstname $lastname"
     }
 ​
     override fun toString(): String {
         return "Person(firstname=$firstname, lastname=$lastname, fullName=$fullName)"
     }
 }
 ​
 fun main() {
     val people = People("Linus", "Torvalds")
 }

以上代码并没有任何输出,并没有执行到lazy代码块中的打印语句println("fullName init"),因为这时候不需要fullName,所以编译器并没有帮我们立即初始化,而当我们去输出toString()方法时

 val people = People("Linus", "Torvalds")
 println(people.toString())
 println(people.toString())

输出如下

 fullName init
 Person(firstname=Linus, lastname=Torvalds, fullName=Linus Torvalds)
 Person(firstname=Linus, lastname=Torvalds, fullName=Linus Torvalds)

它就会帮我们进行初始化操作,执行了println("fullName init"),并且只执行了一次(只初始化一次

Observable property(可观察的属性)

kotlin.properties.Delegates下有一个observable函数,它有两个参数,分别是初始值和修改事的处理函数(接受一个labmda表达式),第二个参数处理函数您可以根据你的需求自定义,它接受三个参数,分别是被赋值的属性、旧值和新值。我们来看官方文档的例子

 import kotlin.properties.Delegates
 ​
 class User {
     var name: String by Delegates.observable("<no name>") {
         old, new ->
         println("$prop $old -> $new")
     }
 }
 ​
 fun main() {
     val user = User()
     user.name = "first"
     user.name = "second"
 }

输出如下

 <no name> -> first
 first -> second

可见当每次属性值发生改变的时候都会去调用old, new -> println("$prop $old -> $new")

说到这再稍微拓展一下另一个函数vetoable,它跟observable一样也可以观察属性变化,只不过它还可以拦截属性修改,它的第二个参数也是一个函数,只不过返回值是一个boolean类型的值,即满足属性修改的条件,比如下面的例子

 import kotlin.properties.Delegates
 ​
 fun main() {
     var i: Int by Delegates.vetoable(0) {
         _, old, new -> new > old
     }
     i = 5
     println(i)
     i = 10
     println(i)
     i = 8
     println(i)
 }

输出如下

 5
 10
 10

我们的处理函数中定义的是如果修改的值大于原来的值返回true,即满足修改条件,否则拦截。我们可以看到第三次修改时修改的值小于原来的值就修改失败,所以第三次输出的还是修改前的值

在map映射中存储属性

属性的委托还可以接受一个map,只需找到map中对应的key即可。当然,如果您的map中key写错的话编译器会抛一个异常(map的内容我们后面还会提及)

可以看以下示例来理解

 class Table(map: Map<String, Any?>) {
     val name: String by map
     val length: Int by map
 }
 ​
 fun main() {
     val table = Table(
         mapOf(
             "name" to "KUG Data Table",
             "length" to 10
         )
     )
     println("name=${table.name} length=${table.length}")
 }

简单总结:

委托其实就是设计模式中的代理模式,只不过Kotlin天生支持委托,用关键字by实现

委托类、委托属性都可以实现解耦合,并且被委托对象还可以有增强功能的作用

lazyobservable和map中存储属性都是基于委托的原理实现的属性委托

12、枚举类

对于枚举最简单的理解就是把它当作一组常量,使用enum class定义

对于以下示例,皆来自官方文档

 enum class Color {
     RED, GREEN, BLUE
 }
 ​
 fun main() {
     val red = Color.RED
     println(red.name) // 枚举实例的名字 RED
     println(red.ordinal) // 枚举实例的索引  0
 }

以上Season枚举类就定义了四个枚举常量,每一个枚举常量都是一个对象,用逗号分隔

既然枚举类也是一个类,那么它也可以有自己的属性方法以及构造函数

由于每一个枚举常量都是一个枚举类的实例,因此可以在枚举类中定义属性,在每个枚举常量中进行初始化

 enum class Color(val rgb: Int) {
     RED(0xFF0000),
     GREEN(0x00FF00),
     BLUE(0x0000FF)
 }
 ​
 fun main() {
     val red = Color.RED
     println("${red.name} ${red.rgb}") // RED 16711680
 }

比如说上面三个REDGREENBLUE实例,我们在Color类的主构造函数中定义了rgb属性,并将它这三个实例的rgb属性初始化

另外,Kotlin中的枚举类支持将匿名类附加到每个常量上(Anonymous classes)

如下

 enum class Color(val rgb: Int) {
     RED(0xFF0000) {
         override fun getColor() = Color.RED
     },
     GREEN(0x00FF00) {
         override fun getColor() = Color.GREEN
     },
     BLUE(0x0000FF) {
         override fun getColor() = Color.BLUE
     };
 ​
     abstract fun getColor(): Color
 }
 ​
 fun main() {
     println(Color.RED.getColor())
 }

上面的例子中我们的枚举类ProtocolState有两个常量,这两个常量重附加了匿名类,并且都重写了基类中的signal方法

实现接口

枚举类既然也是类嘛,它肯定也可以实现接口,如下

 interface Printable {
     fun print()
 }
 ​
 enum class Color(val rgb: Int) : Printable {
     RED(0xFF0000) {
         override fun getColor() = Color.RED
         override fun print() {
             println("color->red")
         }
     },
     GREEN(0x00FF00) {
         override fun getColor() = Color.GREEN
         override fun print() {
             println("color->green")
         }
     },
     BLUE(0x0000FF) {
         override fun getColor() = Color.BLUE
         override fun print() {
             println("color->blue")
         }
     };
 ​
     abstract fun getColor(): Color
 }
 ​
 fun main() {
     Color.RED.print() // color->red
 }

valueOf和values方法

 fun main() {
     for (i in Color.values()) {
         println(i.toString()) // 遍历
     }
     println(Color.valueOf("RED")) // RED
 }

搭配when表达式使用

 fun getWarmth(color: Color) = when(color) {
     Color.RED -> "warm"
     Color.GREEN -> "neutral"
     Color.BLUE -> "cold"
 }
 ​
 fun main() {
     println(getWarmth(Color.BLUE)) // cold
 }

简单总结:

枚举类就是一个用enum class关键字定义的类,每个常量都是它的实例

枚举类支持将匿名类附加到常量身上

枚举类还可以实现接口来增强功能

枚举类可以通过values()方法遍历以及valueOf()方法获取常量

枚举类在一些场景下可以搭配when表达式使用,安全灵活

13、内联类

这个地方有些变故,很多Kotlin代码中您可能经常看见用inline关键字修饰的,但是现在官方已经不推荐使用inline了,换句话说就是已经过时了。下面会使用Kotlin中新引入的value关键字

首先来解释一下为什么要有内联类,其实很多时候我们的业务逻辑需要去围绕一些类型再去做一层封装,但如果封装的是基本数据类型,编译器在底层会做一些优化,而我们封装的那层对象,还是会去堆中分配内存,增加我们运行时的一些负担。很多场景下,这是一种比较严重的性能损失

因此Kotlin中引入了内联类,在编译时会将内联类转换为简单的数据类型,并不会去分配额外的空间

内联类使用value关键字和@JvmInline注解定义

 @JvmInline
 value class Password(val password: String)

比如上述代码,我们定义了一个内联类Password用来封装一个String类型的密码

内联类支持常规类的一些功能,比如可以声明函数和属性、可以有init块。但是内联类不能有lateinit和委托属性,并且属性只能使用val修饰

比如下面的示例

 @JvmInline
 value class Password(private val password: String) {
     val length: Int
         get() = password.length
 }
 ​
 fun main() {
     val password = Password("1234")
     println(password.length) // 4
 }

实现接口

 interface Security {
     fun checkSecurity(): String
 }
 ​
 @JvmInline
 value class Password(private val password: String) : Security {
     val length: Int
         get() = password.length
     override fun checkSecurity() = if (length > 8) "safe" else "unsafe"
 }
 ​
 fun main() {
     val password = Password("1234")
     println(password.length) // 4
     println(password.checkSecurity()) // unsafe
 }

简单总结:

内联类可以解决一些有封装需求但是要降低空间分配开销的场景问题

现在Kotlin官方推荐使用value而不是inline

可以支持一些常规类的功能,比如声明函数、属性和init块,不支持lateinit和委托属性

内联类还可以实现接口来增强功能

14、扩展

关于扩展(Extensions)的这一部分简单说一下

它通常会应用于以下场景:在使用第三方库的时候,需要对第三方库进行修改或者添加新功能

函数扩展

 class RestTemplate
 ​
 fun RestTemplate.get(url: String) {
     println("Successful sent a GET request to $url")
 }
 ​
 fun main() {
     val restTemplate = RestTemplate()
     restTemplate.get("https://kotlinlang.org/")
 }

比如说我们拿到了一个第三方库叫做RestTemplate,我们需要一个get()方法用来发送GET请求,但是这个库里面却没有,我们又不能去源码中修改,因此我们就可以使用函数扩展

属性扩展

 val RestTemplate.timeout: Long
     get() = 5000
 ​
 fun main() {
     val restTemplate = RestTemplate()
     restTemplate.get("https://kotlinlang.org/")
     println("timeout=${restTemplate.timeout}")
 }

如上,比如我们要向RestTemplate类中添加一个timeout属性用来设置默认超时时间,我们就可以用属性扩展

伴生对象扩展

正常来说默认超时时间这种不应该是实例身上的属性,它应该是属于静态属性,应该是全局所共享的。我们采用伴生对象扩展来改造上述代码

 class RestTemplate {
     companion object {} // 必须声明 companion object, 否则外部不能扩展
 }
 fun RestTemplate.get(url: String) {
     println("Successful sent a GET request to $url")
 }
 ​
 val RestTemplate.Companion.timeout: Long
     get() = 5000
 ​
 fun main() {
     val restTemplate = RestTemplate()
     restTemplate.get("https://kotlinlang.org/")
     println("timeout=${RestTemplate.timeout}")
 }

关于扩展的更多内容,比如可为空接收、扩展范围、扩展类成员、可见性等等感兴趣可以去文档阅读

简单总结:

扩展可以应用于定制第三方库的需求,有很多扩展方式,比如函数扩展、属性扩展、伴生对象扩展等等

15、类型别名

Kotlin中提供了*类型别名(Type aliases)*的机制可以用自定义简短的名称来缩短冗长的类型名称,编译器并不会新引入类型,只是在编译阶段转换为相应的类型,这是Kotlin中的一个语法糖。它可以应用在泛型、内部类和嵌套类以及函数中

以下示例均来自Kotlin官方文档

 typealias NodeSet = Set<Network.Node>
 ​
 typealias FileTable<K> = MutableMap<K, MutableList<File>>
 typealias MyHandler = (Int, String, Any) -> Unit
 ​
 typealias Predicate<T> = (T) -> Boolean
 class A {
     inner class Inner
 }
 class B {
     inner class Inner
 }
 ​
 typealias AInner = A.Inner
 typealias BInner = B.Inner

文章若有错误或不足之处,欢迎大家评论区指正,谢谢大家!

另外,欢迎大家来了解一下济南KUG(Jinan Kotlin User Group),如果您对Kotlin技术分享抱有热情,不妨加入济南KUG,济南KUG官网:济南KUG