一杯 Kotlin 美式品味 object 声明

object 是 Kotlin 中一种专门用来创建单例实例的构造。

也就是说,一个 object 在程序里只会存在一个实例。借助这个特性,你可以在定义类的同时直接创建它的实例。

它非常适合用来编写工具类、保存全局常量,或者在不引入额外样板代码的前提下,以线程安全的方式管理共享资源。

关键特性

Kotlin 的 object 声明不仅能方便地创建单例,还自带一些非常实用的特性:

  1. 天然单例object 关键字会保证这个类型在应用生命周期内只会创建一个实例。
  2. 线程安全初始化:默认情况下,这个单例实例的初始化就是线程安全的,因此在多线程环境下也不会出现重复创建的问题。
  3. 不需要显式实例化 :不同于普通类,你不能显式创建一个 object 类型的实例。它会在第一次被访问时自动创建。
  4. 惰性初始化object 声明只有在第一次访问时才会初始化。

下面是一个 object 的简单示例:

kotlin 复制代码
object Logger {
    fun log(message: String) {
        println("Log: $message")
    }
}

fun main() {
    Logger.log("This is a singleton logger.")
    Logger.log("Logging another message.")
}

So easy, right?

在这个例子里,Logger 被定义成了一个单例对象,因此所有 log() 调用共享的都是同一个实例,而且完全不需要手动实例化。Kotlin 会自动负责 Logger 实例的创建与管理。

使用场景

当你需要一个全局可访问、并且只需要一个实例的对象时,object 声明就非常合适,例如:

  1. 工具类:比如日志、数据格式化、输入校验等通用能力。
  2. 全局常量:比如保存配置 key、固定设置等在全局范围内复用的值。
  3. 状态管理:比如维护共享资源、缓存数据或应用级配置。

与普通类的区别

虽然 objectclass 都可以用来定义类型,但它们解决的问题并不一样。理解两者的差异,能够帮助你在 Kotlin 中做出更加正确的选择。

  • 实例化方式object 会在第一次访问时由 Kotlin 运行时惰性创建,而且只创建一次。相对地,class 更像是一个蓝图;每当你需要一个新对象时,都必须通过构造函数(如 MyClass())显式创建。
  • 使用目的object 适合单实例场景,也就是单例;而普通类适合用来创建多个对象,这些对象还可以拥有彼此不同的状态。
  • 线程安全 :单例 object 的初始化由 Kotlin 运行时保证线程安全,因此在多线程环境里也只会生成一个实例。这倒不是说普通类不是线程安全的,但如果你想基于 class 手动实现单例模式,就必须自己处理线程安全初始化这类复杂问题,而 object 已经替你把这些细节处理好了。

Java 中写单例,一直是一个高频考点,下次面试官问你的话,你说你用 Kotlin,🤣

小结

object 把单例的定义与管理都做了内建支持,因此它特别适合共享资源、全局常量和工具函数。它既减少了样板代码,又通过默认的线程安全初始化提升了使用体验;而它的 companion object 形式,则进一步为普通类补上了类似静态成员的能力。

进阶:declaration 与 expression

object 关键字有一个很强大的能力:它允许你在一步之内既定义一个类,又创建这个类的实例。

当你需要一个可复用的单例对象,或者一个只用一次的匿名对象时,这种能力就非常方便。围绕这一点,Kotlin 提供了两种核心写法:object declaration 用来创建具名单例,object expression 用来创建匿名的一次性对象。

两者虽然都写作 object,但它们在用途、作用域以及初始化时机上有着本质差别。理解这些差别,才能在实际开发中把它们用对地方。

declaration:更简洁的单例模式

object declaration 会在一条语句中同时完成两件事:定义一个类,并创建它唯一的实例。

它总是有名字,而这个名字也就是访问该单例实例的全局入口。这正是 Kotlin 对单例模式给出的惯用且内建的实现方式。

kotlin 复制代码
// 声明一个单例对象,用来管理共享资源。
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) { /* ... */ }

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

// 直接通过名字访问唯一实例。
DataProviderManager.registerDataProvider(myProvider)

object declaration 的核心特点包括:

  • 单例实例 :编译器会保证 DataProviderManager 在整个应用生命周期中只存在一个实例。
  • 具名且全局可访问 :它拥有名称(DataProviderManager),因此可以从代码库中的任意位置访问。
  • 惰性初始化:实例只会在第一次访问时创建,这本身就是一种重要的性能优化;同时,这个初始化过程还是线程安全的(这里开发者要与单例中的饿汉式以及懒汉式做区别,这里惰性不是指单例的初始化方式)。
  • 没有构造函数 :你不能手动实例化一个 object declaration,因此它也不能拥有构造函数。
  • 不能定义为局部对象声明object declaration 必须出现在文件顶层,或者嵌套在另一个类 / 对象内部,不能直接声明在函数内部。

object declaration 非常适合用于管理共享状态、提供工具函数,或者充当中心注册表,DataProviderManager 就是一个典型例子。

此外,object declaration 还有一种特殊形式叫 companion object,而这个,我们下一篇文章会专门展开说明。

expression:匿名的一次性对象

与之相对,object expression 会创建一个匿名对象。它会在同一时间、同一位置定义一个类并实例化它,但不会给这个类命名。它特别适合那种只出现一次的接口实现,或者为了局部场景临时扩展一个类的需求。

kotlin 复制代码
// 经典的 Android 匿名对象示例。
view.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        // 这个对象只是为这个监听器而创建的。
    }
})

object expression 的典型特征包括:

  • 匿名:它定义出来的类没有名字,实例通常会赋给一个变量,或者直接作为参数传入。
  • 立即初始化 :不像 object declaration 那样惰性初始化,object expression 会在定义它的位置立刻创建实例。
  • 每次执行都会产生新对象object expression 并不是单例,每执行一次都会新建一个实例。
  • 可以出现在局部作用域中:它可以写在任何地方,包括函数内部。
  • 可以捕获外层作用域object expression 内部的代码可以访问外层作用域中的变量,也就是具备闭包能力。

因此,object expression 很适合用来实现事件监听器、简单接口实现,或者任何只需要一个临时对象、却不值得单独定义完整具名类的场景。

核心区别

可能看到这里,你已经有个大概了,大多数有 Java 经验的开发者应该能总结出这样一段话:object declaration 为单例服务,而 object expression 为匿名类服务。

那我就说的详细一点。

object declarationobject expression 的本质区别,主要体现在实例化方式、作用域以及使用目的上。object declaration 创建的是一个具名单例,采用惰性初始化,并且会在整个应用生命周期内持续存在,因此非常适合共享资源这类长期存在的对象。

object expression 每执行一次都会创建一个新的匿名实例。它会在定义处立即初始化,更适合那种局部、一次性的任务,比如临时实现一个事件监听器,因为它的生命周期只和这次使用本身相关。

总之,

在 Kotlin 中,object declarationobject expression 虽然都依赖 object 关键字,但它们面向的是完全不同的使用场景。

前者提供的是具名、惰性初始化、可全局共享的单例;后者提供的是匿名、立即创建、用于一次性任务的临时实例。

进阶:data object

data objectdataobject 两个关键字的结合体,于 Kotlin 1.9 引入,目的是让"既需要单例语义、又需要保存只读数据"的对象写起来更自然。它把 data class 和单例 object 的优势合并在了一起,让你能够表达"保存数据的唯一只读实例"。

data class 一样,data object 会基于声明的属性自动生成 toString()hashCode()equals() 等常用方法;但它同时又是一个 object,因此天然保证只存在一个实例,也就不需要再为相同状态创建多个对象。

关键特性

  1. 单例本质data object 只会创建一次,并在整个应用内共享。
  2. 自动生成方法 :它会像 data class 一样,根据属性自动生成 toString()hashCode()equals() 等方法。
  3. 不可变倾向:它的属性通常是只读的,因此很适合写出简洁、稳定且线程安全的设计。

下面是一个定义和使用 data object 的例子:

kotlin 复制代码
data object Configuration {
    val appName: String = "Dove Letter"
    val version: String = "1.0.0"
}

fun main() {
    println(Configuration) // 输出:Configuration(appName=Dove Letter, version=1.0.0)
}

在这个例子中,Configuration 是一个 data object 的单例实例。Kotlin 编译器会自动为它生成 toString(),从而给出属性内容的字符串表示。

使用场景

当你需要全局可访问的数据或行为,但又不希望承担额外实例化成本时,data object 尤其适合:

  1. 全局配置或常量:当你需要一个只读配置值的唯一真实来源时。
  2. 共享数据:当某份数据需要在整个应用中共享,而且本身是只读的、不需要多个实例时。

data class vs object vs data object

Kotlin 提供了多种表示数据的方式,它们各有适用场景:

  • data Class:需要显式实例化,并允许存在多个实例,适合表示拥有独立状态的数据模型。
  • object :适合表示单例,但不会自动生成 toString()equals() 等辅助方法。
  • data object :同时拥有 object 的单例语义与 data class 的自动生成方法,因此很适合表达单实例、只读的数据表示。

KEEP 提案

KEEP Data Objects 提案data object 做了一个很简洁的总结:

该 KEEP 引入了 data objects ,用来修复当前 Kotlin 设计中的若干不一致问题,包括如何通过 data classes 处理不可变数据与代数数据类型(ADTs),以及如何避免为 object 手动编写默认 toString 所带来的样板代码。作为效果之一,它还修复了 KT-4107 这个功能请求。

总之,

data object 让"保存不可变数据的单例对象"变得更容易书写,同时还能享受到 data class 提供的那些实用方法。它特别适合全局配置、常量,以及那些要求在应用中始终只有一个不可变实例的共享数据模型。

进阶:Java 字节码

先看一个简单的例子:我们定义一个用于日志配置的单例对象。

kotlin 复制代码
object LoggerConfig {
    val logLevel: String = "DEBUG"

    fun log(message: String) {
        println("[$logLevel]: $message")
    }

    init {
        println("LoggerConfig initialized.")
    }
}

在 Kotlin 中,你可以直接通过对象名访问它的成员,比如 LoggerConfig.log("App started")。其中的 init 代码块只会在第一次访问 LoggerConfig 时执行一次。

当这段 Kotlin 代码被编译,再反编译回 Java 后,就能看到一个经典且线程安全的 Java 单例实现。

java 复制代码
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

// 1. object 会变成一个 final Java 类。
public final class LoggerConfig {

   // 2. 会创建一个唯一的、static 的、final 的实例。这就是单例
   @NotNull
   public static final LoggerConfig INSTANCE = new LoggerConfig();
   @NotNull
   private static final String logLevel = "DEBUG";
   public static final int $stable;
    
   // 3. 构造函数会被设为 private,以阻止外部实例化。
   private LoggerConfig() {
   }

   @NotNull
   public final String getLogLevel() {
      return logLevel;
   }

   public final void log(@NotNull String message) {
      Intrinsics.checkNotNullParameter(message, "message");
      System.out.println('[' + logLevel + "]: " + message);
   }
   
   // 4. init 代码块会被搬进 static 初始化块中。
   static {
      System.out.println("LoggerConfig initialized.");
   }
}

需要说明的是,不同的 Kotlin 版本,不同的 Kotlin 编写方式,object 的初始化逻辑有点区别。当前展示的是 Kotlin 2.2.10 版本在 JVM 21 下的反编译代码。

编译器会通过几项关键策略,把 object 关键字变成一个可靠的单例实现:

  1. 变成 final 类object 会被编译成一个标准的 Java 类,并且带有 final 修饰。这能阻止其他类继承它,而这正是单例实现中常见的做法之一,用来避免子类化带来的问题。
  2. 生成公共的 INSTANCE 字段 :单例模式的核心,在于创建一个名为 INSTANCEpublic static final 字段。这个字段持有该类唯一的实例。当你在 Kotlin 中写下 LoggerConfig.log(...) 时,编译器实际上会把它翻译成字节码里的 LoggerConfig.INSTANCE.log(...)。也正是这个 INSTANCE 字段,让你可以像访问静态成员那样访问单例对象的成员。同时在此刻,也完成了初始化。
  3. 生成私有构造函数 :编译器会生成一个 private constructor。这是保证单例约束的重要一步。构造函数私有化之后,外部代码就无法通过 new LoggerConfig() 再创建新的实例。获取该对象实例的唯一方式,就是通过公开的 INSTANCE 字段。
  4. 使用 static 初始化块 :Kotlin object 中的 init 代码块,以及属性的初始化逻辑,都会被移到 Java 的 static { ... } 初始化块里。这个代码块会由 JVM 在类第一次加载进内存时执行,而且只执行一次。

Kotlin 里的 companion object 也会遵循类似的编译模式。它会被编译成所属类中的一个静态嵌套类,并同样拥有一个 public static final INSTANCE 字段。Kotlin 正是借此在 JVM 上实现了类似静态成员的能力,而无需为成员提供 static 关键字。当你调用 MyClass.myCompanionFunction() 时,实际调用的是 MyClass.Companion.INSTANCE.myCompanionFunction()

相关推荐
常利兵2 小时前
Room 3.0大变身:安卓开发的新挑战与机遇
android·jvm·oracle
阿拉斯攀登2 小时前
【RK3576 安卓 JNI/NDK 系列 09】RK3576 实战(三):JNI 调用 librga 实现 2D 硬件加速图像处理
android·驱动开发·rk3568·瑞芯微·rk安卓驱动·rk3576 rga加速
落羽的落羽2 小时前
【Linux系统】信号机制拆解,透过内核三张表深入本质
android·java·linux·服务器·c++·spring·机器学习
峥嵘life2 小时前
Android16 EDLA【GTS】GtsPermissionTestCases存在fail项
android·学习
魑魅魍魉都是鬼2 小时前
Android:java kotlin 单例模式
android·java·单例模式
俩个逗号。。3 小时前
Kotlin 扩展函数详解
开发语言·kotlin
段娇娇11 小时前
Android jetpack LiveData(一)使用篇
android·android jetpack
XiaoLeisj11 小时前
Android Jetpack 页面架构实战:从 LiveData、ViewModel 到 DataBinding 的生命周期管理与数据绑定
android·java·架构·android jetpack·livedata·viewmodel·databinding
似水明俊德15 小时前
15-C#
android·开发语言·c#