在 Java 中一个类可以声明一个或多个构造方法,Kotlin 也是类似的,只是做出了一点修改:区分了主构造方法 (通常是主要而间接的初始化类的方法,并且在类体外部声明) 和从构造方法(在类体内部声明)。同样也允许在初始化语句块中添加额外的初始化逻辑。
初始化类:主构造方法和初始化语句块
class User(val nickname: String)
通常来讲,类的所有声明都在花括号中,但上面这个类没有花括号而是只包含了声明在括号中。这段被括号围起来的语句块就叫做主构造方法。它主要有两个目的:表明构造方法的参数,以及定义使用那些参数初始化的属性。它的工作原理以及完成同样事情的最明确的代码如下:
// 带一个参数的主构造方法
class User constructor(_nickname: String) {
val nickname: String
// 初始化语句块
init {
nickname = _nickname
}
}
在这个例子中,constructor
关键字用来开始一个主构造方法或从构造方法的声明。init
关键字用来引入一个初始化语句块。这种语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用。因为主构造方法有语法限制,不能包含初始化代码,这就是为什么要使用初始化语句块的原因。也可以在一个类中声明多个初始化语句块。
构造方法参数 _nickname 中的下划线用来区分属性的名称和构造方法参数的名字。另一个可选方案是使用同样的名字,通过 this 来消除歧义,就像 Java 中的常用做法一样:this.nickname = nickname。
在这个例子中,不需要把初始化代码放在初始化语句块中,因为它可以与 nickname 属性的声明结合。如果主构造方法没有注解或可见性修饰符,同样可以去掉 constructor 关键字,代码如下:
// 带一个参数的主构造方法
class User(_nickname: String) {
// 用参数来初始化属性
val nickname = _nickname
}
这就是声明同样的类的另一种方法。墙面两个例子在类体中使用 val 关键字声明了属性,如果属性用相应的构造方法参数来初始化,代码可以通过把 val 关键字加在参数前的方式来进行简化。可以替换类中的属性定义:
// "val" 意味着相应的属性会用构造方法的参数来初始化
class User(val nickname: String)
所有 User 类的声明都是等价的,但是最后一个使用了最简明的语法。
可以像函数一样为构造方法参数声明一个默认值:
// 为构造方法参数提供一个默认值
class User(val nickname: String, val isSubscribed: Boolean = true)
创建一个类的实例,只需要直接调用构造方法,不需要 new 关键字:
// 为 isSubscribed 参数使用默认值 “true”
val alice = User("Alice")
// 可以按照声明顺序写明所有的参数
val bob = User("Bob", false)
// 可以显式的为某些构造方法参数表明名称
val carol = User("Carol", isSubscribed = false)
注意
如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这可以让 Kotlin 使用库时变得更简单,因为可以通过无参构造方法来实例化类。
如果类具有一个父类,主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点:
open class User(val nickname: String) {...}
class TwitterUser(nickname: String) : User(nickname) {...}
如果没有给一个类声明任何的构造方法,将会生成一个不做任何事情的默认构造方法:
// 将会生成一个不带任何参数的默认构造方法
open class Button
如果继承了 Button 类并且没有提供任何的构造方法,必须显式的调用父类的构造方法,即使它没有任何的参数:
class RadioButton: Button()
注意与接口的区别:接口没有构造方法,所以在实现一个接口的时候,不需要在父类型列表中它的名称后面加上括号。
如果想要确保类不被其他代码实例化,必须把构造方法标记为 private。把主构造方法标记为 private 代码如下:
// 这个类有一个 private 构造方法
class Secretive private constructor() {}
因为 Secretive 类只有一个 private 的构造方法,这个类外部的代码不能实例化它。
private 构造方法的替代方案
在 Java 中,可以通过使用 private 构造方法禁止实例化这个类来表示一个更通用的意思:这个类是一个静态实用工具成员的容器或者是单例的。Kotlin 针对这种目的具有内建的语言级别的功能。可以使用顶层函数作为静态实用工具,要想表示单例,可以使用对象声明。
在大多数真实的场景中,类的构造方法是非常简明的:它要么没有参数或者直接把参数与对应的属性关联。这就是为什么 Kotlin 有为主构造方法设计的简洁的语法:在大多数的情况下都能很好地工作。
构造方法:用不同的方式来初始化父类
通常来讲,使用多个构造方法的类在 Kotlin 代码中不如在 Java 中常见。大多数在 Java 中需要重载构造方法的场景都被 Kotlin 支持参数默认值和参数命名的语法涵盖了。
Tips
不要声明多个从构造方法来重载和提供参数的默认值。取而代之的是,应该直接标明默认值。
但是还是会有需要多个构造方法的情景。最常见的一种就来自于当需要扩展一个框架来提供多个构造方法,以便于通过不同的方式来初始化类的时候。如一个在 Java 中声明的具有两个构造方法的类,Kotlin 中相似的声明如下:
open class View {
constructor(ctx: Context) {
// some code
}
constructor(ctx: Context, attr: AttributeSet) {
// some code
}
}
这个类没有声明一个主构造方法(因为类头部的类名后面并没有括号),但是它声明了两个从构造方法。从构造方法使用 constructor 关键字引出。只要需要们可以声明任意多个从构造方法。
如果想扩展这个类,可以声明同样的构造方法:
class MyButton: View {
constructor(ctx: Context) : super(ctx) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
这里定义了两个构造方法,他们都是用 super() 关键字调用了对应的父类构造方法。就像在 Java 中一样,也可以使用 this() 关键字,从一个构造方法中调用自己类中的另一个构造方法,如下:
class MyButton: View {
// 委托给这个类的另一个构造方法
constructor(ctx: Context) : this(ctx, MY_STYLE) {
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
可以修改 MyButton 类使得一个构造方法委托给同一个类的另一个构造方法(使用 this),为参数传入默认值。如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做了的构造方法,每个构造方法必须以一个朝外的箭头开始并且结束于任意一个基类构造方法。
实现在接口中声明的属性
在 Kotlin 中,接口可以包含抽象属性声明。如下:
interface User {
val nickname: String
}
这就意味着实现 User 接口的类需要提供一个取得 nickname 值的方式。接口并没有说明这个值应该存储到一个支持字段还是通过 getter 来获取。接口本身并不包含任何状态,因此只是实现这个接口的类在需要的情况下会存储这个值。
// 代码清单 2.1 实现一个接口属性
class PrivateUser(override val nickname: String) : User
class SubscriberingUser(val email: String) : User {
override val nickname: String
// 自定义 getter
get() = email.substringBefore('@')
}
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId)
}
对于 PrivateUser 来说,使用了简洁的语法直接在主构造方法中声明了一个属性。这个属性实现了来自 User 的抽象属性,所以将其标记为 override。
对于 SubscribingUser 来说,nickname 属性通过一个自定义 getter 实现,这个属性没有一个支持字段来存储它的值,它只有一个 getter 在每次调用时从 email 中得到昵称。
对于 FacebookUser 来说,在初始化时将 nickname 属性与值关联。使用了被认为可以通过账号 ID 返回 Facebook 用户名称的 getFacebookName 函数。
除了抽象属性声明外,接口还可以包含具有 getter 和 setter 的属性,只要它们没有应用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的)。
通过 getter 或 setter 访问支持字段
前面介绍了属性的两种类型:存储值的属性和具有自定义访问器在每次访问时计算值的属性。若想要结合这两种来实现一个既可以存储值又可以在值被访问和修改时提供额外逻辑的属性,就需要能够从属性的访问其中访问它的支持字段。
假设想在任何对存储在属性中的数据进行修改时输出日志,声明了一个可变属性并且在每次 setter 访问时执行额外的代码:
// 代码清单 2.2 在 setter 中访问支持字段
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent())// 读取支持字段的值
// 更新支持字段的值
field = value
}
}
可以像平常一样通过使用 user.address = "new value" 来修改一个属性的值,这其实在底层调用了 setter。
在 setter 的函数体中,使用了特殊的标识符 field
来访问支持字段的值。在 getter 中,只能读取值;在 setter 中,既能读取它也能修改它。
可以只重定义可变属性的一个访问器。
访问属性的方式不依赖于它是否有支持字段,如果显式地引用或使用默认的访问器实现,编译器会为属性生成支持字段。如果提供了一个自定义的访问器实现并且没有使用 field
,支持字段将不会被呈现出来。
修改访问器的可见性
访问器的可见性默认与属性的可见性相同。但是如果需要,可以通过在 get 和 set 关键字前放置可见性修饰符的方式来修改它。
// 代码清单 2.3 声明一个具有 private setter 的属性
class LengthCounter {
var counter: Int = 0
// 不能在类外部修改这个属性
private set
fun addWord(word: String) {
counter += word.length
}
}
这个类用来计算单词加在一起的总长度。持有总长度的属性是 public 的,因为它是这个类提供给客户的 API 的一部分。但是需要确保它只能在类中被修改,否则外部代码有可能会修改它并存储一个不正确的值。因此,让编译器生成一个默认可见性的 getter 方法,并且将 setter 的可见性修改为 private。
网友评论