kotlin類與對象——>對象表達式與對象聲明、內聯類
1.對象表達式與對象聲明
有時候,我們需要創建一個對某個類做了輕微改動的類的對象,而不用為之顯式聲明新的子類。Kotlin 用對象表達式和對象聲明處理這種情況
2.對象表達式
要創建一個繼承自某個(或某些)類型的匿名類的對象,我們會這么寫:
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 foo() { val adHoc = object { var x: Int = 0 var y: Int = 0 } print(adHoc.x + adHoc.y) }
請注意,匿名對象可以用作只在本地和私有作用域中聲明的類型。如果你使用匿名對象作為公有函數 的返回類型或者用作公有屬性的類型,那么該函數或屬性的實際類型會是匿名對象聲明的超類型,如果 你沒有聲明任何超類型,就會是 Any 。在匿名對象中添加的成員將無法訪問。
class C { // 私有函數,所以其返回類型是匿名對象類型 private fun foo() = object { val x: String = "x" } // 公有函數,所以其返回類型是 Any fun publicFoo() = object { val x: String = "x" } fun bar() { val x1 = foo().x // 沒問題 val x2 = publicFoo().x // 錯誤:未能解析的引用“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++ } }) // ...... }
2.對象聲明
單例模式在一些場景中很有用,而 Kotlin(繼 Scala 之后)使單例聲明變得很容易:
object DataProviderManager { fun registerDataProvider(provider: DataProvider) { // ...... } val allDataProviders: Collection<DataProvider> get() = // ...... }
這稱為對象聲明。并且它總是在 object 關鍵字后跟一個名稱。就像變量聲明一樣,對象聲明不是一個 表達式,不能用在賦值語句的右邊。
對象聲明的初始化過程是線程安全的并且在首次訪問時進行。 如需引用該對象,我們直接使用其名稱即可:
DataProviderManager.registerDataProvider(......)
這些對象可以有超類型:
object DefaultListener : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { ...... } override fun mouseEntered(e: MouseEvent) { ...... } }
注意:對象聲明不能在局部作用域(即直接嵌套在函數內部),但是它們可以嵌套到其他對象聲明或非內 部類中。
3.伴生對象
類內部的對象聲明可以用 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 Factory<T> { fun create(): T } class MyClass { companion object : Factory<MyClass> { override fun create(): MyClass = MyClass() } } val f: Factory<MyClass> = MyClass
當然,在 JVM 平臺,如果使用 @JvmStatic 注解,你可以將伴生對象的成員生成為真正的靜態方法和 字段。更詳細信息請參?Java 互操作性一節 。
4.對象表達式和對象聲明之間的語義差異
對象表達式和對象聲明之間有一個重要的語義差別:
— 對象表達式是在使用他們的地方立即執行(及初始化)的;
— 對象聲明是在第一次被訪問到時延遲初始化的;
— 伴生對象的初始化是在相應的類被加載(解析)時,與Java靜態初始化器的語義相匹配。
5.類型別名:
類型別名為現有類型提供替代名稱。如果類型名稱太?,你可以另外引入較短的名稱,并使用新的名稱 替代原類型名。
它有助于縮短較?的泛型類型。
5.1 例如,通常縮減集合類型是很有吸引力的:
typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>
5.2 你可以為函數類型提供另外的別名
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean
5.3 你可以為內部類和嵌套類創建新名稱:
class A { inner class Inner } class B { inner class Inner } typealias AInner = A.Inner typealias BInner = B.Inner
5.4 類型別名不會引入新類型。它們等效于相應的底層類型。當你在代碼中添加 typealias Predicate<T> 并使用 Predicate<Int> 時,Kotlin 編譯器總是把它擴展為 (Int) -> Boolean 。因此,當你需要泛型函數類型時,你可以傳遞該類型的變量,反之亦然:
typealias Predicate<T> = (T) -> Boolean fun foo(p: Predicate<Int>) = p(42) fun main() { val f: (Int) -> Boolean = { it > 0 } println (foo(f)) // 輸出 "true" val p: Predicate<Int> = { it > 0 } println(listOf(1, -2).filter(p)) // 輸出 "[1]" }
6.內聯類:內聯類僅在 Kotlin 1.3 之后版本可用,目前還是實驗性的
有時候,業務邏輯需要圍繞某種類型創建包裝器。然而,由于額外的堆內存分配問題,它會引入運行時的 性能開銷。此外,如果被包裝的類型是原生類型,性能的損失是很糟糕的,因為原生類型通常在運行時就 進行了大量優化,然而他們的包裝器卻沒有得到任何特殊的處理。
為了解決這類問題,Kotlin 引入了一種被稱為 內聯類 的特殊類,它通過在類的前面定義一個 inline 修飾符來聲明:
inline class Password(val value: String)
內聯類必須含有唯一的一個屬性在主構造函數中初始化。在運行時,將使用這個唯一屬性來表示內聯類的實例
// 不存在 'Password' 類的真實實例對象 // 在運行時,'securePassword' 僅僅包含 'String' val securePassword = Password("Don't try this in production")
這就是內聯類的主要特性,它靈感來源于“inline”這個名稱:類的數據被“內聯”到該類使用的地方(類 似于內聯函數中的代碼被內聯到該函數調用的地方)
7.成員
內聯類支持普通類中的一些功能。特別是,內聯類可以聲明屬性與函數:
inline class Name(val s: String) { val length: Int get() = s.length fun greet() { println("Hello, $s") } } fun main() { val name = Name("Kotlin") name.greet() // `greet` 方法會作為一個靜態方法被調用 println(name.length) // 屬性的 get 方法會作為一個靜態方法被調用 }
當然,內聯類的成員也有一些限制:
— 內聯類不能含有init代碼塊
— 內聯類不能含有幕后字段
— 因此,內聯類只能含有簡單的計算屬性(不能含有延遲初始化/委托屬性)
8.繼承
內聯類允許去繼承接口
interface Printable { fun prettyPrint(): String } inline class Name(val s: String) : Printable { override fun prettyPrint(): String = "Let's $s!" } fun main() { val name = Name("Kotlin") println(name.prettyPrint()) // 仍然會作為一個靜態方法被調用 }
禁止內聯類參與到類的繼承關系結構中。這就意味著內聯類不能繼承其他的類而且必須是 final
9.表示方式
在生成的代碼中,Kotlin 編譯器為每個內聯類保留一個包裝器。內聯類的實例可以在運行時表示為包裝 器或者基礎類型。這就類似于 Int 可以表示為原生類型 int 或者包裝器 Integer 。
為了生成性能最優的代碼,Kotlin 編譯更傾向于使用基礎類型而不是包裝器。然而,有時候使用包裝器 是必要的。一般來說,只要將內聯類用作另一種類型,它們就會被裝箱。
interface I inline class Foo(val i: Int) : I fun asInline(f: Foo) {} fun <T> asGeneric(x: T) {} fun asInterface(i: I) {} fun asNullable(i: Foo?) {} fun <T> id(x: T): T = x fun main() { val f = Foo(42) asInline(f) // 拆箱操作: 用作 Foo 本身 asGeneric(f) // 裝箱操作: 用作泛型類型 T asInterface(f) // 裝箱操作: 用作類型 I asNullable(f) // 裝箱操作: 用作不同于 Foo 的可空類型 Foo? // 在下面這里例子中,'f' 首先會被裝箱(當它作為參數傳遞給 'id' 函數時)然后又被拆箱(當它從'id'函數 中被返回時) // 最后, 'c' 中就包含了被拆箱后的內部表達(也就是 '42'), 和 'f' 一樣 val c = id(f) }
因為內聯類既可以表示為基礎類型有可以表示為包裝器,引用相等對于內聯類而言毫無意義,因此這也 是被禁止的。
10.名字修飾
由于內聯類被編譯為其基礎類型,因此可能會導致各種模糊的錯誤,例如意想不到的平臺簽名沖突:
inline class UInt(val x: Int) // 在 JVM 平臺上被表示為'public final void compute(int x)' fun compute(x: Int) { } // 同理,在 JVM 平臺上也被表示為'public final void compute(int x)'! fun compute(x: UInt) { }
為了緩解這種問題,一般會通過在函數名后面拼接一些穩定的哈希碼來重命名函數。
compute(x: UInt) //將會被表示為 public final void compute-<hashcode>(int x) //以 此來解決沖突的問題。 //請注意在 Java 中 - 是一個 無效的 符號,也就是說在 Java 中不能調用使用內聯類作為形參的函數
11.內聯類和類標別名
初看起來,內聯類似乎與類型別名非常相似。實際上,兩者似乎都引入了一種新的類型,并且都在運行時表示為基礎類型。
然而,關鍵的區別在于類型別名與其基礎類型(以及具有相同基礎類型的其他類型別名)是 賦值兼容 的,而內聯類卻不是這樣。
換句話說,內聯類引入了一個真實的新類型,與類型別名正好相反,類型別名僅僅是為現有的類型取了個新的替代名稱(別名)
typealias NameTypeAlias = String inline class NameInlineClass(val s: String) fun acceptString(s: String) {} fun acceptNameTypeAlias(n: NameTypeAlias) {} fun acceptNameInlineClass(p: NameInlineClass) {} fun main() { val nameAlias: NameTypeAlias = "" val nameInlineClass: NameInlineClass = NameInlineClass("") val string: String = "" acceptString(nameAlias) // 正確: 傳遞別名類型的實參替代函數中基礎類型的形參 acceptString(nameInlineClass) // 錯誤: 不能傳遞內聯類的實參替代函數中基礎類型的形參 // And vice versa: acceptNameTypeAlias(string) // 正確: 傳遞基礎類型的實參替代函數中別名類型的形參 acceptNameInlineClass(string) // 錯誤: 不能傳遞基礎類型的實參替代函數中內聯類類型的形參 }
12.內聯類的實驗性狀態
內聯類的設計目前是實驗性的,這就是說此特性是正在 快速變化的,并且不保證其兼容性。在 Kotlin 1.3+ 中使用內聯類時,將會得到一個警告,來表明此特性還是實驗性的。
如需移除警告,必須通過指定編譯器參數 -Xinline-classes 來選擇使用這項實驗性的特性。
13.在 Gradle 中啟用內聯類
compileKotlin { kotlinOptions.freeCompilerArgs += ["-Xinline-classes"] } tasks.withType<KotlinCompile> { kotlinOptions.freeCompilerArgs += "-Xinline-classes" }
14.在 Maven 中啟用內聯類
<configuration>
<args>
<arg>-Xinline-classes</arg>
</args>
</configuration>

浙公網安備 33010602011771號