12、对象与伴生对象:object声明、伴生对象(companion object)、对象表达式、单例模式
说实话,Kotlin 里关于「对象」的这几个概念,刚接触时很容易搞混。我自己刚转 Kotlin 那会儿,就经常在 object 和 companion 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