5.8.7 对象声明与对象表达式

原文链接: https://kotlinlang.org/docs/object-declarations.html

5.8.7 对象声明与对象表达式

在 Kotlin 中,对象让你可以一步完成类的定义和实例的创建。当你需要一个可复用的单例实例,或一次性使用的对象时,这非常有用。为应对这些场景,Kotlin 提供了两种主要方式:用于创建单例的对象声明,以及用于创建匿名、一次性对象的对象表达式。

提示: 单例确保一个类只有一个实例,并提供访问它的全局入口。

对象声明和对象表达式最适合以下场景:

  • 为共享资源使用单例: 你需要确保整个应用中只存在一个类的实例。例如管理数据库连接池。
  • 创建工厂方法: 你需要一种便捷的方式来高效创建实例。伴生对象允许你定义与类绑定的类级函数和属性,从而简化这些实例的创建和管理。
  • 临时修改已有类的行为: 你想修改已有类的行为,而不必创建新的子类。例如为某个特定操作给对象添加临时功能。
  • 需要类型安全的设计: 你需要用对象表达式提供接口或抽象类的一次性实现。这在按钮点击处理器等场景中很有用。

对象声明

在 Kotlin 中,你可以用对象声明创建对象的单一实例,它总是在 object 关键字后跟一个名称。这让你可以一步完成类的定义和实例的创建,非常适合实现单例:

 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
// 声明一个单例对象来管理数据提供者
object DataProviderManager {
    private val providers = mutableListOf<DataProvider>()

    // 注册新的数据提供者
    fun registerDataProvider(provider: DataProvider) {
        providers.add(provider)
    }

    // 获取所有已注册的数据提供者
    val allDataProviders: Collection<DataProvider>
        get() = providers
}

// 示例数据提供者接口
interface DataProvider {
    fun provideData(): String
}

// 示例数据提供者实现
class ExampleDataProvider : DataProvider {
    override fun provideData(): String {
        return "Example data"
    }
}

fun main() {
    // 创建 ExampleDataProvider 的实例
    val exampleProvider = ExampleDataProvider()

    // 要引用该对象,直接使用它的名称
    DataProviderManager.registerDataProvider(exampleProvider)

    // 获取并打印所有数据提供者
    println(DataProviderManager.allDataProviders.map { it.provideData() })
    // [Example data]
}

提示: 对象声明的初始化是线程安全的,并且发生在首次访问时。

要引用这个 object,直接使用它的名称:

1
DataProviderManager.registerDataProvider(exampleProvider)

对象声明也可以有超类型,类似于匿名对象可以继承已有类或实现接口:

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

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

与变量声明一样,对象声明不是表达式,因此不能用在赋值语句的右侧:

1
2
3
4
// 语法错误:对象表达式不能绑定名称。
val myObject = object MySingleton {
    val name = "Singleton"
}

对象声明不能是局部的,也就是说不能直接嵌套在函数内部。不过,它们可以嵌套在其他对象声明或非 inner 类中。

数据对象

在 Kotlin 中打印普通对象声明时,字符串表示中会同时包含它的名称和该 object 的哈希值:

1
2
3
4
5
6
object MyObject

fun main() {
    println(MyObject)
    // MyObject@hashcode
}

不过,通过给对象声明加上 data 修饰符,你可以让编译器在调用 toString() 时返回对象的真实名称,其工作方式与数据类相同:

1
2
3
4
5
6
7
8
data object MyDataObject {
    val number: Int = 3
}

fun main() {
    println(MyDataObject)
    // MyDataObject
}

此外,编译器还会为你的 data object 生成几个函数:

  • toString() 返回数据对象的名称
  • equals()/hashCode() 支持相等性检查和基于哈希的集合

注意: 你不能为 data object 提供自定义的 equals 或 hashCode 实现。

data object 的 equals() 函数确保所有具有该 data object 类型的对象都被视为相等。大多数情况下,运行时你只会有一个 data object 实例,因为 data object 声明的是单例。不过在极端情况下,运行时可能会生成同类型的另一个对象(例如通过 java.lang.reflect 使用平台反射,或使用底层依赖该 API 的 JVM 序列化库),此时该函数能确保这些对象被视为相等。

警告: 请确保只对 data object 做结构比较(使用 == 运算符),绝不要做引用比较(使用 === 运算符)。这有助于你在运行时存在多个数据对象实例时避免陷阱。

 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
import java.lang.reflect.Constructor

data object MySingleton

fun main() {
    val evilTwin = createInstanceViaReflection()

    println(MySingleton)
    // MySingleton

    println(evilTwin)
    // MySingleton

    // 即使某个库强制创建了 MySingleton 的第二个实例,
    // 它的 equals() 函数仍返回 true:
    println(MySingleton == evilTwin)
    // true

    // 不要用 === 比较数据对象
    println(MySingleton === evilTwin)
    // false
}

fun createInstanceViaReflection(): MySingleton {
    // Kotlin 反射不允许实例化数据对象。
    // 这里"强行"创建了一个新的 MySingleton 实例(使用 Java 平台反射)
    // 请勿自行这样做!
    return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance()
}

生成的 hashCode() 函数的行为与 equals() 函数保持一致,因此 data object 的所有运行时实例都具有相同的哈希值。

数据对象与数据类的区别

虽然 data object 和 data class 声明经常一起使用、并且有一些相似之处,但有些函数不会为 data object 生成:

  • 没有 copy() 函数。因为 data object 声明旨在作为单例使用,所以不会生成 copy() 函数。单例限制一个类只能有一个实例,而允许创建实例副本会破坏这一点。
  • 没有 componentN() 函数。与 data class 不同,data object 没有任何数据属性。由于对没有数据属性的这类对象进行解构没有意义,因此不会生成 componentN() 函数。

在密封层级中使用数据对象

数据对象声明对密封类或密封接口这类密封层级特别有用。它们让你能与同时定义的数据类保持对称性。

在这个例子中,把 EndOfFile 声明为 data object 而不是普通 object,意味着它会拥有 toString() 函数,而无需手动重写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult

fun main() {
    println(Number(7))
    // Number(number=7)
    println(EndOfFile)
    // EndOfFile
}

伴生对象

伴生对象允许你定义类级函数和属性。这让创建工厂方法、保存常量和访问共享工具变得很容易。

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

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

companion object 的成员可以直接用类名作为限定符来调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class User(val name: String) {
    // 定义一个伴生对象,作为创建 User 实例的工厂
    companion object Factory {
        fun create(name: String): User = User(name)
    }
}

fun main(){
    // 用类名作为限定符调用伴生对象的工厂方法。
    // 创建一个新的 User 实例
    val userInstance = User.create("John Doe")
    println(userInstance.name)
    // John Doe
}

companion object 的名称可以省略,此时会使用名称 Companion:

1
2
3
4
5
6
7
class User(val name: String) {
    // 定义一个没有名称的伴生对象
    companion object { }
}

// 访问伴生对象
val companionUser = User.Companion

类成员可以访问其对应 companion object 的 private 成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class User(val name: String) {
    companion object {
        private val defaultGreeting = "Hello"
    }

    fun sayHi() {
        println(defaultGreeting)
    }
}
User("Nick").sayHi()
// Hello

当类名被单独使用时,它充当对该类伴生对象的引用,无论伴生对象是否有名称:

 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
class User1 {
    // 定义一个具名伴生对象
    companion object Named {
        fun show(): String = "User1's Named Companion Object"
    }
}

// 用类名引用 User1 的伴生对象
val reference1 = User1

class User2 {
    // 定义一个匿名伴生对象
    companion object {
        fun show(): String = "User2's Companion Object"
    }
}

// 用类名引用 User2 的伴生对象
val reference2 = User2

fun main() {
    // 调用 User1 伴生对象中的 show() 函数
    println(reference1.show())
    // User1's Named Companion Object

    // 调用 User2 伴生对象中的 show() 函数
    println(reference2.show())
    // User2's Companion Object
}

虽然 Kotlin 中伴生对象的成员看起来像其他语言中的静态成员,但它们实际上是伴生对象的实例成员,也就是说属于该对象本身。这使得伴生对象可以实现接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Factory<T> {
    fun create(name: String): T
}

class User(val name: String) {
    // 定义一个实现 Factory 接口的伴生对象
    companion object : Factory<User> {
        override fun create(name: String): User = User(name)
    }
}

fun main() {
    // 把伴生对象用作 Factory
    val userFactory: Factory<User> = User
    val newUser = userFactory.create("Example User")
    println(newUser.name)
    // Example User
}

不过在 JVM 上,如果使用 @JvmStatic 注解,可以让伴生对象的成员生成为真正的静态方法和字段。更多细节请参阅 Java 互操作一节。

对象表达式

对象表达式声明一个类并创建该类的实例,但两者都不命名。这类类适合一次性使用。它们可以从零创建、继承已有类或实现接口。这类类的实例也称为匿名对象,因为它们是由表达式而非名称定义的。

从零创建匿名对象

对象表达式以 object 关键字开头。

如果该对象不继承任何类、也不实现任何接口,可以直接在 object 关键字之后的花括号内定义对象的成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main() {
    val helloWorld = object {
        val hello = "Hello"
        val world = "World"
        // 对象表达式继承自 Any 类,而它已经有 toString() 函数,
        // 因此必须重写
        override fun toString() = "$hello $world"
    }

    print(helloWorld)
    // Hello World
}

让匿名对象继承超类型

要创建继承某个(或多个)类型的匿名对象,请在 object 和冒号 : 之后指定该类型。然后像继承一样实现或重写该类的成员:

1
2
3
4
5
window.addMouseListener(object : MouseAdapter() {
    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
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 创建一个带 balance 属性的开放类 BankAccount
open class BankAccount(initialBalance: Int) {
    open val balance: Int = initialBalance
}

// 定义一个带 execute() 函数的接口 Transaction
interface Transaction {
    fun execute()
}

// 一个对 BankAccount 执行特殊交易的函数
fun specialTransaction(account: BankAccount) {
    // 创建一个继承 BankAccount 类并实现 Transaction 接口的匿名对象
    // 所提供账号的 balance 会传给 BankAccount 超类的构造函数
    val temporaryAccount = object : BankAccount(account.balance), Transaction {

        override val balance = account.balance + 500 // 临时奖励

        // 实现 Transaction 接口中的 execute() 函数
        override fun execute() {
            println("Executing special transaction. New balance is $balance.")
        }
    }
    // 执行该交易
    temporaryAccount.execute()
}
fun main() {
    // 创建一个初始余额为 1000 的 BankAccount
    val myAccount = BankAccount(1000)
    // 对创建的账号执行一次特殊交易
    specialTransaction(myAccount)
    // Executing special transaction. New balance is 1500.
}

把匿名对象用作返回类型和值类型

当你从局部函数、private 函数或属性返回匿名对象时,该匿名对象的所有成员都可以通过该函数或属性访问:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class UserPreferences {
    private fun getPreferences() = object {
        val theme: String = "Dark"
        val fontSize: Int = 14
    }

    fun printPreferences() {
        val preferences = getPreferences()
        println("Theme: ${preferences.theme}, Font Size: ${preferences.fontSize}")
    }
}

fun main() {
    val userPreferences = UserPreferences()
    userPreferences.printPreferences()
    // Theme: Dark, Font Size: 14
}

这样你就可以返回带有特定属性的匿名对象,提供了一种无需创建单独类就能封装数据或行为的简单方式。

如果返回匿名对象的函数或属性具有 public、protected 或 internal 可见性,其实际类型是:

  • 如果该匿名对象没有声明超类型,则为 Any。
  • 如果只有一个这样的类型,则为该匿名对象声明的超类型。
  • 如果有多个声明的超类型,则为显式声明的类型。

在所有这些情况下,匿名对象中新增的成员都不可访问。如果被重写的成员声明在该函数或属性的实际类型中,则可以访问。例如:

 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
42
43
44
45
46
47
48
49
50
51
interface Notification {
    // 在 Notification 接口中声明 notifyUser()
    fun notifyUser()
}

interface DetailedNotification

class NotificationManager {
    // 返回类型是 Any。message 属性不可访问。
    // 当返回类型是 Any 时,只有 Any 类的成员可以访问。
    fun getNotification() = object {
        val message: String = "General notification"
    }

    // 返回类型是 Notification,因为该匿名对象只实现了一个接口
    // notifyUser() 函数可以访问,因为它属于 Notification 接口
    // message 属性不可访问,因为它没有在 Notification 接口中声明
    fun getEmailNotification() = object : Notification {
        override fun notifyUser() {
            println("Sending email notification")
        }
        val message: String = "You've got mail!"
    }

    // 返回类型是 DetailedNotification。notifyUser() 函数和 message 属性都不可访问
    // 只有 DetailedNotification 接口中声明的成员可以访问
    fun getDetailedNotification(): DetailedNotification = object : Notification, DetailedNotification {
        override fun notifyUser() {
            println("Sending detailed notification")
        }
        val message: String = "Detailed message content"
    }
}
fun main() {
    // 这里不会产生任何输出
    val notificationManager = NotificationManager()

    // 这里无法访问 message 属性,因为返回类型是 Any
    // 这里不会产生任何输出
    val notification = notificationManager.getNotification()

    // notifyUser() 函数可以访问
    // 这里无法访问 message 属性,因为返回类型是 Notification
    val emailNotification = notificationManager.getEmailNotification()
    emailNotification.notifyUser()
    // Sending email notification

    // 这里无法访问 notifyUser() 函数和 message 属性,因为返回类型是 DetailedNotification
    // 这里不会产生任何输出
    val detailedNotification = notificationManager.getDetailedNotification()
}

从匿名对象访问变量

对象表达式主体中的代码可以访问外层作用域中的变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    // MouseAdapter 为鼠标事件函数提供了默认实现
    // 模拟 MouseAdapter 处理鼠标事件
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // clickCount 和 enterCount 变量在对象表达式内部可以访问
}

对象声明与对象表达式在行为上的差异

对象声明与对象表达式在初始化行为上存在差异:

  • 对象表达式在使用处立即执行(并初始化)。
  • 对象声明在首次访问时惰性初始化。
  • 伴生对象在对应的类被加载(解析)时初始化,这与 Java 静态初始化器的语义一致。