[TOC]

概述

object 关键字在 kotlin 中有两种使用场景:对象表达式 (object expressions)和对象声明(object declarations)。本文将对这两种使用场景分别说明。

对象表达式

有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。Java 用匿名内部类 处理这种情况。
Kotlin 用对象表达式对象声明对这个概念稍微概括了下。

创建继承某个(或某些)类型的匿名类的对象,这些类型可以是接口(以给 Button 设置点击事件为例):

1
2
3
4
5
6
7
8
9
10
11
12
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
Log.d(MainActivity::class.java.simpleName, "click button")
}
})
}
}

当然也可以是抽象类(以属性动画中的 AnimatorListenerAdapter 为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
val valueAnimator = ValueAnimator.ofInt(0, 5)
valueAnimator.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
super.onAnimationStart(animation)
Log.d(MainActivity::class.java.simpleName, "onAnimationStart")
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
Log.d(MainActivity::class.java.simpleName, "onAnimationEnd")
}
})
valueAnimator.duration = 2000L
valueAnimator.start()

那么,超类型是接口和类的区别和联系是什么呢?

相同的地方是都是要把 object 关键字放在前面,后面加:,再加上接口或者类,在花括号里重写方法;
不同的地方是接口后面没有小括号,而类后面必须有小括号。

从上面的对象表达式的用法可以看到,它和 Java 中的匿名内部类的作用是可以对应得上的。

但是,如果完全把 object 对象表达式的用法和 Java 中的继承一个父类或者实现一个接口的匿名内部类对应,就太局限了。

实际上,对象表达式要更加强大

对象表达式可以同时继承一个类以及多个接口,或者同时继承多个接口,匿名内部类却不能这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
open class Man(_name: String) {
val name = _name
}

interface Flyable {
fun fly()
}

interface SuperHearing {
fun hearSubtleNoises()
}

interface SuperSmell {
fun trackBySmell()
}

interface SuperVision {
fun seeThroughWalls()
}

fun main() {
val superMan = object: Man("Clark"), Flyable, SuperHearing, SuperVision {
override fun hearSubtleNoises() {
}
override fun seeThroughWalls() {
}
override fun fly() {
}
}
val superPower = object: Flyable, SuperHearing,SuperVision {
override fun fly() {
}

override fun hearSubtleNoises() {
}

override fun seeThroughWalls() {
}

}
}

如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。
多个超类型可以由跟在冒号后面的逗号分隔的列表指定:

1
2
3
4
5
6
7
8
9
10

open class A(x: Int) {
public open val y: Int = x
}

interface B { …… }

val ab: A = object : A(1), B {
override val y = 15
}

对象表达式后面可以不跟任何类或者接口,这时它仅仅表示一个对象,封装一些数据和方法,而匿名内部类不能这样,那么我们可以简单地写:

1
2
3
4
5
6
7
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型

会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}

// 公有函数,所以其返回类型是 Any
fun public Foo() = object {
val x: String = "x"
}

fun bar() {
val x1 = foo().x // 没问题
val x2 = public Foo().x // 错误:未能解析的引用“x”
}
}

就像 Java 匿名内部类一样,对象表达式中的代码可以访问来自包含它的作用域的变量。
(与 Java 不同的是,这不仅限于 final 或实际相当于 final 的变量。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0

window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}

override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// ……
}

对象声明

单例模式在一些场景中很有用,而 Kotlin(继 Scala 之后)使单例声明变得很容易:

1
2
3
4
5
6
7
8
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}

val allDataProviders: Collection<DataProvider>
get() = // ……
}

这称为对象声明。并且它总是在 object {: .keyword } 关键字后跟一个名称。
就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。

对象声明的初始化过程是线程安全的。

如需引用该对象,我们直接使用其名称即可:

1
DataProviderManager.registerDataProvider(……)

这些对象可以有超类型:

1
2
3
4
5
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { …… }

override fun mouseEntered(e: MouseEvent) { …… }
}

注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。

伴生对象

类内部的对象声明可以用 companion{: .keyword } 关键字标记:

1
2
3
4
5
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}

该伴生对象的成员可通过只使用类名作为限定符来调用:

1
val instance = MyClass.create()

可以省略伴生对象的名称,在这种情况下将使用名称 Companion

1
2
3
4
5
class MyClass {
companion object { }
}

val x = MyClass.Companion

其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象
(无论是否命名)的引用:

1
2
3
4
5
6
7
8
9
10
11
class MyClass1 {
companion object Named { }
}

val x = MyClass1

class MyClass2 {
companion object { }
}

val y = MyClass2

请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:

1
2
3
4
5
6
7
8
9
10
11
interface Factory<T> {
fun create(): T
}

class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}

val f: Factory<MyClass> = MyClass

当然,在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正静态方法和字段。更详细信息请参见Java 互操作性一节

两者差异

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。