12、对象与伴生对象:object声明、伴生对象(companion object)、对象表达式、单例模式

说实话,Kotlin 里关于「对象」的这几个概念,刚接触时很容易搞混。我自己刚转 Kotlin 那会儿,就经常在 objectcompanion object 之间犹豫——到底该用哪个?

这一章,咱们就把它们彻底理清楚。你想想看,搞懂了这些,写单例、写工具类、写回调,都会顺手很多。

Kotlin 对象体系 object 关键字 对象声明(单例) object Singleton { ... } 伴生对象 companion object { ... } 对象表达式 object : Interface { ... } ✅ 线程安全 ✅ 延迟初始化 ✅ 全局唯一实例 ✅ 静态成员替代 ✅ 可命名/实现接口 ✅ 类内部声明 ✅ 匿名内部类 ✅ 可访问局部变量 ✅ 一次性使用 核心:object 关键字 = 声明 + 创建实例

12.1 object 声明:天生单例

在 Kotlin 里,用 object 关键字声明一个类,同时就创建了它的唯一实例。说白了,这就是语言级别的单例模式。

object NetworkConfig {
    val baseUrl = "https://api.example.com"
    val timeout = 5000L

    fun printConfig() {
        println("BaseUrl: $baseUrl, Timeout: $timeout")
    }
}

// 使用方式
fun main() {
    NetworkConfig.printConfig()
    println(NetworkConfig.baseUrl)
}

你看,不需要 getInstance(),不需要 synchronized。Kotlin 编译器帮我们搞定了线程安全和延迟初始化。我个人习惯用这种方式来管理全局配置,比 Java 的饿汉式、懒汉式都清爽得多。

核心要点:object 声明是线程安全的单例,在第一次访问时初始化。

12.2 伴生对象:Kotlin 的「静态成员」

Java 有 static,Kotlin 没有。那静态方法、静态属性怎么办?用 companion object

class User(val name: String) {
    companion object {
        const val TYPE_ADMIN = "admin"
        const val TYPE_USER = "user"

        fun createAdmin(): User {
            return User("Administrator")
        }

        fun fromJson(json: String): User {
            // 解析逻辑...
            return User("ParsedUser")
        }
    }
}

// 调用方式
fun main() {
    val admin = User.createAdmin()
    println(User.TYPE_ADMIN)
}

伴生对象本质上是类内部的一个单例对象。你可以给它起名字,也可以实现接口:

interface JsonParser<T> {
    fun fromJson(json: String): T
}

class Product(val id: Int) {
    companion object Parser : JsonParser<Product> {
        override fun fromJson(json: String): Product {
            return Product(1001)
        }
    }
}

// 调用
fun main() {
    val product = Product.fromJson("{}")
    // 也可以直接用伴生对象名
    val p2 = Product.Parser.fromJson("{}")
}

我的经验:伴生对象里的 const val 会被编译为真正的静态常量,性能更好。如果只是普通 val,它属于伴生对象实例,调用时多一层对象访问。

12.3 对象表达式:匿名内部类的进化版

Java 里写匿名内部类,啰嗦得很。Kotlin 的对象表达式让这件事变得优雅:

interface ClickListener {
    fun onClick(view: String)
}

class Button {
    var listener: ClickListener? = null

    fun click() {
        listener?.onClick("button_1")
    }
}

fun main() {
    val button = Button()
    button.listener = object : ClickListener {
        override fun onClick(view: String) {
            println("$view was clicked")
        }
    }
    button.click()
}

对象表达式不只是简化了语法。它还能访问局部变量,而且没有 Java 的「变量必须是 final」的限制:

fun countClicks() {
    var count = 0
    val button = Button()
    button.listener = object : ClickListener {
        override fun onClick(view: String) {
            count++  // 直接修改局部变量
            println("Clicked $count times")
        }
    }
    button.click()
    button.click()
}

注意:对象表达式每次执行都会创建新实例。如果你在循环里用,要小心内存开销。我曾在 RecyclerView 的 Adapter 里踩过这个坑——每次绑定都 new 一个 listener,导致内存抖动。

12.4 单例模式:不止一种写法

Kotlin 里实现单例,其实有好几种姿势。咱们对比一下:

方式 代码 特点
object 声明 object Singleton 最简单,线程安全,延迟加载
伴生对象持有 class App { companion object { val instance = App() } } 类似 Java 饿汉式,类加载即初始化
懒加载委托 val instance by lazy { App() } 可控制初始化时机,线程安全
带参数单例 object SingletonWithParam { lateinit var config: Config } 需要手动初始化,灵活性高

我个人最常用的是 object 声明。但如果你需要传参初始化,可以用懒加载委托的方式:

class Database private constructor(val name: String) {
    companion object {
        @Volatile
        private var instance: Database? = null

        fun getInstance(name: String): Database {
            return instance ?: synchronized(this) {
                instance ?: Database(name).also { instance = it }
            }
        }
    }
}

// 或者更简洁的 by lazy 方式
class ConfigManager private constructor(val env: String) {
    companion object {
        val instance by lazy { ConfigManager("production") }
    }
}

避坑指南:我曾经在 Android 项目里用 object 声明了一个网络请求单例,结果在单元测试时发现实例无法重置。后来改用依赖注入框架才解决。所以,如果你的单例需要被测试 mock,建议用接口 + 注入的方式,而不是硬编码 object。

12.5 实战:一个完整的工具类设计

咱们把今天学的串起来,写一个日志工具类:

interface LogPrinter {
    fun log(tag: String, message: String)
}

object Logger {
    private var printer: LogPrinter? = null

    fun init(printer: LogPrinter) {
        this.printer = printer
    }

    fun d(tag: String, message: String) {
        printer?.log(tag, "[DEBUG] $message")
    }

    fun e(tag: String, message: String) {
        printer?.log(tag, "[ERROR] $message")
    }

    // 伴生对象里放常量
    companion object {
        const val TAG_UI = "UI"
        const val TAG_NETWORK = "Network"
        const val TAG_DB = "Database"
    }
}

// 使用
fun main() {
    Logger.init(object : LogPrinter {
        override fun log(tag: String, message: String) {
            println("$tag: $message")
        }
    })

    Logger.d(Logger.TAG_UI, "Button clicked")
    Logger.e(Logger.TAG_NETWORK, "Request failed")
}

你看,object 声明了全局唯一的 Logger,companion object 存放了日志标签常量,对象表达式 用来创建临时的 LogPrinter 实现。三个概念配合得恰到好处。

小结一下:

  • 需要全局唯一实例 → object 声明
  • 需要静态成员/工厂方法 → companion object
  • 需要一次性匿名实现 → 对象表达式

搞懂这三者的区别和适用场景,Kotlin 的对象体系你就拿捏了。


公众号:蓝海资料掘金营,微信deep3321