13.Kotlin 对象:Object 关键字:单例、伴生对象与对象表达式

88 阅读6分钟

希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 —— Android_小雨

整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系

一、前言

1.1 Object 关键字的核心价值

在 Java 开发中,"类"和"对象"是两个分离的概念。我们需要先定义类,再通过 new 创建对象。

  • 要实现单例,我们需要写双重检查锁(DCL)或静态内部类。
  • 要实现静态方法,我们需要 static 关键字。
  • 要实现回调,我们需要写匿名内部类。

Kotlin 的 object 关键字打破了这种繁琐,它提出了一个核心概念:在定义类的同时,立即创建一个实例。 它的核心价值在于:

  1. 简化单例:一行代码实现线程安全的懒加载单例。
  2. 替代匿名内部类:支持多继承、可修改闭包变量。
  3. 关联类级逻辑:通过伴生对象替代 Java 的静态成员。

1.2 Kotlin Object 的设计优势

  • 语法简洁:去除了 Java 中冗余的样板代码。
  • 线程安全:编译器利用 JVM 类加载机制自动保证初始化的线程安全性。
  • 语义清晰:不同的用法对应不同的设计意图(全局唯一 vs 类级共享 vs 临时实现)。

1.3 本文核心内容

本文将深入剖析 Kotlin object 的三大核心场景:

  1. Object 单例:全局唯一的对象。
  2. 伴生对象 (Companion Object):寄生于类内部的“静态”成员。
  3. 对象表达式 (Object Expression):随用随丢的匿名对象。

二、Object 单例:极简单例模式实现

2.1 基本语法

在 Kotlin 中,只需要将 class 关键字替换为 object,就定义了一个单例(Singleton)。你不需要定义构造函数(因为它是私有的),也不需要手动创建实例。

2.2 核心特性

  • 懒加载 (Lazy Loading)object 类在第一次被访问时才会初始化。
  • 线程安全:虚拟机的类加载机制天然保证了线程安全。
  • 全局唯一:在整个应用生命周期中,内存中只有一份实例。

2.3 & 2.4 完整运行示例:游戏配置管理器

这个例子展示了单例的状态共享特性。无论你在哪里访问它,修改的都是同一个数据。适用于全局配置管理、工具类等场景。

// --- 复制以下代码到 IDE 即可运行 ---

// 1. 定义单例
object GameConfig {
    // 单例类的属性(状态)
    var volume: Int = 50
    var resolution: String = "1080p"

    // 初始化块(类似 Java 静态代码块,仅执行一次)
    init {
        println(">>> [系统] 游戏配置模块初始化...")
    }

    // 单例类的方法
    fun printConfig() {
        println("当前配置 -> 音量: $volume, 分辨率: $resolution")
    }
}

fun main() {
    println("--- 游戏启动 ---")

    // 第一次访问:触发 init 代码块
    GameConfig.printConfig()

    // 修改状态
    println("--- 玩家修改了设置 ---")
    GameConfig.volume = 80
    GameConfig.resolution = "4K"

    // 再次访问:使用的是同一个实例,状态已被保留
    GameConfig.printConfig()

    // 验证单例唯一性
    val reference1 = GameConfig
    val reference2 = GameConfig
    println("两个引用是否指向同一对象: ${reference1 === reference2}")
}

预期输出结果

--- 游戏启动 ---
>>> [系统] 游戏配置模块初始化...
当前配置 -> 音量: 50, 分辨率: 1080p
--- 玩家修改了设置 ---
当前配置 -> 音量: 80, 分辨率: 4K
两个引用是否指向同一对象: true


三、伴生对象(Companion Object):类级别的 "静态" 成员

3.1 基本语法

Kotlin 移除了 static 关键字。如果你需要定义“属于类而不是属于实例”的成员(例如常量、工厂方法),需要使用 伴生对象。在类内部使用 companion object 定义。

3.2 核心特性

  • 寄生性:伴生对象与外部类一一对应,一个类只能有一个伴生对象。
  • 访问权限:伴生对象可以访问外部类的 private 构造函数(这使得它非常适合做工厂模式)。
  • 实例归属:虽然用法像 Java 静态方法,但在运行时,伴生对象依然是一个真实的对象实例。

3.3 & 3.4 完整运行示例:用户工厂模式

这个例子展示了如何使用伴生对象来管理工厂方法类级常量

// --- 复制以下代码到 IDE 即可运行 ---

class User private constructor(val id: Int, val name: String) {

    // 定义伴生对象
    companion object {
        // 1. 类级常量 (类似 public static final)
        const val MAX_NAME_LENGTH = 8
        private var autoIncrementId = 1

        // 2. 工厂方法 (类似 public static User create(...))
        fun create(name: String): User? {
            if (name.length > MAX_NAME_LENGTH) {
                println("错误: 用户名 '$name' 太长了 (最大 $MAX_NAME_LENGTH)")
                return null
            }
            // 伴生对象可以访问外部类的 private 构造函数
            return User(autoIncrementId++, name)
        }
    }

    fun showInfo() {
        println("用户 ID: $id, 名字: $name")
    }
}

fun main() {
    // 直接通过类名访问常量
    println("系统限制最大用户名长度: ${User.MAX_NAME_LENGTH}")

    // 直接通过类名调用工厂方法
    val user1 = User.create("Alice")
    val user2 = User.create("Bob")
    val user3 = User.create("Christopher") // 名字过长

    user1?.showInfo()
    user2?.showInfo()
    // user3 为 null,不执行 showInfo
}

预期输出结果

系统限制最大用户名长度: 8
错误: 用户名 'Christopher' 太长了 (最大 8)
用户 ID: 1, 名字: Alice
用户 ID: 2, 名字: Bob


四、对象表达式:替代匿名内部类

4.1 基本语法

当我们需要创建一个继承自某个类实现某个接口的临时对象,且不需要给这个类命名时,使用 对象表达式。格式为 object : 父类/接口 { ... }

4.2 核心特性与 Java 的区别

  • 多继承/实现:Kotlin 对象表达式可以同时继承一个类并实现多个接口(Java 匿名内部类只能选其一)。
  • 闭包捕获:可以访问并修改外部作用域中的非 final 变量(Java 匿名内部类只能访问 effective final 变量,不能修改)。

4.3 & 4.4 完整运行示例:事件监听与闭包修改

这个例子展示了 Kotlin 对象表达式如何修改外部变量。

// --- 复制以下代码到 IDE 即可运行 ---

// 定义一个简单的点击监听接口
interface MouseListener {
    fun onClick(x: Int, y: Int)
}

// 模拟一个系统组件
class WindowButton {
    var listener: MouseListener? = null

    // 模拟触发点击
    fun simulateClick() {
        println("[系统] 检测到鼠标点击...")
        listener?.onClick(100, 200)
    }
}

fun main() {
    val button = WindowButton()

    // 外部变量 (普通 var)
    var totalClickCount = 0

    // 【核心代码】使用 object : 接口 创建匿名对象
    button.listener = object : MouseListener {
        override fun onClick(x: Int, y: Int) {
            // 特性:可以直接修改外部的非 final 变量 (Java 做不到这点)
            totalClickCount++
            println("回调: 坐标 ($x, $y), 这是第 $totalClickCount 次点击")
        }
    }

    // 模拟多次操作
    button.simulateClick()
    button.simulateClick()

    println("--- 最终统计: 总点击次数 = $totalClickCount ---")
}

预期输出结果

[系统] 检测到鼠标点击...
回调: 坐标 (100, 200), 这是第 1 次点击
[系统] 检测到鼠标点击...
回调: 坐标 (100, 200), 这是第 2 次点击
--- 最终统计: 总点击次数 = 2 ---


五、三大用法对比与选型

5.1 核心差异表

维度Object 单例伴生对象 (Companion)对象表达式 (Expression)
关键字object Name {}companion object {}object : Type {}
定义位置文件顶层或类外部类内部函数内部或赋值语句中
是否有名称必须有可省略 (默认 Companion)无名称 (匿名)
实例数量全局 1每个类 1每次执行创建 1 个新对象
初始化时机第一次访问时 (懒加载)加载外部类时代码执行到表达式时

5.2 选型技巧

  1. 需要全局唯一实例? (如 AppConfig, NetworkClient) -> 选 Object 单例
  2. 逻辑是否属于类的一部分,类似静态方法? (如 Factory.create(), MathUtils.PI) -> 选 伴生对象
  3. 只在某个方法里临时用一下? (如 OnClickListener, Runnable) -> 选 对象表达式

六、实用场景举例

6.1 高级实战:临时数据载体 (Ad-hoc)

有时我们需要临时组合两个数据返回,但不想专门定义一个 data class,对象表达式可以作为临时的 DTO。

fun main() {
    // 创建一个临时的匿名对象来持有数据
    val result = object {
        val x = 10
        val y = 20
        fun sum() = x + y
    }

    // 仅在当前私有作用域内有效
    println("X: ${result.x}, Y: ${result.y}, Sum: ${result.sum()}")
}

6.2 高级实战:自定义比较器 (Comparator)

这是 Java 和 Kotlin 开发中最常见的场景之一。

import java.util.Collections

data class Student(val name: String, val score: Int)

fun main() {
    val students = arrayListOf(
        Student("ZhangSan", 85),
        Student("LiSi", 90),
        Student("WangWu", 60)
    )

    // 使用对象表达式实现 Comparator 接口
    Collections.sort(students, object : Comparator<Student> {
        override fun compare(o1: Student, o2: Student): Int {
            // 按分数降序排列
            return o2.score - o1.score
        }
    })

    println("按分数降序: $students")
}


七、总结与使用建议

7.1 核心知识点回顾

  • Object 是 Kotlin 中“单例”的代言词,它将类定义与实例化合二为一。
  • Companion Object 是类内部的单例,专门用于承载静态逻辑(工厂、常量)。
  • Object Expression 是更强大的匿名内部类,支持多接口实现和闭包变量修改。

7.2 避坑指南

  1. 单例的继承限制object 可以继承父类,但它自己不能被继承(它是 final 的)。
  2. 伴生对象不是真静态:除非加了 @JvmStaticconst,否则它在 Java 字节码层面依然是对象调用。
  3. 对象表达式的开销:虽然好用,但每次执行都会 new 一个新对象,如果是在高频循环中使用,需要注意内存开销。

7.3 最佳实践

在 Kotlin 中,不要像写 Java 一样先写 class Utils 再写 static method

  • 如果是纯函数工具,首选 顶层函数 (Top-level functions)
  • 如果有状态需要维护,使用 Object 单例
  • 如果逻辑与特定类强相关(如构建逻辑),使用 伴生对象