本文将伴随大家进入Kotlin语言的正式学习生涯中,希望大家不要半途而废哦!笔者将Kotlin用于Android开发中,因此将从Android开发的视角叙述相关内容,同时将与Java语言有所联系。

1. 对象表达式

1.1 对象表达式的基本格式

对象表达式的最基本格式如下:

1
2
3
4
5
6
7
object {
var title: String = "对象表达式的基本格式"

fun changeTitle() {
title = "对象已改变"
}
}

对象可以继承于某个基类,或者实现其他接口:

1
2
3
4
5
6
7
8
9
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 对象表达式的使用场景

看过了对象表达式的基本格式后,是不是觉得很眼熟?没错,我在Kotlin征途(五):Kotlin类和对象中写的匿名内部类就是使用了对象表达式:

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
class Test {
var v = "成员属性"

fun setInterFace(test: TestInterFace) {
test.test()
}
}

/**
* 定义接口
*/
interface TestInterFace {
fun test()
}

fun main(args: Array<String>) {
var test = Test()

/**
* 采用对象表达式来创建接口对象,即匿名内部类的实例。
*/
test.setInterFace(object : TestInterFace {
override fun test() {
println("对象表达式创建匿名内部类的实例")
}
})
}

1.3 作用域

匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}

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

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

在对象表达中可以方便的访问到作用域中的其他变量:

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++
}
})
// ……
}

2. 对象声明(单例模式)

2.1 对象声明(单例模式)的格式

Kotlin 使用 object 关键字来声明一个对象。我们可以方便的通过对象声明来获得一个单例。

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

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

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

1
DataProviderManager.registerDataProvider(……)

Java中则需要特殊使用:

1
DataProviderManager.INSTANCE.registerDataProvider(……)

当然你也可以定义一个变量来获取获取这个对象,当时当你定义两个不同的变量来获取这个对象时,你会发现你并不能得到两个不同的变量。也就是说通过这种方式,我们获得一个单例。

1
2
3
4
var data1 = DataProviderManager
var data2 = DataProviderManager
data1.name = "test"
print("data1 name = ${data2.name}")

2.2 与对象表达式的异同

与对象表达式相同的是,对象可以有超类型,可以实现接口:

1
2
3
4
5
6
7
8
9
object DefaultListener : MouseAdapter(), MouseInterface {
override fun mouseClicked(e: MouseEvent) {
// ……
}

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

与对象表达式不同,当对象声明在另一个类的内部时,这个对象并不能通过外部类的实例访问到该对象,而只能通过类名来访问,同样该对象也不能直接访问到外部类的方法和变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student {
var name = "Mike"
object Book{
var count = 3
fun showName(){
print{"show name: $name"} // 错误,不能访问到外部类的方法和变量
}
}
}

fun main(args: Array<String>) {
var student = Student()
student.Book.count // 错误,不能通过外部类的实例访问到该对象
Student.Book.count // 正确
}

3. 伴生对象

3.1 伴生对象的用法

类内部的对象声明可以用 companion 关键字标记,这样它就与外部类关联在一起,我们就可以直接通过外部类访问到对象的内部元素。

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

val instance = MyClass.create() // 访问到对象的内部元素

我们可以省略掉该对象的对象名,然后使用 Companion 替代需要声明的对象名:

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

val x = MyClass.Companion // 可以看做单例模式的另一个表现方式

注意:一个类里面只能声明一个内部关联对象,即关键字 companion 只能使用一次。

3.2 主要使用场景

首先,从伴生对象的声明和基本使用就可以看出,伴生对象可以用来表示一个单例,不过得把类的构造方法用private修饰一下。

而另一个主要的用法就是持有静态变量。相信聪明的你已经注意到了,Kotlin中没有static关键字,这让静态变量无法在Kotlin中快乐的玩耍了,不过伴生对象帮了我们一个忙。由于伴生对象在一个类中只存在一个,与静态变量的性质类似,这样我们给伴生对象添加的变量和方法就可以当做静态变量和静态方法使用。

1
2
3
4
5
6
7
8
9
class MyClass {
companion object {
var myVar = "var"
fun myFun() { }
}
}

val x = MyClass.Companion.myVar
MyClass.Companion.myFun()

3.3 注意伴生对象只是对象

虽然伴生对象看起来像单例,伴生对象的成员看起来像其他语言的静态成员,但在运行时他们仍然是真实对象的实例成员。例如还可以实现接口:

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

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

4. 对象表达式、对象声明、伴生对象之间的语义差异

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

  • 对象表达式是在使用他们的地方立即执行的

  • 对象声明是在第一次被访问到时延迟初始化的

  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配