Logo
Published on

Kotlin object 關鍵字:一個關鍵字,三種超能力

在 Kotlin 裡,object 這個關鍵字值得停下來想一想。它不只是個保留字或語法糖,而是直接把 Singleton、static 替代方案、匿名物件這三種東西融進語言層級,一個關鍵字搞定

Object Declaration — 語言層級的 Singleton

在 Java 裡寫一個 thread-safe 的 Singleton,得處理 private constructorstatic instancesynchronized 或 double-checked locking。Kotlin 用一行 object 宣告就解決了

object DatabaseConfig {
    val url = "jdbc:mysql://localhost:3306/mydb"
    val maxConnections = 10

    fun connect() {
        println("Connecting to $url")
    }
}

使用時直接透過名稱存取,不需要 new,也不需要呼叫 getInstance()

DatabaseConfig.connect()
println(DatabaseConfig.maxConnections)

底層實作 (Kotlin/JVM)

在 Kotlin/JVM 上,Kotlin 編譯器把 object declaration 編譯成帶有 static INSTANCE 欄位的 Java class,透過 static initializer 保證初始化的 thread safety。反編譯後長這樣

public final class DatabaseConfig {
    public static final DatabaseConfig INSTANCE;

    static {
        INSTANCE = new DatabaseConfig();
    }

    private DatabaseConfig() {}

    // ... 其他方法
}

在 JVM 上,它是 lazy 的 (首次存取時才觸發 class loading),也是 thread-safe 的 (由 JVM 的 class loading 機制保證)

Object Declaration 也能繼承和實作介面

Object declaration 是完整的物件,可以繼承 class 和實作介面

interface Logger {
    fun log(message: String)
}

object ConsoleLogger : Logger {
    override fun log(message: String) {
        println("[LOG] $message")
    }
}

這樣 Singleton 就可以被依賴注入、被抽換、被測試,不再像傳統 Singleton 那樣難搞

Companion Object — 比 static 更強大的設計

Kotlin 刻意拿掉了 static 關鍵字,改用 companion object — 宣告在 class 內部的特殊 object

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

    companion object {
        fun fromEmail(email: String): User {
            val name = email.substringBefore("@")
            return User(name, email)
        }

        fun fromMap(data: Map<String, String>): User {
            return User(data["name"]!!, data["email"]!!)
        }
    }
}

呼叫方式跟 Java 的 static method 一樣

val user = User.fromEmail("[email protected]")

為什麼不直接用 static?

因為 companion object 是真正的物件實例,所以能做到 static 做不到的事

可以有名稱

class User(val name: String) {
    companion object Factory {
        fun create(name: String) = User(name)
    }
}

// 兩種呼叫方式都可以
User.create("Cash")
User.Factory.create("Cash")

名稱可以省略,省略時預設名稱為 Companion

可以實作介面

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

class User(val name: String) {
    companion object : JsonDeserializer<User> {
        override fun fromJson(json: String): User {
            return User(json) // 簡化的解析邏輯
        }
    }
}

因為 companion object 本身是物件,可以直接當成參數傳遞

fun <T> parse(json: String, deserializer: JsonDeserializer<T>): T {
    return deserializer.fromJson(json)
}

// 直接傳 User,而不是 User.Companion 或 User::class
val user = parse("""{"name":"Cash"}""", User)

User 看起來像是在傳 class 本身,實際上傳的是它背後的 companion object。只要其他 class 的 companion object 也實作同一個介面,就能用同一個 parse() 處理不同型別

class Product(val id: String) {
    companion object : JsonDeserializer<Product> {
        override fun fromJson(json: String): Product {
            return Product(json)
        }
    }
}

// User 和 Product 都能傳進同一個 parse()
val user = parse("""{"name":"Cash"}""", User)
val product = parse("""{"id":"A001"}""", Product)

「直接傳 class 名稱」這種寫法在其他語言幾乎看不到,這正是 companion object 實作介面帶來的好處

可以被擴充

class User(val name: String) {
    companion object
}

// 在別的檔案裡擴充
fun User.Companion.fromCsv(csv: String): User {
    val name = csv.split(",")[0]
    return User(name)
}

// 使用
val user = User.fromCsv("Cash,[email protected]")

不用動原始 class,就能替它的「static API」加新功能

Object Expression — 更靈活的匿名物件

Java 的 anonymous inner class 有兩個限制:只能繼承一個 class 或實作一個介面,而且不能修改被捕獲的區域變數。Kotlin 的 object expression 兩個都解了

基本用法

interface ClickListener {
    fun onClick(source: String)
}

val listener = object : ClickListener {
    override fun onClick(source: String) {
        println("Clicked: $source")
    }
}

同時實作多個介面

val hybrid : Runnable = object : Runnable, Comparable<String> {
    override fun run() {
        println("Running")
    }

    override fun compareTo(other: String): Int = 0
}

不繼承任何東西的匿名物件

只是想打包幾個值,不想為此宣告一個 class:

fun getResult(): Any {
    val result = object {
        val status = "success"
        val code = 200
    }
    return result
}

要注意,這種匿名物件的型別只在 local 和 private 的場景下才能被正確識別。透過 public function 回傳時,型別會退化成 Any,屬性就存取不到了

可以修改外層變數

Java 的匿名內部類別只能存取 effectively final 的區域變數,Kotlin 沒有這個限制

var clickCount = 0

val listener = object : ClickListener {
    override fun onClick(source: String) {
        clickCount++  // 直接修改外層變數
        println("Clicked $clickCount times")
    }
}

在 Kotlin/JVM 上,底層是把變數包裝在一個 Ref 物件裡,所以能突破 Java 的 effectively final 限制

三種用法的比較

特性Object DeclarationCompanion ObjectObject Expression
主要用途Singleton類似 static 的替代方案匿名物件
生命週期全域唯一,首次存取時初始化隨外層類別首次使用時初始化隨所在的作用域
可以有名稱必須有可選
可以實作介面可以可以可以
可以繼承 class可以可以可以
Thread-safe 初始化 (JVM)可以可以不適用
可以被擴充 (extension)可以可以無法
可以存取外層 class 成員不適用可以可以

總結

Kotlin 的 object 把三種在其他語言裡要靠 pattern 或 workaround 才能搞定的東西,直接提升到語言層級。Object Declaration 讓 Singleton 不再需要樣板程式碼。Companion Object 用真正的物件取代了 static,順便帶來多型、介面實作和擴充函式。Object Expression 解除了 Java 匿名內部類別的兩個限制

一個關鍵字,三種用法,設計上沒有浪費


圖片來源:AI 產生