Object
如果只想用一個實例管理整個應用程式執行期間的一致性狀態,可以考慮定義一個單例 (Singleton)。
透過 object
關鍵字,便能定義一個只能產生一個實例的類別—單例。
使用 object
關鍵字的三種方式:
- 物件宣告 (object declaration)
- 物件運算式 (object expression)
- 伴生物件 (companion object)
物件宣告 (Object declaration)
Singleton design pattern
object Game{
private val player = Player("Madrigal")
private var currentRoom: Room = TownSquare() init{
println("Welcome, adventurer!")
player.castFireball()
} fun play(){
while(true){
//Play NyetHack
}
}
}
- 因為物件宣告會自動實例化生成物件,所以無需自訂建構函數執行初始化任務。如果有相關的程式碼要在物件初始化時執行,可以選擇加入一個初始化區塊。
fun main(){
Game.play()
}
- 直接使用物件的名稱加上函數的名稱,就可以不需經過實例化直接呼叫函數。
物件運算式 (Object expression)
anonymous implementation
有時候不一定非要定義一個新的命名類別不可。也許需要某個現有類別的一種變體實例,但只需要使用一次就行。事實上,對於這種用完即丟的類別實例,連命名都可以省略。
val abandonedTownSquare = object : TownSquare(){
override fun load() = "You anticipate applause, but no one is here..."
}
- 此匿名類別依然遵循
object
關鍵字的一個規則,亦即一旦產生實體,該匿名類別只能存在唯一一個實例。當然,他的生命週期或作用範圍遠遠小於命名單例。
Android 會在 listener 的時候使用物件運算式,來替換 Java 中的匿名類別。
window.addMouseListener(object : MouseAdapter(){
override fun mouseClicked(e: MouseEvent) {/*...*/}
override fun mouseEntered(e: MouseEvent) {/*...*/}
})
伴生物件 (Companion object)
instance/factory design pattern
如果想將某個物件的初始化關聯至一個類別實例,便可以考慮伴生物件。透過 companion
修飾符,就能在類別中宣告一個伴生物件。一個類別只允許有一個伴生物件。
伴生物件的初始化分為兩種情況:
- 初始化包含伴生物件的類別時,就會同時初始化伴生物件。所以伴生物件,很適合存放和類別定義有上下文關係的單例資料。
- 只要直接存取伴生物件的某個屬性或函數,就會觸發伴生物件的初始化。
class PremadeWorldMap{
...
companion object{
private const val MAPS_FILEPATH = "nyethack.maps"
fun load() = File(MAPS_FILEPATH).readBytes()
}
}
- 只有初始化
PremadeWorldMap
類別或呼叫load
函數時,才會載入伴生物件的內容。此外,無論產生多少個PremadeWorldMap
類別實例多少次,始終只會存在一個該伴生物件的實例。
Factory 設計模式 (Factory design pattern)
class MyClass private constructor(){
companion object Factory {
fun create(): MyClass = MyClass()
}
}
- 用
private
可見性修飾符將 MyClass 的建構式constructor
不可對外公開。 - 利用伴生物件設定一個建立類別的函數
create()
,外部的使用者就必須呼叫 MyClass 中的 create() 來建立。
fun main(){
MyClass.create()
MyClass.Factory.create() //Factory can be omitted
MyClass() //not allowed
}
伴生物件常常被用來替代使用 Java 的 public static final
public static final 是編譯期的常數
在 Kotlin 1.3 之後,可以在伴生物件加上 @JvmField
,那麼在轉成 Java Code 的時候,就會提升到相對應的位置並且加上 static。
//Kotlin
class Circle{
companion object{
@JvmField
val MAX_RADIUS = 20
}
}
轉成 Java Code 等於
//Java
class Circle {
public static final int MAX_RADIUS = 20;
}
如果沒有加上 @JvmField
,轉成 Java Code 的時候就只能內部存取(private)
//Java
class Circle {
private static final int MAX_RADIUS = 20;
}
巢狀類別 (Nested class)
不是所有類別中的類別都是匿名的。可以使用 class
關鍵字定義一個嵌入至另一個類別的命名類別。這樣定義代表嵌入的類別只與外面的類別有關聯,所以不需要放在外面。
如下例: GameInput
只與 Game
有關聯,所以將 GameInput
嵌入 Game
中。
object Game{
...
private class GameInput(arg: String?){
private val input = arg?: ""
val command = input.split(" ")[0]
val argument = input.split(" ").getOrElse(1, {""}) fun processCommand = when (command.toLowerCase()){
else -> commandNotFound()
} private fun commandNotFound() = "I'm not quite sure what you're trying to do!"
}
}
- 在
Game
類別函式中可以使用GameInput
的函式。
object Game{
...
fun play(){
while(true){
...
println(GameInput(readLine()).processCommand())
}
}
...
}
內部類別 (Inner class)
巢狀類別有一個問題,內側的類別無法使用外側類別的屬性。
如下:NestedClass 無法使用 OuterClass 的 intProp 屬性
class OuterClass{
private val intProp = 40
class NestedClass{
private val nestedIntProp = intProp
}
}
利用關鍵字 inner
加在巢狀類別宣告前,該巢狀類別成為內部類別 (inner class)
class OuterClass{
private val intProp = 40
inner class NestedClass{
private val nestedIntProp = intProp
}
}
- 如此,就可以在內部類別取得外部類別的屬性。
資料類別 (Data class)
在物件導向程式設計中,我們會將相關的資料類型儲存在一個類別裡,方便在其他類別中取用。
Kotlin 提供了 data
關鍵字,在 class
前方用 data
修飾,該類別便成為了資料類別。
data class Gamer(val name:String, val health:Int)
我們知道,在 Kotlin 的所有類別都繼承於 Any
,所以我們可以在任何實例使用 Any
類別定義的函數。包括 toString()
、 equals
以及 hashCode
等等...
在資料類別中,針對常用的函數,Kotlin 將它們的輸出稍做處理,方便使用者利用,例如
toString()
一般我們在類別呼叫其 toString()
時,我們經常發現吐出來的訊息不如我們所預期。
例如:@前方顯示的是類別名稱,後方顯示的是記憶體的位置,這對一般使用是沒有用途的。
Gamer@60e53b93
如果使用 data class
的 toString()
fun main() {
print(Gamer("andy", 100).toString())
}//Gamer(name=andy, health=100)
我們會發現,顯示的資訊變得容易閱讀,使用 data class
讓我們不一定需要覆寫 toString()
。因為資料類別提供了客製化的 toString()
函數。
equals()
如果在一般類別使用 equals
來比較兩個實例是否相同,因為是用記憶體的位址來比較,所以縱使內容相同,也是會是 fail。
class Gamer1(val name:String, val health:Int)fun main(){
val gamer1 = Gamer1("Andy", 100)
val gamer2 = Gamer1("Andy", 100)
print(gamer1 == gamer2)
}
//false
若是改用 data class
,因為是用屬性的值來比較,所以兩個實例若有相同的屬性,則兩個類別相比較就會是相等。
fun main(){
val gamer1 = Gamer("Andy", 100)
val gamer2 = Gamer("Andy", 100)
print(gamer1 == gamer2)
}
//true
copy()
除了覆寫 Any
類的函數之外,資料類別也提供了一個方便的函 數 copy
。
利用 copy
函數,我們可以將一個實例複製一份,若有需要修改的屬性,可以在引數列修改即可。
例如:
fun main(){
var gamer1 = Gamer("Andy", 100)
var gamer2 = gamer1.copy(name = "Alex")
}
println(gamer1)
println(gamer2)
//Gamer(name=Andy, health=100)
//Gamer(name=Alex, health=100)
解構宣告 (Destructuring Declarations)
資料類別還支援解構宣告,什麼是解構宣告呢?以下面的範例解釋:
fun main(){
var (name, health) = Gamer("Andy", 100)
}
在資料類別的左方用括弧包起來,並按照 data class
屬性順序填入變數名稱, data class
就會自動將相對應的值傳入變數中。
如上例:name 為 Andy,health 為 100。
因為資料類別為自動產生主建構函數的屬性增加對應的元件函數。
如下:
data class Gamer (val name:String, val health: Int){
operator fun component1() = name //自動產生
operator fun component2() = health //自動產生
}
注意到了嗎?這邊是用 operator
關鍵字來定義解構函數,所以在其他的類別同樣也可以使用 operator
來定義解構函數。
我們可以將原本的資料類別加上一個解構函數。
data class Gamer (val name:String, val health: Int){
operator fun component3() = 10
}fun main(){
var (name, health, point) = Gamer("Andy", 100)
}
//point = 10
資料類別需要注意的事:
- 至少需要一個帶一個參數的主建構函數。
- 主建構函數的參數必須要是
val
或是var
。 - 不能使用
abstract
、open
、sealed
和inner
修飾符。
列舉類別 (Enuerated class)
簡稱「enum」,用來定義常數集合的特殊類別。
在 class
前方加上 enum
關鍵字,該類別即成為列舉類別。
enum class Direction{
NORTH,
EAST,
SOUTH,
WEST
}
可以直接用 .
引用列舉類別。
Direction.NORTH
每一個列舉類別的元素都包含兩個函數:name、oridinal
Direction.NORTH.name //NORTH
Direction.NORTH.oridinal //0
我們也可以替列舉類別增加主建構函數,
enum class Direction(val short_name:String){
NORTH("N"),
EAST("E"),
SOUTH("S"),
WEST("W");
}Direction.NORTH.short_name //N
當用 when
判斷列舉類別時,若沒有將所有條件寫上,編譯器將會發出警告。
fun showDirection(direction: Direction){
when(direction){
Direction.NORTH -> TODO()
Direction.EAST -> TODO()
Direction.SOUTH -> TODO()
Direction.WEST -> TODO()
}
}
- 將所有列舉類別的元素填入即可,或是用
eles
branch 代表未加入的所有元素。
運算子重載 (Operator overloading)
要將內建運算子應用至自訂類別,必須先覆寫運算子函數,告訴編譯器要如何操作自訂類別。
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}println(Counter(1)+2)
//Counter(dayIndex=3)
密封類別 (Sealed class)
在列舉類別中,若在 when
判斷式中沒有將所有的列舉類別元素填入,那麼編譯器將會發出警告。
密封類別也具有相同的效果,與列舉類別不同的是,針對不同的元素,可以有不同的建構子。
如下例:密封類別 StudentStatus 中,有三個類別,其中 Active
的建構子有一個參數,其餘兩個皆沒有。
sealed class StudentStatus{
object NotEnrolled :StudentStatus()
class Active(val courseId: String) : StudentStatus()
object Graduated: StudentStatus()
}fun main(){
val student = Student(StudentStatus.Active("Kotlin101"))
studentMessage(student.status)
}fun studentMessage(status: StudentStatus):String{
return when(status){
is StudentStatus.NotEnrolled -> "Please choose a churse!"
is StudentStatus.Active -> "You are enrolled in: ${status.courseId}"
is StudentStatus.Graduated -> "Congratulations!"
}
}
如果本篇文章有幫助到您,請 👏🏻鼓勵我。
謝謝 🙇🏻
參考
KotlinLang.org: Companion Objects
KotlinLang.org: Nested and Inner Classes