《Kotlin核心编程》 ——3.1.2 更简洁地构造类的对象
3.1.2 更简洁地构造类的对象
需要注意的是,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
}
}
2. 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具有可空性,而只是想稍后再进行赋值,所以这种方案也有局限性。
3.延迟初始化: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中真的只需要一个构造方法呢?
- 点赞
- 收藏
- 关注作者
评论(0)