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