《Kotlin核心编程》 ——3.1.2 更简洁地构造类的对象

举报
华章计算机 发表于 2020/02/21 22:28:25 2020/02/21
【摘要】 本节书摘来自华章计算机《Kotlin核心编程》 —— 书中第3章,第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中真的只需要一个构造方法呢?


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。