读书笔记《Kotlin核心编程》

dive-into-kotlin.jpg

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语法实在是高。

彩蛋

看完书以后整理的笔记大纲

笔记大纲
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342