Skip to content

对象 (Objects)

源:Object expressions and declarations

在 Kotlin 中,object 关键字是一个多面手。根据使用场景的不同,它可以分别扮演:

  • 对象声明:实现单例模式。
  • 伴生对象:替代 Java 的静态成员。
  • 对象表达式:替代 Java 的匿名内部类。

对象声明:天生的单例 (Object Declaration)

Kotlin 提供了一种极简的定义单例的方式:直接定义一个 object

语法结构

kotlin
object SingletonName [: SuperType] {
    // 成员变量与函数
}
kotlin
object NetworkManager {
    private const val TIMEOUT = 3000
    
    fun connect() { /*...*/ }
}

核心特性

  • 线程安全:它的初始化是线程安全的,由 JVM 的类加载机制保证(类似于 Java 的饿汉式或静态内部类单例)。
  • 懒加载:只有在第一次被访问时(访问属性或方法)才会初始化。
  • 不可有构造函数:因为它是单例,无需外部创建。
底层原理:字节码实现与单例安全

当定义一个 object 时,Kotlin 编译器会生成一个私有构造函数和一个名为 INSTANCE 的静态字段。

生成的 JVM 伪代码:

java
public final class NetworkManager {
   public static final NetworkManager INSTANCE;

   private NetworkManager() { }

   static {
      NetworkManager var0 = new NetworkManager();
      INSTANCE = var0;
   }
}

序列化安全:普通的 Java 单例在反序列化时会创建新实例,除非手动实现 readResolve()。Kotlin 的 object 在编译后会自动处理反序列化逻辑,确保 readResolve() 指向 INSTANCE,从而维持全局唯一性。

data object Kotlin 1.9+

对于纯粹作为标识符的单例(如密封类的子类),建议加上 data 关键字,以获得更友好的 toString() 输出。

kotlin
object NormalObj // toString() -> "NormalObj@7b23d"
data object DataObj // toString() -> "DataObj"

伴生对象 (Companion Objects)

Kotlin 类没有 static 关键字。如果你需要定义类级别的属性或方法,应使用伴生对象。

语法结构

kotlin
class ClassName {
    companion object [CompanionName] [: SuperType] {
        // 类级别成员
    }
}
kotlin
class User {
    companion object Factory {
        fun create(): User = User()
    }
}

// 调用时看起来像静态方法
val u = User.create()

与 Java static 的区别

虽然调用语法像静态方法,但在运行时,伴生对象是真正对象的实例成员

kotlin
// 它可以实现接口
companion object : Factory<User> { ... }

@JvmStatic

为了方便 Java 调用,或者为了性能优化(避免生成额外的伴生对象实例访问指令),可以使用 @JvmStatic 注解将伴生对象的成员编译为真正的 JVM 静态方法。

底层原理:伴生对象的调用开销

伴生对象在字节码中表现为外部类的一个静态内部类,名为 Companion。每次通过 ClassName.method() 调用时,实际上是执行了 ClassName.Companion.method()。 使用 @JvmStatic 可以在外部类生成一个同名的静态方法,直接转发调用,从而在 Java 互操作时减少一层方法跳转开销,并符合 Java 静态方法的语义。

对象表达式 (Object Expressions)

当需要创建一个“稍微修改现有类”或“实现某个接口”的临时对象时,使用对象表达式。这对应于 Java 的匿名内部类

语法结构

kotlin
val instance = object [: SuperType1, SuperInterface2] {
    // 成员实现
}
kotlin
val listener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }
    override fun mouseEntered(e: MouseEvent) { ... }
}

相比 Java 匿名类的增强

  • 多继承:可以同时继承一个类并实现多个接口。
  • 访问闭包:可以访问并修改外部非 final 变量。
kotlin
var clickCount = 0
button.setOnClickListener(object : OnClickListener {
    override fun onClick() {
        clickCount++ // Java 匿名类只能访问 final 变量
    }
})

私有 vs 公有类型的行为差异 进阶

  • 私有成员:如果对象表达式赋值给一个 private 属性,编译器会保留这个匿名对象的具体类型。你可以访问它的自定义成员。
  • 公有成员:如果赋值给 public 属性,编译器会将其类型擦除为父类(如果没有父类则为 Any)。
kotlin
class MyClass {
    // 私有:保留类型
    private val myPrivateObj = object {
        val x: Int = 10
    }

    // 公有:类型擦除为 Any
    val myPublicObj = object {
        val x: Int = 10
    }

    fun test() {
        val a = myPrivateObj.x // ✅ 正常访问
        // val b = myPublicObj.x // ❌ 编译错误:Unresolved reference 'x'
    }
}
底层原理:作为返回值的匿名类型

从 Kotlin 1.9 开始,局部或私有作用域内的匿名对象类型会被保留。如果该对象作为内联函数的返回值,其具体属性也可以被调用方识别。但在非内联的公有函数中,为了保证 ABI 稳定性,返回类型会被强制提升为该对象显式声明的父类或 Any

总结

  • 单例:用 object 实现,不仅代码少,而且天然线程安全。
  • 静态:用 companion object 替代,配合 @JvmStatic 优化互操作。
  • 匿名类:用 object : Type 表达式,支持访问外部可变变量。
  • data object:让单例日志更清晰。