1. 前言
我们知道,在 Java 中一个类可以声明一个或多个构造方法。而在 Kotlin 中,也是类似的,但是略有不同,区分了主构造方法和次构造方法。
本文将介绍主构造方法和次构造方法的使用,以及它们之间的区别和联系。
2. 正文
2.1 主构造方法
主构造方法就是主要而简洁的初始化类的方法,它是在类体外部声明的,是类头的一部分:它在类名的后面并有可选的参数,例子如下:
class User constructor(_nickname: String) { // 带一个参数的主构造方法
val nickname: String
init { // 初始化代码块
nickname = _nickname
}
}
这个例子用到了两个 Kotlin 关键字:constructor
和 init
。现在做一下说明:constructor
关键字用来开始一个主构造方法和从构造方法的声明。init
关键字用来引入一个初始化语句块。初始化语句块包含在类被创建时执行的代码,并与主构造方法一起使用。
因为主构造方法不能包含初始化代码,所以需要使用初始化语句块。
上面的例子可以写成下面这样:
class User(_nickname: String) {
val nickname = _nickname // 不使用初始化代码块,用参数来初始化属性
}
这里没有把初始化代码放在初始化语句块中,而是用参数来初始化属性。去掉了 constructor
关键字,因为这里主构造方法没有注解或可见性修饰符。
上面的例子可以进一步简化:
class User(val nickname: String) // "val" 表示相应的属性会用构造方法的参数来初始化
到这儿,已经是最简洁的写法了。
Ps:
-
在一个类中,可以声明多个初始化语句块,那么初始化语句块和属性初始化器的执行顺序是什么样的?
class InitOrder(name: String) { val firstProperty = "First property: $name".also { println(it) } init { println("First initializer block that prints $name") } val secondProperty = "Second property: $name".also { println(it) } init { println("Second initializer block that prints $name") } } fun main(args: Array<String>) { InitOrder("patient") } /* * 打印结果: * First property: patient * First initializer block that prints patient * Second property: patient * Second initializer block that prints patient */
从上面的例子可以看出,打印顺序就是它们出现在代码中的顺序。并且,也可以知道主构造器中的参数不仅可以用在初始化代码块中,也可以用在属性初始化器中。
- 主构造器中的参数可以声明为
val
或者var
。 - 如果主构造器有注解或者可见性修饰符,那么
constructor
这个关键字就是必需的。注解或修饰符位于constructor
关键字前面。
class Customer private constructor(name: String) class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defaultStyleAttr: Int = 0 ) : View(context, attrs, defaultStyleAttr)
- 可以像函数参数一样为构造方法参数声明一个默认值。需要注意的是,如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。看下面的例子:
class User5( val nickname: String = "wzc", val isSubscribed: Boolean = true ) fun main(args: Array<String>) { val wzc = User5() // 这个就是额外生成的不带参数的构造方法,但使用了所有的默认值 println("nickname=${wzc.nickname},isSubscribed=${wzc.isSubscribed}") val john = User5("John") println("nickname=${john.nickname},isSubscribed=${john.isSubscribed}") val peter = User5("Peter", false) println("nickname=${peter.nickname},isSubscribed=${peter.isSubscribed}") val may = User5("May", isSubscribed = false) println("nickname=${may.nickname},isSubscribed=${may.isSubscribed}") val tiger = User5(isSubscribed = false, nickname = "Tiger") println("nickname=${tiger.nickname},isSubscribed=${tiger.isSubscribed}") } /* nickname=wzc,isSubscribed=true nickname=John,isSubscribed=true nickname=Peter,isSubscribed=false nickname=May,isSubscribed=false nickname=Tiger,isSubscribed=false */
- 如果你的类具有一个父类,主构造方法同样需要初始化父类。
open class User6(val nickname: String) class TwitterUser(nickname: String) : User6(nickname) fun main(args: Array<String>) { val twitterUser = TwitterUser("alex") println(twitterUser.nickname) }
- 如果一个非抽象类没有声明任何的构造方法(主构造方法或从构造方法),那么它就会有一个生成的不带任何参数的主构造方法。这个构造方法的可见性是 public 的。private 主构造方法,那么这个类外部的代码不能实例化它。这点在单例模式中会用到。
- 在主构造方法中使用参数默认值和参数命名,而不是使用多个从构造方法来重载和提供参数的默认值。
- 主构造方法里的参数带
val
或var
,与不带val
或var
的区别是什么?
class Dog(_name: String) { val name = _name fun show() { // println(_name) // 编译错误:不能引用到 _name } } class Cat(_name: String) { val name: String init { name = _name } } class Fish(val name: String) { fun show() { println(name) } }
可以看到带
val
或var
,可以在方法中直接使用;而不带的,只能用于属性初始化,或者初始化代码块,而不能用于方法中。 - 主构造器中的参数可以声明为
2.2 从构造方法
从构造方法使用 constructor
关键字引出。可以声明任意多个从构造方法。
例子:
没有主构造方法,有两个从构造方法的例子
class MyBehavior : CoordinatorLayout.Behavior<View> {
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
}
如果类有一个主构造方法,那么每一个次构造方法都需要委托给主构造方法;
class User constructor(name: String) {
var name: String
init {
println("init")
this.name = name
}
constructor(): this("unknown") {
println("second")
}
}
fun main(args: Array<String>) {
val user = User()
println(user.name)
}
/*
打印结果:
init
second
unknown
*/
从打印结果可以看出,主构造方法是首先执行的。
如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做了的构造方法。
从构造方法和初始化代码块的执行顺序:
class Constructors {
init {
println("Init block")
}
constructor(i: Int) {
println("Second constructor")
}
}
fun main(args: Array<String>) {
Constructors(1)
}
/*
Init block
Second constructor
*/
从构造方法的主要使用场景:Java 的互操作性;当你使用不同的参数列表,以多种方法创建类的实例时,使用不同的参数列表。
3. 最后
简单总结一下:
- 一个类至多可以声明一个主构造方法,但一个类可以声明任意多个从构造方法。
- 主构造方法的
constructor
关键字可以省略,但从构造方法的constructor
关键字不可以省略。 - 主构造方法不可以包含初始化代码块,但从构造方法可以包含初始化代码块。
那么最后再说一下:主构造方法和从构造方法,怎么选用呢?
优先考虑使用主构造方法;如果主构造方法不能解决,再去考虑从构造方法。
参考
- https://kotlinlang.org/docs/reference/classes.html#constructors;
- 《Kotlin 实战》