Kotlin 精讲 — companion object

0 阅读6分钟

处理图片.png

companion object 是一个和类绑定在一起的单例对象,你可以在其中定义属于类本身、而不是某个类实例的函数和属性。

它和 Java 中的 static 成员很像,但具有更高的灵活性和能力。

关键特性

中文翻译——伴生对象已经足够说明它的特点了。但我依然需要提醒下面几个特性,这样可以让屏幕前的开发者更清楚地把握 companion object 的作用:

  1. 它本身就是单例: companion object 只会创建一次,不需要先实例化外部类,就能直接访问它的成员。

  2. 可以访问私有成员: 它能够访问所属类中的私有字段和私有函数,因此非常适合实现工厂方法,或实现那些需要读取对象内部状态的工具逻辑。

  3. 它可以实现接口: 这点和 Java 的静态成员很不一样。companion object 由于本质上是真实对象,因此可以实现接口,从而具备更强的扩展性。

  4. 它可以命名: 默认情况下,companion object 是匿名的;但如果你愿意,也可以给它起名字。这通常能让代码表达更明确、可读性更好。

下面这个例子展示了如何利用 companion object 来定义工厂方法和常量:

class User private constructor(private val name: String) {

    fun greet() = "Hello, my name is $name"

    // You can explicitly name a companion object, like `Factory`.
    // In most cases, naming is optional. You can simply write
    // `companion object { ... }`.
    companion object Factory : Creator { // A companion object can implement interfaces
        private val createdUsers = mutableListOf<User>()

        // Acts as a Singleton: accessible without creating a User instance
        fun create(name: String): User {
            val user = User(name)
            createdUsers.add(user)
            return user
        }

        // Access to private members: can log internal state
        fun listAllUsers(): List<String> = createdUsers.map { it.name }

        override fun printFactoryInfo() {
            println("User factory created ${createdUsers.size} user(s).")
        }
    }
}

// Companion object implements an interface
interface Creator {
    fun printFactoryInfo()
}

fun main() {
    val user1 = User.create("RockByte")
    val user2 = User.create("加我同名公众号")

    println(user1.greet())       // Hello, my name is RockByte
    println(User.listAllUsers()) // [RockByte, 加我同名公众号]
    User.printFactoryInfo()      // User factory created 2 user(s).
}

除了常规的命名函数(如 create),在实现工厂方法时,Kotlin 还有一种更地道的高阶玩法——重载 invoke 操作符。这能让工厂方法的调用看起来就像是在调用构造函数一样:

class User private constructor(private val name: String) {
    companion object {
        // 重载 invoke 操作符
        operator fun invoke(name: String): User {
            return User(name)
        }
        
        // 使用默认的名称构建 User
        operator fun invoke(): User {
            return User("RockByte")
        }
    }
}

// 用法:看起来像是在调用构造函数,实际是在调用伴生对象的工厂方法
val user = User("Android")
val defaultUser = User()

这种技巧可能不太常见,但是如果你尝试使用一下,你会发现它非常方便。如果你不想通过暴露过多重载的构造函数来构造实例,那么可以通过这种方法来构建足够多的 invoke 操作符来模拟重载构造函数。

它巧妙地向调用端屏蔽了“这是工厂方法还是构造函数”的区别,未来即使要替换底层创建逻辑,也不需要改动任何调用方的代码。

同时,还有另一个很实用的思路:你可以给现有类型的 Companion 扩展属性,从而进一步发挥 companion object 的能力:

val String.Companion.Empty: String
    get() = ""

// Usage
// instead of User.createUser(name = "")
val fakeUser = User.createUser(name = String.Empty)

与 Java 的互操作

当 Java 调用 Kotlin 代码时,companion object 的运行方式会表现得非常明显。如果不加任何特殊注解,Java 侧必须先通过 Companion 实例,才能访问其中的成员。

class Logger {
    companion object {
        fun logMessage(message: String) {
            println("Log: $message")
        }
    }
}

在 Java 中,调用方式会是这样:

// From Java, you must access the Companion instance.
Logger.Companion.logMessage("Hello from Java");

从 Java 开发者的视角来看,这种写法并不够自然,因为每次调用方法都得先显式经过 companion object。为了解决这个问题,Kotlin 提供了 @JvmStatic 注解。只要把它加在 companion object 内的方法或属性上,Kotlin 编译器就会在外围类的字节码中额外生成一个真正的静态成员。

class Logger {
    companion object {
        @JvmStatic
        fun logMessage(message: String) {
            println("Log: $message")
        }
    }
}

这样一来,Java 端的调用方式就自然多了:

// Now, you can call it like a normal static method from Java.
Logger.logMessage("Hello from Java");

这也让 Kotlin 和 Java 之间的互操作变得更加顺滑、直观,两边的开发者都更容易接受。

另外,@JvmStatic 主要解决的是方法调用的痛点。如果是为了暴露伴生对象中的静态属性/常量,我们还有另外两个好帮手:const val@JvmField

  • 如果你声明的是基本数据类型或 String 类型的常量,优先使用 const val。这会告诉编译器进行内联优化,并在外围类直接生成真正的 public static final 字段,Java 调用时性能最好。
  • 对于其他无法使用 const 的对象类型属性,加上 @JvmField 注解。它会消除 Kotlin 默认生成的 getter/setter,直接在 Java 侧暴露为一个公开的字段,比把 @JvmStatic 加在属性上更加直观高效。

什么时候用

如果某些成员在逻辑上属于某个类,但又不依赖某个具体实例,那么它们就很适合放进 companion object

最典型的场景,就是工厂方法。它可以为对象创建提供受控入口,尤其适合主构造函数被声明为 private 的情况。

除了对象创建之外,companion object 也非常适合存放类级常量和共享属性,把那些概念上属于“类本身”的值集中管理。

与此同时,它还很适合承载与该类强绑定的工具函数(例如内部校验逻辑),因为这些逻辑通常只和类相关的数据有关,却不需要某个具体实例。

不过需要特别注意:如果你的工具函数不需要访问类的私有属性,也不需要参与多态(实现接口),那么最佳实践其实是完全不使用 companion object,而是直接声明为顶层函数 (Top-level Functions)(直接写在同一文件内或专属工具文件中)。

因为 companion object 即使不包含状态,也会在内存中额外创建一个单例对象;而顶层函数会被编译器直接编译为类似 Java 的 public static 方法,性能开销为零,是更加轻量、原生的工具函数承载方式。

最后,由于 companion object 本质上是一个真正的对象,它还能实现接口。这让一些更优雅的设计模式成为可能,比如让类本身以类型安全的方式扮演工厂或提供者的角色。

好处

使用 companion object 的最大价值,在于改善代码的组织方式和整体设计。

它把所有类级别的逻辑集中到一个清晰、专门的作用域里,从而增强封装性,也提升代码的可读性和可维护性。

配合私有构造函数时,它还能很好地支撑工厂方法模式,保证类实例始终以有效、受控的状态被创建出来。

更重要的是,companion object 不是“伪静态”,而是真正的对象实例。所以它具备传统静态成员不具备的面向对象能力,比如实现接口、参与更灵活的设计模式。

同时,借助 @JvmStatic@JvmField 等特性,它又能把成员以 Java 熟悉的静态形式暴露出去,因此在 Kotlin 与 Java 混合开发场景中尤其实用。

总结

companion object 是定义在类内部的一种特殊单例对象,用来承载在其他语言里通常会写成静态成员的内容,比如工厂方法和常量。它既保留了“可直接通过类名访问”的便利性,又因为自身是真实对象,而拥有实现接口、被传递和参与更复杂设计的能力。掌握它的惰性初始化特性、与顶层函数的取舍,以及与 Java 互操作的各种注解,是写出地道 Kotlin 代码的关键。