Kotlin 核心编程 (46):面向对象 3.1.2

阅读数:1 2019 年 12 月 25 日 15:35

Kotlin核心编程(46):面向对象 3.1.2

(更简洁地构造类的对象)

内容简介
本书不是一本简单介绍 Kotlin 语法应用的图书,而是一部专注于帮助读者深入理解 Kotlin 的设计理念,指导读者实现 Kotlin 高层次开发的实战型著作。书中深入介绍了 Kotlin 的核心语言特性、设计模式、函数式编程、异步开发等内容,并以 Android 和 Web 两个平台为背景,演示了 Kotlin 的实战应用。
全书共 13 章,分为 4 个部分:
热身篇—Kotlin 基础(第 1~2 章),简单介绍了 Kotlin 设计哲学、生态及基础语法,其中包括 Kotlin 与 Scala、Java 之间的关联与对比,以及 Kotlin 的类型声明的特殊性、val 和 var 的使用、高阶函数的使用、面向表达式编程的使用、字符串的定义与操作等内容;
下水篇—Kotlin 核心(第 3~8 章),深入介绍了面向对象、代数数据类型、模式匹配、类型系统、Lambda、集合、多态、扩展、元编程等 Kotlin 开发核心知识,这是本书的重点,其中涉及很多开发者特别关心的问题,比如多继承问题、模式匹配问题、用代数数据类型抽象业务问题、泛型问题、反射问题等。
潜入篇—Kotlin 探索(第 9~11 章),探索 Kotlin 在设计模式、函数式编程、异步和并发等编程领域的应用,其中包括对 4 大类设计模式、Typeclass 实现、函数式通用结构设计、类型替代异常处理、共享资源控制、CQRS 架构等重点内容的深入剖析;
遨游篇—Kotlin 实战(第 12~13 章),着重演示了 Kotlin 在 Android 和 Web 平台的实战案例,其中涉及架构方式、单向数据流模型、解耦视图导航、响应式编程、Spring 5 响应式框架和编程等内容。

需要注意的是,Kotlin 中并没有我们熟悉的 new 关键字。你可以这样来直接声明一个类的对象:

复制代码
val bird = Bird()

当前我们并没有给 Bird 类传入任何参数。现实中,你很可能因为需要传入不同的参数组合,而在类中创建多个构造方法,在 Java 中这是利用构造方法重载来实现的。

复制代码
class Bird {
private double weight;
private int age;
private String color;
public Bird(double weight, int age, String color) {
this.weight = weight;
this.age = age;
this.color = color;
}
public Bird(int age, String color) {
this.age = age;
this.color = color;
}
public Bird(double weight) {
this.weight = weight;
}
...
}

我们发现 Java 中的这种方式存在两个缺点:

  • 如果要支持任意参数组合来创建对象,那么需要实现的构造方法将会非常多。
  • 每个构造方法中的代码会存在冗余,如前两个构造方法都对 age 和 color 进行了相同的赋值操作。

Kotlin 通过引入新的构造语法来解决这些问题,我们来看看它具体是如何做的。

  1. 构造方法默认参数

要解决构造方法过多的问题,似乎也很简单。在 Kotlin 中你可以给构造方法的参数指定默认值,从而避免不必要的方法重载。我们现在用 Kotlin 来改写上述的例子:

复制代码
class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue")
// 可以省略{}

竟然用一行代码就搞定了。我们可以实现与 Java 版本等价的效果:

复制代码
val bird1 = Bird(color = "black")
val bird2 = Bird(weight = 1000.00, color = "black")

需要注意的是,由于参数默认值的存在,我们在创建一个类对象时,最好指定参数的名称,否则必须按照实际参数的顺序进行赋值。比如,以下最后一个例子在 Kotlin 中是不允许的:

复制代码
>>> val bird1 = Bird(1000.00)
>>> bird2 = Bird(1000.00, 1, "black")
>>> val bird3 = Bird(1000.00, "black")
error: type mismatch: inferred type is kotlin.String but kotlin.Int was expected

如之前所言,我们在 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
}
}
  1. init 语句块

Kotlin 引入了一种叫作 init 语句块的语法,它属于上述构造方法的一部分,两者在表现形式上却是分离的。Bird 类的构造方法在类的外部,它只能对参数进行赋值。如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用 init 语句块来执行。比如:

复制代码
class Bird(weight: Double, age: Int, color: String) {
init {
println("do some other things")
println("the weight is ${weight}")
}
}

如你所见,当没有 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,它们会在对象被创建时按照类中从上到下的顺序先后执行。看看以下代码的执行结果:

复制代码
class Bird(weight: Double, age: Int, color: String) {
val weight: Double
val age: Int
val color: String
init {
this.weight = weight
println("The bird's weight is ${this.weight}.")
this.age = age
println("The bird's age is ${this.age}.")
}
init {
this.color = color
println("The bird's color is ${this.color}.")
}
}
fun main(args: Array<String>) {
val bird = Bird(1000.0, 2, "bule")
}
// 运行结果
The bird's weight is 1000.0.
The bird's age is 2.
The bird's color is bule.

可以发现,多个 init 语句块有利于我们进一步对初始化的操作进行职能分离,这在复杂的业务开发(如 Android)中显得特别有用。

再来思考一种场景,现实中我们在创建一个类对象时,很可能不需要对所有属性都进行传值。其中存在一些特殊的属性,比如鸟的性别,我们可以根据它的颜色来进行区分,所以它并不需要出现在构造方法的参数列表中。

有了 init 语句块的语法支持,我们很容易实现这一点。假设黄色的鸟儿都是雌性,剩余的都是雄鸟,我们就可以如此设计:

复制代码
class Bird(val weight: Double, val age: Int, val color: String) {
val sex: String
init {
this.sex = if (this.color == "yellow") "male" else "female"
}
}

我们再来修改下需求。这一次我们并不想在 init 语句块中对 sex 直接赋值,而是调用一个专门的 printSex 方法来进行,如:

复制代码
class Bird(val weight: Double, val age: Int, val color: String) {
val sex: String
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()
}
// 运行结果
Error:(2, 1) Property must be initialized or be abstract
Error:(5, 8) Val cannot be reassigned

结果报错了,主要由以下两个原因导致:

  • 正常情况下,Kotlin 规定类中的所有非抽象属性成员都必须在对象创建时被初始化值。
  • 由于 sex 必须被初始化值,上述的 printSex 方法中,sex 会被视为二次赋值,这对 val 声明的变量来说也是不允许的。

第 2 个问题比较容易解决,我们把 sex 变成用 var 声明,它就可以被重复修改。关于第 1 个问题,最直观的方法是指定 sex 的默认值,但这可能是一种错误的性别含义;另一种办法是引入可空类型(我们会在第 5 章具体介绍),即把 sex 声明为“String?”类型,则它的默认值为 null。这可以让程序正确运行,然而实际上也许我们又不想让 sex 具有可空性,而只是想稍后再进行赋值,所以这种方案也有局限性。

  1. 延迟初始化:by lazy 和 lateinit

更好的做法是让 sex 能够延迟初始化,即它可以不用在类对象初始化的时候就必须有值。在 Kotlin 中,我们主要使用 lateinit 和 by lazy 这两种语法来实现延迟初始化的效果。下面来看看如何使用它们。

如果这是一个用 val 声明的变量,我们可以用 by lazy 来修饰:

复制代码
class Bird(val weight: Double, val age: Int, val color: String) {
val sex: String by lazy {
if (color == "yellow") "male" else "female"
}
}

总结 by lazy 语法的特点如下:

  • 该变量必须是引用不可变的,而不能通过 var 来声明。
  • 在被首次调用时,才会进行赋值操作。一旦被赋值,后续它将不能被更改。

lazy 的背后是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,第一次访问该属性时,会执行 lazy 对应的 Lambda 表达式并记录结果,后续访问该属性时只是返回记录的结果。

另外系统会给 lazy 属性默认加上同步锁,也就是 LazyThreadSafetyMode.SYNCHRON IZED,它在同一时刻只允许一个线程对 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"
}

与 lazy 不同,lateinit 主要用于 var 声明的变量,然而它不能用于基本数据类型,如 Int、Long 等,我们需要用 Integer 这种包装类作为替代。相信你已经猜到了,利用 lateinit 我们就可以解决之前的问题,就像这样子:

复制代码
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

Delegates.notNull<T>
你可能比较好奇,如何让用 var 声明的基本数据类型变量也具有延迟初始化的效果,一种可参考的解决方案是通过 Delegates.notNull<T>,这是利用 Kotlin 中委托的语法来实现的。我们会在后续介绍它的具体用法,当前你可以通过一个例子来认识这种神奇的效果:

复制代码
var test by Delegates.notNull<Int>()
fun doSomething() {
test = 1
println("test value is ${test}")
test = 2
}

总而言之,Kotlin 并不主张用 Java 中的构造方法重载,来解决多个构造参数组合调用的问题。取而代之的方案是利用构造参数默认值及用 val、var 来声明构造参数的语法,以更简洁地构造一个类对象。那么,这是否可以说明在 Kotlin 中真的只需要一个构造方法呢?

Kotlin核心编程(46):面向对象 3.1.2

购书地址 https://item.jd.com/12519581.html?dist=jd

评论

发布