在 Java 中,一个类可以声明一个或多个构造方法。 Kotlin 也是类似的,只是做出了一点修改:区分主构造方法(通常是主要而简洁的初始化类的方法,并且在类体外部声明)和从构造方法(在类体内部声明)。同样也允许在初始化语法块中添加额外的初始化逻辑。
1 初始化类:主构造方法和初始化语句块
声明一个简单的类:
class User(val nickname: String)
通常来讲,类的所有声明都在花括号中。那么,这个类为什么没有花括号,而只是在括号中声明了属性?
其中被括号包裹起来的语句块就叫主构造方法。它主要有两个目的:表明构造方法的参数,以及定义使用这些参数初始化的属性。
将其转换成 Java 代码:
public final class User {
@NotNull
private final String nickname;
@NotNull
public final String getNickname() {
return this.nickname;
}
public User(@NotNull String nickname) {
Intrinsics.checkNotNullParameter(nickname, "nickname");
super();
this.nickname = nickname;
}
}
下面是用来完成同样功能的代码:
class User constructor(_nickname: String) { // 这里加上下划线是为了更好的区分
val nickname: String
init {
this.nickname = _nickname
}
}
在上面的例子中,可以看到两个新的 Kotlin 关键字:constructor 和 init:
- constructor 关键字用来开始一个主构造方法或从构造方法的声明;
- init 关键字用来引入一个初始化语句块。这种语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用。因为主构造方法有语法限制,不能包含初始化代码,这就是为什么要使用初始化语句块的原因。如果需要,也可以在一个类中声明多个初始化语句块;
构造方法参数 _nickName 中的下划线用来区分属性的名称和构造方法参数的名字。另一个可选方案是使用相同的名字,通过 this 来消除歧义,就像 Java 中的常用做法一样:this.nickName = nickName。
在这个例子中,不需要把初始化代码放在初始化语句块中,因为它可以与 nickName 属性的声明结合。 如果主构造方法没有注解或可见性修饰符,同样也可以去掉 constructor 关键字。
class User(_nickname: String) {
val nickname = _nickname
}
这就是声明同样类的另一种方法。
前面的两个例子在类体中使用 val 关键字声明了属性。 如果属性用相应的构造方法参数来初始化,代码可以通过 val 关键字加在参数前的方式来进行简化。 这样可以替换类中的属性定义:
class User(val nickname: String)
所有 User 类的声明都是等价的。
可以像函数参数一样为构造方法参数声明一个默认值:
class User(val nickname: String, val isSubscribed: Boolean = true)
要创建一个类的实例,只需要直接调用构造方法,不需要 new 关键字:
val eileen = User("Eileen")
println(eileen.isSubscribed) // true
val ross = User("Ross", false)
println(ross.isSubscribed) // false
val monica = User("Monica", isSubscribed = false)
println(monica.isSubscribed) // false
注意:如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这可以让 Kotlin 使用库时变得更简单,因为可以通过无参构造方法来实例化类。
如果我们的类具有一个父类,主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点:
class Son(nickname: String) : User(nickname) {
}
如果没有给一个类声明任何的构造方法,将会生成一个不做任何事情的默认构造方法:
open class Button
注意:Kotlin 中任何类,包括 object/data class/sealed class,都有一个默认的无参构造函数。如果显式的声明了构造函数,默认的无参构造函数就失效了。
如果继承了 Button 类并且没有提供任何的构造方法,必须显式地调用父类的构造方法,即使它没有任何的参数:
class RadioButton : Button()
为什么要在父类名称后面加一个空的括号?这是和接口做区分,接口没有构造方法,所以在实现一个接口的时候,不需要在父类型列表中它的名称后面再加上括号。
如果我们想要确保我们的类不被其他代码实例化,必须把构造方法标记为 private。下面就是怎样把主构造方法标记为 private:
class Secretive private constructor() {}
因为 Secretive 类只有一个 private 构造方法,这个类外部的代码不能实例化它。
private 构造方法的替代方案:在 Java 中,可以通过使用 private 构造方法禁止实例化这个类来表示一个更通用的意思:这个类是一个静态实用工具成员的容器或者是单例的。
在大多数真实的场景中,类的构造方法是非常简明的:它要么没有参数或者直接把参数和相应的属性关联。
2 构造方法:用不同的方式来初始化父类
通常来讲,使用多个构造方法的类在 Kotlin 代码中不如在 Java 中常见。 大多数在 Java 中需要重载构造方法的场景的场景都被 Kotlin 支持参数默认值和参数命名的语法涵盖了。
这里需要注意的是:不要声明多个从构造方法用来重载和提供参数的默认值。取而代之的是,应该直接标明默认值。
但是还是会有需要多个构造方法的情景。最常见的一种就是来自于当我们需要扩展一个框架类来提供多个构造方法,以便于通过不同的方式来初始化类的时候。
设想一下一个在 Java 中声明的具有两个构造方法的 View 类,Kotlin 中相似的声明如下:
open class View {
constructor(ctx: Context) {
}
constructor(ctx: Context, attr: AttributeSet) {
}
}
这个类没有声明一个主构造方法,但是它声明了两个从构造方法。从构造方法使用 constructior 关键字引出。只要需要,可以声明任意多个从构造方法。
class DefaultButton : View {
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
}
}
这里定义了两个构造方法,它们都是用 super() 关键字调用了对应的父类构造方法。
就像是在 Java 中一样,也可以使用 this() 关键字,从一个构造方法中调用我们自己的类的另一个构造方法。下面是使用:
class DefaultButton : View {
constructor(ctx: Context) : this(ctx, MY_STYLE)
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
}
}
可以修改 DefaultButton 类使得一个构造方法委托给另一个类的另一个构造方法(使用 this),为参数传入默认值,第二个构造方法继续调用 super()。
如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做的构造方法。
Java 的互操作性是我们需要使用从构造方法的主要使用场景。
特殊类:
object/companion object 是对象示例,作为单例类或者伴生对象,没有构造函数。
data class 要求必须有一个含有至少一个成员属性的主构造函数,其余方面和普通类相同
sealed class 只是声明类似抽象类一般,可以有主构造函数,含参无参以及次级构造等。
3 执行次序
主构造函数 —> init 代码块 —> 次构造函数
class User(name: String) {
val _name: String = name // 这里是主构造方法的一部分
constructor(name: String, age: Int) : this(name) {
println("this.name = ${this._name}")
println("secondary constructor")
println("======================")
}
init {
println("this.name = ${this._name}")
println("first init")
println("+++++++++++++++++++++++++++")
}
init {
println("this.name = ${this._name}")
println("second init")
println("*****************************")
}
}
val user = User("Eileen", 30)
//this.name = Eileen
//first init
//+++++++++++++++++++++++++++
//this.name = Eileen
//second init
//*****************************
//this.name = Eileen
//secondary constructor
//======================
Kotlin 中若存在主构造函数,其不能有代码执行,init 代码块可以起到补充的作用,在类初始化的时候执行相关的代码块
在主构造函数中,形参有 var/val,那么就变成了成员属性的声明。这些属性声明是早于 init 代码块的;
即使在类的继承体系中,各自的 init 也是优先于构造函数执行的。
class Student(name: String, sex: Boolean) : User(name) {
override val _name: String = name // 这里是主构造方法的一部分
val _sex: Boolean = sex
constructor(name: String, sex: Boolean, grade: Int) : this(name, sex) {
println("this.name = ${this._name} this.sex = ${this._sex}")
println("secondary constructor")
println("Student======================")
}
init {
println("this.name = ${this._name} this.sex = ${this._sex}")
println("first init")
println("Student+++++++++++++++++++++++++++")
}
init {
println("this.name = ${this._name} this.sex = ${this._sex}")
println("second init")
println("Student*****************************")
}
}
//this.name = null
//first init
//User+++++++++++++++++++++++++++
//this.name = null
//second init
//User*****************************
//this.name = Ross this.sex = false
//first init
//Student+++++++++++++++++++++++++++
//this.name = Ross this.sex = false
//second init
//Student*****************************