有時候我們需要稍微改動一個類別,但我們不想宣告一個新的類別繼承它。Kotlin 提供物件表達式 (object expressions) 跟物件宣告 (object declarations) 來處理這種情況。
物件表達式 Object Expressions
當我們需要一個匿名類別來繼承某類別時,通常我們會這麼做。
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*...*/ }
override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
假如父類別有建構式,就必須傳入適當的參數。如果有許多父類別,它們都必須列在冒號後面並用逗號隔開。
open class A(x: Int) {
public open val y: Int = x
}
interface B { /*...*/ }
val ab: A = object : A(1), B {
override val y = 15
}
如果只是需要一個物件,不需要繼承任何父類別,我們可以直接這樣寫。
fun sayHi() {
val person = object {
var firstName: String = "Hello"
var lastName: String = "World"
}
print(firstName + " " + lastName)
}
這邊有一點需要注意,匿名物件只能被使用在區域或私有宣告。如果要用在一個公開函數或公開屬性,實際型態將會變成匿名物件的父類別或是 Any (如果沒指定父類別)。在這種情況下,匿名物件裡的屬性無法被存取。
class C {
// Private function, so the return type is the anonymous object type
private fun foo() = object {
val x: String = "x"
}
// Public function, so the return type is Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // Works
val x2 = publicFoo().x // ERROR: Unresolved reference 'x'
}
}
物件表達式裡面的程式碼可以存取外部範圍變數。
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// ...
}
物件宣告 Object Declarations
單例 (singleton) 是一種很常被使用的設計模式。Kotlin 提供了很容易的方式宣告單例。
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
我們稱這為物件宣告 (object declaration)。由關鍵字 object 開頭,後面接名稱。跟一般變數宣告一樣,物件宣告不是表達式,不能被使用在等號 (=) 的右邊。
物件宣告的初始化是執行緒安全的 (thread-safe)。
直接使用名稱就可以存取這個物件。
DataProviderManager.registerDataProvider(...)
它也可以繼承父類別。
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
}
物件宣告不能被放在區域範圍裡 (如函數),但可以被放在另一個物件宣告或非內部類別裡。
協同物件 Companion Objects
當我們宣告一個物件在類別裡時,可以替它標上 companion。
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
協同物件裡的所有成員都可以直接透過類別名稱被存取。
val instance = MyClass.create()
協同物件的名稱可以省略,編譯器會直接選擇 Companion 這個字當名稱。
class MyClass {
companion object { }
}
val x = MyClass.Companion
類別名稱本身可以當作協同物件的實體參考。
class MyClass1 {
companion object Named { }
}
val x = MyClass1
class MyClass2 {
companion object { }
}
val y = MyClass2
雖然協同物件的成員看起來像是其他語言的靜態成員,但在執行期間他們是真實物件的實例成員,甚至可以實作介面 (interface)。
interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
val f: Factory<MyClass> = MyClass
在 JVM 平台上我們還是可以使用 @JvmStatic 讓協同物件的成員變成真正的靜態函數跟屬性。
物件表達式跟物件宣告的語義差別
物件表達式跟物件宣告之間有一個重要的語義差別:
- 物件表達式是在使用它們的時候立即被執行 (及初始化)
- 物件宣告是在第一次被用到的時候才被延遲 (lazily) 初始化。
- 協同物件的初始化是在類別被載入時發生,跟 Java 的靜態初始化語義對應。