对象声明
在面向对象系统设计里,使用单例模式是相当常见的情形,在 Java 中,这通常通过一个使用 private 构造方法并且用静态字段来持有唯一实例的方式来实现。而在Kotlin中,这个功能可以通过对象声明功能实现:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}
val allDataProviders: Collection<DataProvider>
get() = // ……
}
对象声明通过 object 关键字引入,一个对象声明包括一个类的定义和一个该类的变量。一个声明关键字加变量名,是不是和变量声明有点像?这就是它被叫做对象声明的原因。
与类一样,一个对象声明可以包含属性,方法,初始化语句块等,但不能拥有初始化方法(包括主构造方法和次构造方法)。这是因为对象声明与普通类实例不同,它在定义的时候就已经创建了,不可能在其他地方再调用构造方法,所以为它创建构造方法是没有意义的。
和变量一样,对象声明通过对象名 + .字符的方式调用方法和访问属性:
DataProviderManager.registerDataProvider(DataProvider(...))
val dataProviders = DataProviderManager.allDataProviders
你可以让对象声明继承类和接口,也可以在类中声明对象声明(这和Java的静态成员方法有点像,在后面的伴生对象再说)。
对象表达式
object 也可以用来声明匿名对象,匿名对象作为Java中的匿名内部类的代替。首先,让我们看看最常用的写法:
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ……
}
override fun mouseEntered(e: MouseEvent) {
// ……
}
})
上面这段代码看起来像使用一个省略名字的对象声明继承了MouseAdapter,并且重载了一些方法。这就是匿名内部类在Kotlin中的写法。
如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的新成员将无法访问。
伴生对象
Java 中的 static 关键字并不是Kotlin的一部分。作为代替,你可以使用顶层函数取代Java中的静态方法。但是顶层函数是不能访问类的private成员的,那么怎么办呢?也许你想到了刚刚说过的对象声明。是的,如果你需要一个在没有类实例的情况下访问类内部方法的函数,你可以使用对象声明,就像下面这样:
class MyClass {
object Factory {
fun create(): MyClass {
val myClass = MyClass()
myClass.hello()
return myClass
}
}
private fun hello() = println("hello world!")
}
这样也算完成了目标,但事实上,我们可以做得更简单——使用一个 companion 关键字标记类中定义的对象,这个对象就可以选择不显式地标明名称,你还可以直接通过容器类名称来访问这个对象的方法和属性。于是最终的语法看起来非常像Java中的静态方法调用:
class MyClass {
companion object {
fun create(): MyClass = MyClass()
}
}
val instance = MyClass.create() //通过类名调用方法
val x = MyClass.Companion //通过默认名称 Companion 获得对象
这个对象就叫作伴生对象。请注意,即使伴生对象的成员看起来像Java的静态成员,在运行时他们仍然是真实对象的实例成员,所以伴生对象可以实现接口和给伴生对象扩展函数。
interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
fun MyClass.Companion.createOther() {
//....
}
val instance = MyClass.createOther() //还是通过类名的方式调用
委托
Kotlin中的类默认是不支持继承的,但很多时候,你需要向某些类添加一些行为,这个时候常用的方式就是使用装饰器模式,这种模式的本质就是创建一个新类,实现原始类的接口而将其实例作为一个字段保存,然后实现新方法处理新的行为,与原始类有同样行为的方法直接转发给原始类实现处理。
这种方式虽然避免了由于继承层数过多导致的系统的脆弱性和复杂性,但是它有一个缺点就是需要很多的样板代码,即使你只是新添了一两个小功能,也需要实现所有原始类方法的转发。这时候委托的用处就体现出来了。
你可以使用关键字 by 将 一个类的实现委托给一个对象:
interface Base {
fun printMessage()
fun printMessageLine()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
override fun printMessageLine() { println(x) }
}
class Derived(b: Base) : Base by b {
override fun printMessage() { print("abc") }
}
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).printMessage() //输出 abc
Derived(b).printMessageLine() //输出 10
}
Derived 类可以通过将其所有公有成员都委托给指定对象来实现一个接口 Base。Derived 的超类型列表中的 by-子句表示 b 将会在 Derived 中内部存储, 并且编译器将生成转发给 b 的所有 Base 的方法。同时如果你覆盖了一个方法的话,编译器会使用 override 覆盖的实现而不是委托对象中的。
总结
本文主要是关于Kotlin中对象声明和委托两个特性的使用,与Java相比,Kotlin并没有增加什么新东西,但是相比较而言,Kotlin提供了更为简洁的实现方式。文章涉及到的关键字有:
关键字 | 说明 |
---|---|
object | 同时声明一个类及其实例 |
by | 将接口的实现委托给另一个对象 |
companion | 声明一个伴生对象 |