在 Kotlin 中, 你可以使用对象, 只需一步就能定义一个类并创建它的一个实例. 当你需要重用一个单例(singleton instance), 或者一个一次性的对象时, 这个功能会非常有用. 为了处理这样的场景, Kotlin 提供了两种方案: 对象声明 用于创建单例, 以及 对象表达式 用于创建匿名的, 一次性对象.
对象声明(Object declaration) 在 Kotlin 中, 你可以使用对象声明创建对象的单个实例, 对象声明由 object
关键字加上对象名称构成. 只需一步就能定义一个类并创建它的一个实例, 非常便于实现单例:
//sampleStart
// 声明一个单例对象, 用来管理 DataProvider
object DataProviderManager {
private val providers = mutableListOf<DataProvider>()
// 注册一个新的 DataProvider
fun registerDataProvider(provider: DataProvider) {
providers.add(provider)
}
// 获取所有注册的 DataProvider
val allDataProviders: Collection<DataProvider>
get() = providers
}
//sampleEnd
// 示例 DataProvider 的接口
interface DataProvider {
fun provideData(): String
}
// 示例 DataProvider 的实现
class ExampleDataProvider : DataProvider {
override fun provideData(): String {
return "Example data"
}
}
fun main() {
// 创建 ExampleDataProvider 的一个实例
val exampleProvider = ExampleDataProvider()
// 要引用 `object`, 直接使用它的名称
DataProviderManager.registerDataProvider(exampleProvider)
// 获取所有注册的 DataProvider, 并打印输出
println(DataProviderManager.allDataProviders.map { it.provideData() })
// 输出结果为: [Example data]
}
对象声明中的初始化处理是线程安全的(thread-safe), 而且会在对象初次访问时完成初始化处理.
要引用这个 object
, 请直接使用它的名称:
DataProviderManager.registerDataProvider(exampleProvider)
对象声明也可以指定基类, 与 匿名对象从既有的类继承, 或实现接口 的方式类似:
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
}
与变量声明一样, 对象声明不是表达式, 因此不能用在赋值语句的右侧:
// 语法错误: 对象表达式不能指定名称.
val myObject = object MySingleton {
val name = "Singleton"
}
对象声明不可以是局部的, 也就是说, 不可以直接嵌套在函数之内. 但是, 可以嵌套在另一个对象声明之内, 或者嵌套在另一个非内部类(non-inner class)之内.
数据对象 如果在 Kotlin 中打印一个普通的对象声明, 它的字符串表达包含对象的名称和 hash 值:
object MyObject
fun main() {
println(MyObject)
// 输出结果为: MyObject@hashcode
}
但是, 如果使用 data
修饰符标记对象表达式, 你可以让编译器在调用 toString()
时返回对象真正的名称, 与 数据类 的工作方式一样:
data object MyDataObject {
val number: Int = 3
}
fun main() {
println(MyDataObject)
// 输出结果为: MyDataObject
}
此外, 编译器还会为你的 data object
生成一些函数:
data object
的 equals()
函数会保证你的 data object
的所有对象都被看作相等. 大多数情况下, 你的 data object
在运行期只会存在单个实例, 因为 data object
声明的就是一个单例(singleton). 但是, 在某些特殊情况下, 也可以在运行期生成相同类型的其他对象 (例如, 通过 java.lang.reflect
使用平台的反射功能, 或通过底层使用了这个 API 的 JVM 序列化库), 这个功能可以确保这些对象被当作相等.
请确保只对 data objects
进行结构化的相等比较 (使用 ==
操作符), 而不要进行引用相等比较 (使用 ===
操作符). 如果数据对象在运行期有一个以上的实例存在, 这样可以帮助你避免错误.
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
的所有运行期实例都拥有相同的 hash 值.
数据对象与数据类的不同 尽管 data object
和 data class
声明经常一起使用, 而且很相似, 但对于 data object
有一些函数没有生成:
没有 copy()
函数. 因为 data object
声明通常用作单例, 因此不会生成 copy()
函数. 单例限制一个类只有单个实例, 如果允许创建实例的拷贝, 就破坏了只存在单个实例的原则.
没有 componentN()
函数. 与 data class
不同, data object
没有任何数据属性. 对这种没有数据属性的对象进行解构是没有意义的, 因此不会生成 componentN()
函数.
在封闭层级结构(Sealed Hierarchy)中使用数据对象 数据对象声明非常适合在封闭层级结构(Sealed Hierarchy) 中使用, 例如 封闭类或封闭接口 . 这样的方式允许你声明数据类和数据对象, 并保持对称性.
在这个示例中, 将 EndOfFile
声明为 data object
, 而不是普通的 object
, 代表它自动拥有 toString()
函数, 不需要手动的覆盖这个函数:
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 Object) 同伴对象(Companion Object) 可以用来定义类级的函数和属性. 因此可以很容易的创建工厂方法, 声明常数, 访问共用的工具函数.
一个类内部的对象声明, 可以使用 companion
关键字标记为同伴对象:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
访问 companion object
的成员时, 可以直接使用类名称作为限定符:
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
:
class User(val name: String) {
// 定义一个同伴对象, 不指定名称
companion object { }
}
// 访问同伴对象
val companionUser = User.Companion
类的成员可以访问对应的 companion object
的 private
成员:
class User(val name: String) {
companion object {
private val defaultGreeting = "Hello"
}
fun sayHi() {
println(defaultGreeting)
}
}
User("Nick").sayHi()
// 输出结果为: Hello
直接使用一个类的名称时, 表示对这个类的同伴对象的引用, 无论同伴对象有没有名称:
//sampleStart
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
//sampleEnd
fun main() {
// 对 User1 的同伴对象调用 show() 函数
println(reference1.show())
// 输出结果为: User1's Named Companion Object
// 对 User2 的同伴对象调用 show() 函数
println(reference2.show())
// 输出结果为: User2's Companion Object
}
尽管 Kotlin 中的同伴对象的成员看起来很像其他语言中的类的静态成员(static member), 但它们实际上是同伴对象的实例成员, 也就是说它们属于对象自身, 因此同伴对象可以实现接口:
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
注解, 你可以让同伴对象的成员被编译为真正的静态方法(static method)和静态域(static field). 详情请参见 与 Java 的互操作性 .
对象表达式(Object expression) 对象表达式(object expression) 会声明一个类, 并为这个类创建一个实例, 但类和实例都没有名称. 这些类适合一次性使用. 这种类可以从头开始创建, 也可以从既有的类继承, 或者实现接口. 这些类的实例称为 匿名对象 , 因为它们通过表达式来定义, 而不是通过名称.
从头创建匿名对象 对象表达式以 object
关键字起始.
如果对象不继承任何类也不实现任何接口, 你可以直接在 object
关键字之后的大括号内定义对象的成员:
fun main() {
//sampleStart
val helloWorld = object {
val hello = "Hello"
val world = "World"
// 对象表达式继承 Any 类型, 已经有了 toString() 函数,
// 因此必须覆盖这个函数
override fun toString() = "$hello $world"
}
print(helloWorld)
// 输出结果为: Hello World
//sampleEnd
}
从基类继承匿名对象 要创建一个继承自某个类(或多个类)的匿名对象, 需要在 object
关键字和冒号 :
之后指定基类. 然后实现或覆盖基类的成员, 就和你在 继承 这个基类时一样:
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*...*/ }
override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
如果某个基类有构造器, 那么必须向构造器传递适当的参数. 要指定多个基类, 可以用逗号分隔, 放在冒号之后:
//sampleStart
// 创建一个 open 类 BankAccount, 包含 balance 属性
open class BankAccount(initialBalance: Int) {
open val balance: Int = initialBalance
}
// 定义一个接口 Transaction, 包含 execute() 函数
interface Transaction {
fun execute()
}
// 这个函数对一个 BankAccount 执行特殊交易
fun specialTransaction(account: BankAccount) {
// 创建一个匿名对象, 继承 BankAccount 类, 并实现 Transaction 接口
// 指定的 account 的 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()
}
//sampleEnd
fun main() {
// 创建一个 BankAccount, 初始的 balance 值为 1000
val myAccount = BankAccount(1000)
// 对创建的 account 执行特殊交易
specialTransaction(myAccount)
// 输出结果为: Executing special transaction. New balance is 1500.
}
将匿名对象用作返回类型或值类型 当你从一个局部的, 或 private
的函数或属性 (但不是 inline 函数 ), 返回一个匿名对象, 那么通过这个函数或属性可以访问匿名对象的所有成员:
//sampleStart
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}")
}
}
//sampleEnd
fun main() {
val userPreferences = UserPreferences()
userPreferences.printPreferences()
// 输出结果为: Theme: Dark, Font Size: 14
}
因此你可以返回一个包含特定属性的匿名对象, 提供一种简单的方式来封装数据或行为, 而不必创建一个单独的类.
如果返回匿名对象的函数或属性是 public
的, 或 private
的, 那么它的真实类型为:
如果匿名对象没有声明基类型, 则类型为 Any
.
如果匿名对象声明了唯一一个基类型, 则类型为这个基类型.
如果匿名对象声明了多个基类型, 则需要为这个函数或属性明确声明类型.
在这些情况中, 通过这个函数或属性的返回值, 对于匿名对象新添加的成员, 不可访问. 对于匿名对象覆盖的成员, 如果定义在这个函数或属性的真实类型中, 则可以访问. 例如:
//sampleStart
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"
}
}
//sampleEnd
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()
}
通过匿名对象访问变量 对象表达式 body 部之内的代码, 可以访问创建这个对象的代码范围内的变量:
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
// MouseAdapter provides default implementations for mouse event functions
// Simulates MouseAdapter handling mouse events
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// 在对象表达式内部, 可以访问 clickCount 和 enterCount 变量
}