读书笔记《Kotlin核心编程》
1. 重点理解val的使用规则
引用1
如果说var代表了varible(变量),那么val可看成value(值)的缩写。但也有人觉得这样并不直观或准确,而是把val解释成varible+final,即通过val声明的变量具有Java中的final关键字的效果,也就是引用不可变。
val声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。
引用2
优先使用val来避免副作用
在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数(其实就是没有副作用的函数,具备引用透明性)来设计程序。
引用3
然而,在Kotlin编程中,我们推荐优先使用val来声明一个本身不可变的变量,这在大部分情况下更具有优势:
❑ 这是一种防御性的编码思维模式,更加安全和可靠,因为变量的值永远不会在其他地方被修改(一些框架采用反射技术的情况除外);
❑ 不可变的变量意味着更加容易推理,越是复杂的业务逻辑,它的优势就越大。
点评
上面说的其实非常明确了,val声明的变量具有Java中的final关键字的效果,也就是引用不可变,但其引用的内容是可变的。其实这里扯出了两个概念对我来说更重要,一个是变量或函数的副作用,一个是防御性编程思维。
在后续编程中,会注意到变量副作用这块,尽量避免。而防御性思维其实一直都有,对于外部输入的参数,总是站在不可靠的角度上对待,从而写出可靠的代码。
2. 关于函数和Lambda
引用1
Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在顶层直接定义一个函数,也可以在一个函数内部定义一个局部函数。
引用2
所谓的高阶函数,你可以把它理解成“以其他函数作为参数或返回值的函数”。高阶函数是一种更加高级的抽象机制,它极大地增强了语言的表达能力。
引用3
Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用。
为什么使用双冒号的语法?
如果你了解C#,会知道它也有类似的方法引用特性,只是语法上不同,是通过点号来实现的。然而,C#的这种方式存在二义性,容易让人混淆方法引用表达式与成员表达式,所以Kotlin采用::(沿袭了Java 8的习惯),能够让我们更加清晰地认识这种语法。
引用4
Lambda表达式,你可以把它理解成简化表达后的匿名函数,实质上它就是一种语法糖。
现在来总结下Lambda的语法:
❑ 一个Lambda表达式必须通过{}来包裹;
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
❑ 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明;
val sum = { x: Int, y: Int -> x + y }
❑ 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略。
val sum: (Int, Int) -> Int = { x, y -> x + y }
引用5
区分函数、Lambda
❑ fun在没有等号、只有花括号的情况下,是我们最常见的代码块函数体,如果返回非Unit值,必须带return。
fun foo(x: Int) { print(x) } fun foo(x: Int, y: Int): Int { return x * y }
❑ fun带有等号,是单表达式函数体。该情况下可以省略return。
fun foo(x: Int, y: Int) = x + y
❑ 不管是用val还是fun,如果是等号加花括号的语法,那么构建的就是一个Lambda表达式,Lambda的参数在花括号内部声明。所以,如果左侧是fun,那么就是Lambda表达式函数体,也必须通过()或invoke来调用Lambda。
val foo = { x: Int, y: Int -> x + y } // foo.invoke(1, 2)或foo(1, 2) fun foo(x: Int) = { y: Int -> x + y } // foo(1).invoke(2)或foo(1)(2)
点评
这部分内容对我个人而言,是区分函数和lambda。之前没有仔细思考扩这个问题,一直是凭着感觉来。这次算是理清楚了:如果是等号加花括号的语法,那么构建的就是一个Lambda表达式。那么调用时就必须通过()或invoke来实现。
还有一点是对函数是头等公民的理解,之前也听过这句话,但是具体体现在哪不得而知。你见过在Java中,函数里面在定义一个函数吗?你见过在Java类外部定义一个函数吗?没有吧,因为Java中对象是头等公民。而Kt中你可以在函数中再定义函数,再类顶层定义函数,这就是区别。
3. 表达式
引用
.. 闭区间 , until 半开区间
for(i in 1 until 10){ print(i) // 123456789 } println() for(i in 1..10){ print(i) // 12345678910 } println() for(i in 0..0){ print(i) // output 0 } println() for(i in 0 until 0){ print(i) // nothing }
点评
关于until半开区间的特性,自己还在工作过程中犯过一个错误,需求其实很简单,就是在一个范围内获取一个随机数,但是当你写出这句时 (0 until 0).random(),就有bug在等着你了。
上面实例代码中其实做了实验,打印 0 until 0 是没有任何内容输出的,再去向无任何输出内容的表达式要一个随机数,编译器不报错能干嘛呢?
print((0 until 0).random()) // Exception in thread "main" java.util.NoSuchElementException: Cannot get random in empty range: 0..-1
print((0..0).random()) // 0
4. init语句块
引用1
Kotlin引入了一种叫作init语句块的语法,它属于构造方法的一部分,两者在表现形式上却是分离的。Bird类的构造方法在类的外部,它只能对参数进行赋值。如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。
当没有val或var的时候,构造方法的参数可以在init语句块被直接调用。其实它们还可以用于初始化类内部的属性成员的情况。
class Bird(weight: Double = 0.00, age: Int = 0,color: String = "blue") { val weight: Double = weight //在初始化属性成员时调用weight val age: Int = age val color: String = color }
除此之外,我们并不能在其他地方使用。
class Bird(weight: Double, age: Int, color: String) { fun printWeight() { print(weight) // Unresolved reference: weight } }
事实上,我们的构造方法还可以拥有多个init,它们会在对象被创建时按照类中从上到下的顺序先后执行。多个init语句块有利于我们进一步对初始化的操作进行职能分离,这在复杂的业务开发(如Android)中显得特别有用。
引用2
我们在Bird类中可以用val或者var来声明构造方法的参数。这一方面代表了参数的引用可变性,另一方面它也使得我们在构造类的语法上得到了简化。
为什么这么说呢?事实上,构造方法的参数名前当然可以没有val和var,然而带上它们之后就等价于在Bird类内部声明了一个同名的属性,我们可以用this来进行调用。比如我们前面定义的Bird类就类似于以下的实现:
class Bird( weight: Double = 0.00, // 参数名前没有val age: Int = 0, color: String = "blue") { val weight: Double val age: Int val color: String init { this.weight = weight // 构造方法参数可以在init语句块被调用 this.age = age this.color = color } }
点评
这一部分的内容对我个人而言是知识盲区,这次算是补上了。用val或var修饰的构造方法参数,实际上等价于在类内部声明了一个同名属性,可以用this进行调用。
5. 关于延迟初始化
引用1
总结by lazy语法的特点如下
❑ 该变量必须是引用不可变的,而不能通过var来声明。
❑ 在被首次调用时,才会进行赋值操作。一旦被赋值,后续它将不能被更改。
class Bird(val weight: Double, val age: Int, val color: String) { val sex: String by lazy { if (color == "yellow") "male" else "female" } }
另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。你还可以给lazy传递LazyThreadSafetyMode. NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。
val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { //并行模式 if (color == "yellow") "male" else "female" } val sex: String by lazy(LazyThreadSafetyMode.NONE) { //不做任何线程保证也不会有任何线程开销 if (color == "yellow") "male" else "female" }
引用2
总结lateinit语法特点如下
❑ 主要用于var声明的变量,然而它不能用于基本数据类型,如Int、Long等,我们需要用Integer这种包装类作为替代。
class Bird(val weight: Double, val age: Int, val color: String) { lateinit var sex: String // sex可以延迟初始化 fun printSex() { this.sex = if (this.color == "yellow") "male" else "female" println(this.sex) } } fun main(args: Array<String>) { val bird = Bird(1000.0, 2, "bule") bird.printSex() } // 运行结果 female
引用3
你可能比较好奇,如何让用var声明的基本数据类型变量也具有延迟初始化的效果,一种可参考的解决方案是通过Delegates.notNull<T>,这是利用Kotlin中委托的语法来实现的。
var test by Delegates.notNull<Int>() fun doSomething() { test = 1 println("test value is ${test}") test = 2 }
点评
这块主要是总结by lazy和lateinit的区别,根据两者的区别选择合适的延迟方案很重要。简单粗暴一点就是by lazy对应val,lateint对应var。
6. 可见性修饰符
引用
可见性修饰符对比Kotlin中的可见性修饰符也与Java中的很类似。但也有不一样的地方,主要有以下几点:
1)Kotlin与Java的默认修饰符不同,Kotlin中是public,而Java中是default,它只允许包内访问。
2)Kotlin中有一个独特的修饰符internal。
3)Kotlin可以在一个文件内单独声明方法及常量,同样支持可见性修饰符。
4)Java中除了内部类可以用private修饰以外,其他类都不允许private修饰,而Kotlin可以。
5)Kotlin和Java中的protected的访问范围不同,Java中是包、类及子类可访问,而Kotlin只允许类及子类。
关于internal
Kotlin中有一个独特的修饰符internal,和default有点像但也有所区别。internal在Kotlin中的作用域可以被称作“模块内访问”。那么到底什么算是模块呢?以下几种情况可以算作一个模块
❑ 一个Eclipse项目❑ 一个Intellij IDEA项目
❑ 一个Maven项目
❑ 一个Grandle项目
❑ 一组由一次Ant任务执行编译的代码
总的来说,一个模块可以看作一起编译的Kotlin文件组成的集合。那么,Kotlin中为什么要诞生这么一种新的修饰符呢?Java的包内访问不好吗?
Java的包内访问中确实存在一些问题。举个例子,假如你在Java项目中定义了一个类,使用了默认修饰符,那么现在这个类是包私有,其他地方将无法访问它。然后,你把它打包成一个类库,并提供给其他项目使用,这时候如果有个开发者想使用这个类,除了copy源代码以外,还有一个方式就是在程序中创建一个与该类相同名字的包,那么这个包下面的其他类就可以直接使用我们前面的定义的类。这样我们便可以直接访问该类了。
而Kotlin默认并没有采用这种包内可见的作用域,而是使用了模块内可见,模块内可见指的是该类只对一起编译的其他Kotlin文件可见。开发工程与第三方类库不属于同一个模块,这时如果还想使用该类的话只有复制源码一种方式了。这便是Kotlin中internal修饰符的一个作用体现。
关于private
在Java程序中,我们很少见到用private修饰的类,因为Java中的类或方法没有单独属于某个文件的概念。比如,我们创建了Rectangle.java这个文件,那么它里面的类要么是public,要么是包私有,而没有只属于这个文件的概念。若要用private修饰,那么这个只能是其他类的内部类。而Kotlin中则可以用private给单独的类修饰,它的作用域就是当前这个Kotlin文件。
关于protected
Java中的protected修饰的内容作用域是包内、类及子类可访问,而在Kotlin中,由于没有包作用域的概念,所以protected修饰符在Kotlin中的作用域只有类及子类。
在了解了Kotlin中的可见修饰符后,我们来思考一个问题:前面已经讲解了为什么要诞生internal这个修饰符,那么为什么Kotlin中默认的可见性修饰符是public,而不是internal呢?
关于这一点,Kotlin的开发人员在官方论坛进行了说明,这里我做一个总结:Kotlin通过分析以往的大众开发的代码,发现使用public修饰的内容比其他修饰符的内容多得多,所以Kotlin为了保持语言的简洁性,考虑多数情况,最终决定将public当作默认修饰符。
点评
Kotlin与Java的可见性修饰符比较这一部分是我强烈推荐仔细阅读的部分,可见性修饰符虽然简单但却非常重要,kotlin的internal、private、protected修饰符都有自身独特的特点,跟你原本掌握的Java有很大的不同。
对比学习可能会让我们对两门语言的理解层次更深。
Kotlin中没有包内可见这种作用域,转而代之的是模块内可见,这种方式对比Java中的包内可见在某种意义上可能会更加“安全”。
另一边Java中某个类或方法没有单独属于某个文件的概念,而Kotlin中则可以用private单独修饰某个类,它的作用域就是当前这个kotlin文件,这种设计在我看来可能会让你更加能精准控制某个类的访问权限。
7. getter和setter
引用
1)用val声明的属性将只有getter方法,因为它不可修改;而用var修饰的属性将同时拥有getter和setter方法。
2)用private修饰的属性编译器将会省略getter和setter方法,因为在类外部已经无法访问它了,这两个方法的存在也就没有意义了。
8. 内部类vs嵌套类
引用
众所周知,在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类,必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。
内部类和嵌套类有明显的差别,具体体现在:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性;而嵌套类不包含对其外部类实例的引用,所以它无法调用其外部类的属性。
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() }
9. 数据类的约定与使用
引用
如果你要在Kotlin声明一个数据类,必须满足以下几点条件:
❑ 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何用处的;
❑ 与普通的类不同,数据类构造方法的参数强制使用var或者val进行声明;
❑ data class之前不能用abstract、open、sealed或者inner进行修饰;
❑ 在Kotlin1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类。
10. 何谓伴生
引用
顾名思义,“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。
11. 关于泛型
引用
关于协变
普通方式定义的泛型是不变的,简单来说就是不管类型A和类型B是什么关系,Generic<A>与Generic<B>(其中Generic代表泛型类)都没有任何关系。比如,在Java中String是Oject的子类型,但List<String>并不是List<Object>的子类型,在Kotlin中泛型的原理也是一样的。但是,Kotlin的List为什么允许List<String>赋值给List<Any>呢?
public interface List<E> extends Collection<E> { ... } public interface List<out E> : Collection<E> { ... }
关键在于这两个List并不是同一种类型。如果在定义的泛型类和泛型方法的泛型参数前面加上out关键词,说明这个泛型类及泛型方法是协变,简单来说类型A是类型B的子类型,那么Generic<A>也是Generic<B>的子类型,比如在Kotlin中String是Any的子类型,那么List<String>也是List<Any>的子类型,所以List<String>可以赋值给List<Any>。
List协变的特点是它将无法添加元素,只能从里面读取内容。假如支持协变的List允许插入新对象,那么它就不再是类型安全的了,也就违背了泛型的初衷。
所以我们可以得出结论:支持协变的List只可以读取,而不可以添加。其实从out这个关键词也可以看出,out就是出的意思,可以理解为List是一个只读列表。在Java中也可以声明泛型协变,用通配符及泛型上界来实现协变:<? extends Object>,其中Object可以是任意类。
关于逆变
简单来说,假若类型A是类型B的子类型,那么Generic<B>反过来是Generic<A>的子类型。
前面我们说过,用out关键字声明的泛型参数类型将不能作为方法的参数类型,但可以作为方法的返回值类型,而in刚好相反。
interface WirteableList<in T> { fun get(index: Int): T //Type parameter T is declared as 'in' but occurs in 'out' position in type T fun get(index: Int): Any //允许 fun add(t: T): Int //允许 }
我们不能将泛型参数类型当作方法返回值的类型,但是作为方法的参数类型没有任何限制,其实从in这个关键词也可以看出,in就是入的意思,可以理解为消费内容,所以我们可以将这个列表看作一个可写、可读功能受限的列表,获取的值只能为Any类型。在Java中使用<? super T>可以达到相同效果。
Kotlin与Java的型变比较
Kotlin与Java的型变比较关于通配符
MutableList<*>与MutableList<Any?>不是同一种列表,后者可以添加任意元素,而前者只是通配某一种类型,但是编译器却不知道这是一种什么类型,所以它不允许向这个列表中添加元素,因为这样会导致类型不安全。
不过细心的读者应该发现前面所说的协变也是不能添加元素,那么它们两者之间有什么关系呢?其实通配符只是一种语法糖,背后上也是用协变来实现的。所以MutableList<*>本质上就是MutableList<out Any?>,使用通配符与协变有着一样的特性。
点评
这一小节对于理解泛型的型变有很大的帮助,不过前提是你需要先理解Java中的PECS原则(Producer Extends Consumer Super),再阅读下面的协变和逆变就会轻松不少,其中的示例代码好评。
协变和逆变描述的就是在集合中,子类与父类之间的转换关系。协变即子类集合可赋值给父类集合,逆变即父类集合可赋值给子类集合,这是他们最大的特点。只是由于Java本身泛型的擦除特性,整出了一些副作用,如:协变不可添加元素,逆变读取元素不安全;协变不可作为入参,逆变不可作为返回值等副作用。
Java泛型是高阶知识,对于开发框架有很大的帮助,属于进阶必备技能。泛型的详细知识可参考 https://www.jianshu.com/p/716e941b3128 里面的2.12小节。
12. 关于惰性求值
引用1
在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个最重要的好处就是它可以构造出一个无限的数据类型。
通过上面的定义我们可以简单归纳出惰性求值的两个好处,一个是优化性能,另一个就是能够构造出无限的数据类型。
list.asSequence().filter {it > 2}.map {it * 2}.toList()
其实,Kotlin中序列的操作就分为两类,一类是中间操作,另一类则为末端操作。
引用2 中间操作
在对普通集合进行链式操作的时候,有些操作会产生中间集合,当用这类操作来对序列进行求值的时候,它们就被称为中间操作,比如上面的filter和map。每一次中间操作返回的都是一个序列,产生的新序列内部知道如何去变换原来序列中的元素。中间操作都是采用惰性求值的
引用3 末端操作
在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾,在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将“被需要”这个状态打开了。
普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第1个元素执行完所有的操作之后,第2个元素再去执行所有的操作,以此类推。
13. 内联函数简化抽象工厂
引用
何为抽象工厂模式?即为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
package factory interface Computer class Dell : Computer class Asus : Computer class Acer : Computer abstract class AbstractFactory { abstract fun produce(): Computer companion object { operator fun invoke(factory: AbstractFactory): AbstractFactory { return factory } } } class DellFactory : AbstractFactory() { override fun produce() = Dell() } class AsusFactory : AbstractFactory() { override fun produce() = Asus() } class AcerFactory : AbstractFactory() { override fun produce() = Acer() } abstract class AbstractFactory2 { abstract fun produce(): Computer companion object { inline operator fun <reified T : Computer> invoke(): AbstractFactory2 = when (T::class) { Dell::class -> DellFactory2() Asus::class -> AsusFactory2() Acer::class -> AcerFactory2() else -> throw IllegalArgumentException() } } } class DellFactory2 : AbstractFactory2() { override fun produce() = Dell() } class AsusFactory2 : AbstractFactory2() { override fun produce() = Asus() } class AcerFactory2 : AbstractFactory2() { override fun produce() = Acer() } fun main() { testAbsFactory() testAbsFactory2() } private fun testAbsFactory2() { // Kotlin中的内联函数来改善每次都要传入工厂类对象的做法 val dellFactory = AbstractFactory2<Dell>() val dell = dellFactory.produce() println(dell) } private fun testAbsFactory() { // 当你每次创建具体的工厂类时,都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不是很优雅 val dellFactory = AbstractFactory(DellFactory()) val dell = dellFactory.produce() println(dell) }
由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时(AbstractFactory),都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不是很优雅。而AbstractFactory2就是利用Kotlin中的内联函数来改善这一情况。我们所需要做的,就是用inline+reified重新实现AbstractFactory2类中的invoke方法。
这下我们的AbstractFactory2类中的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。我们来分析下这段代码:
1)通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性;
2)要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。
现在我们终于可以用类似创建一个泛型类对象的方式,来构建一个抽象工厂具体对象了。不管是工厂方法还是抽象工厂,利用Kotlin的语言特性,我们在一定程度上改进、简化了Java中设计模式的实现。
点评
这一节的知识点在实际工作中有很大的用处,inline结合reified,实现具体化类型参数。对比Java,kt在这块确实抗打,代码写出来又进一步优雅了呢。
14. 构建者模式的不足
引用
1)如果业务需求的参数很多,代码依然会显得比较冗长;
2)你可能会在使用Builder的时候忘记在最后调用build方法;
3)由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。
15. by关键字简化装饰者模式
引用
装饰者模式,在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。该模式通过创建一个包装对象,来包裹真实的对象。
总结来说,装饰者模式做的是以下几件事情:
❑ 创建一个装饰类,包含一个需要被装饰类的实例;
❑ 装饰类重写所有被装饰类的方法;
❑ 在装饰类中对需要增强的功能进行扩展。
可以发现,装饰者模式很大的优势在于符合“组合优于继承”的设计原则,规避了某些场景下继承所带来的问题。然而,它有时候也会显得比较啰唆,因为要重写所有的装饰对象方法,所以可能存在大量的样板代码。
在Kotlin中,我们可以让装饰者模式的实现变得更加优雅。猜想你已经想到了它的类委托特性,我们可以利用by关键字,将装饰类的所有方法委托给一个被装饰的类对象,然后只需覆写需要装饰的方法即可。
interface MacBook { fun getCost(): Int fun getDesc(): String fun getProdDate(): String } class MacBookPro : MacBook { override fun getCost() = 10000 override fun getDesc() = "Macbook Pro" override fun getProdDate() = "Late 2019" } class ProcessorUpgradeMacBookPro(private val macBook: MacBook) : MacBook by macBook { override fun getCost() = macBook.getCost() + 219 override fun getDesc() = macBook.getDesc() + ", +1G Memory" } fun main() { val macBookPro = MacBookPro() val processorUpgradeMacBookPro = ProcessorUpgradeMacBookPro(macBookPro) println(processorUpgradeMacBookPro.getCost()) println(processorUpgradeMacBookPro.getDesc()) }
如代码所示,我们创建一个代表MacBook Pro的类,它实现了MacBook的接口的3个方法,分别表示它的预算、机型信息,以及生产的年份。当你觉得原装MacBook的内存配置不够的时候,希望再加入一条1G的内存,这时候配置信息和预算方法都会受到影响。
所以通过Kotlin的类委托语法,我们实现了一个ProcessorUpgradeMacbookPro类,该类会把MacBook接口所有的方法都委托给构造参数对象macbook。因此,我们只需通过覆写的语法来重写需要变更的cost和getDesc方法。由于生产年份是不会改变的,所以不需重写,ProcessorUpgradeMacbookPro类会自动调用装饰对象的getProdDate方法。
总的来说,Kotlin通过类委托的方式减少了装饰者模式中的样板代码,否则在不继承Macbook类的前提下,我们得创建一个装饰类和被装饰类的公共父抽象类。
点评
装饰者模式问题所在:要重写所有的装饰对象的方法。这也就极大的限制了其使用场景,有时候还不如继承来的实在。但kt中,通过by关键字委托给一个对象,完全化解了这波尴尬,只能说kt语法实在是高。
彩蛋
看完书以后整理的笔记大纲
笔记大纲