【Kotlin】第11課-單例與伴生物件

使用 Kotlin 的「object」關鍵字,可以在不宣告類別的情況下,建立抽象類別或介面的物件。除此之外,object 尚能建立單例物件。

本文會先解說上述的物件。接著介紹單例,讓所有程式碼都存取同一個物件。最後講解伴生物件,藉此實現類似 Java 靜態變數與方法的用法。

一、抽象類別與介面的物件

關於第8課的抽象類別和第9課的介面,我們會宣告類別去繼承或實作,並完成它們定義的抽象屬性與方法。之後建立的物件。但若這個類別的使用機會極少,甚至是一次性使用,那麼我們能用「object :」語法直接建立一個物件,範例如下。

// 建立抽象類別的物件
val book = object : Product("BK001", "數學複習講義", 320, "本") {
override val TYPE = "書籍"
override fun getBasicInfo() = "$TYPE $name $$price"
override fun getExtraInfo() = "作者:許小明,東東出版社"
}

// 建立介面的物件
val listener = object : ExportFinishListener {
override fun onFinish(msg: String) {
println(msg)
}
}

上面分別用抽象類別與介面建立物件,前者是產品,後者是 callback 方法。使用 object 關鍵字,效果相當於宣告類別並隨即建立它的物件,只不過這個類別是「匿名」的。


二、單例物件

單例的意思是,在程式運行期間,某個類別的物件必定只存在唯一一個。所以各個程式碼或執行緒使用該類別的物件時,都是存取記憶體中的同一個。以第9課範例中的 Exporter 類別為例,它是一個能模擬將物件輸出成檔案的類別。本節將利用它來講解單例物件的宣告與使用。

object Exporter {
fun export(item: Exportable, listener: ExportFinishListener) {
val isFailed = (Math.random().toInt() * 2) % 2 == 0
val msg =
if (isFailed)
"輸出失敗,請重新嘗試!"
else
"輸出成功!\n檔案路徑:${item.getExportPath()}" +
"\n檔案內容:\n${item.getExportContent()}"
listener.onFinish(msg)
}
}

單例物件一樣使用 object 關鍵字來宣告(不需加上冒號)。讀者可以像類別那樣宣告屬性與方法,甚至繼承抽象類別與介面。下面提供一個範例,示範介面物件與單例物件的使用。

fun main(args: Array<String>) {
// 建立介面物件
val book = object : Exportable {
override var exportLocation = "\\product"
override fun getFileName() = ""
override fun getExportContent() = ""
}

// 定義 callback 方法
val listener = object : ExportFinishListener {
override fun onFinish(msg: String) {
JOptionPane.showMessageDialog(
null,
msg,
"匯出檔案",
JOptionPane.INFORMATION_MESSAGE)
}
}

Exporter.export(book, listener)
}

首先建立一個可匯出的物件,內容是產品資料。接著定義 callback 方法,在匯出完成後會出現訊息視窗。最後使用單例物件 Exporter,傳入產品物件 book 與 callback 方法,模擬將資料進行匯出。

範例程式中呼叫單例物件的方法時,跟 Java 的靜態方法很像。然而 Kotlin 沒有靜態(static)的概念,它的概念其實是「物件.方法」的呼叫方式。

三、集中放置常數與方法

在大一點的程式專案,難免會有一些常用的方法、字串與數字等。在使用 Java 時,我們可能會以靜態方法與常數的形式宣告在專門的類別,並在需要的地方存取,確保一致性也方便管理。

然而 Kotlin 不像 Java 一樣有靜態(static)的語法可用,因此可選擇將這些方法與常數轉而存放在單例物件中。以下是一些例子,DateUtils 單例存放了能夠將日期與字串互相轉換的工具方法,而 ExportConstants 單例則是存放一些字串常數。

object DateUtils {
private val sdf = SimpleDateFormat("yyyy-MM-dd hh:mm:ss")

fun toDate(dateStr: String) = sdf.parse(dateStr)
fun toDateString(date: Date) = sdf.format(date)
}

object ExportConstants {
const val EXPORT_FILE = "匯出檔案"
const val EXPORT_SUCCESS = "輸出成功!"
const val EXPORT_FAILED = "輸出失敗,請重新嘗試!"
}

在單例中宣告存取權限為 public 的常數時,若型態為 Int、Double 等原生型態(primitive)或String,IntelliJ 會建議加上「const」關鍵字。原因是為了增進效率,也能被Java的程式碼使用。筆者在第6課提到,類別的屬性不需宣告 get 方法,因為 Kotlin 會隱含地建立。而單例也是如此,不過宣告常數時加上了 const,就不會產生 get 方法,執行時便能減少方法的呼叫。


四、伴生物件

第三節示範的是在一個獨立的單例物件中存放常數與方法,接下來要講解的是如何存放在一般的類別。我們在第10課認識了列舉,筆者介紹 valueOf 方法時有提到,若找不到對應的列舉物件,程式便會拋出例外。本節就以列舉類別為例子,示範伴生物件的使用。

enum class CurrencyType(val chineseName: String,
val simpleExRate: Double) {
USD("美元", 30.0),
CNY("人民幣", 4.5),
JPY("日圓", 0.3),
TWD("臺幣", 1.0);

companion object {
fun findByName(typeName: String): CurrencyType? {
return values().find { typeName.equals(it.name, true) }
}

val foreignTypes = values().filter { it != TWD }
}
}

使用「companion object」關鍵字,可以在類別中劃分出一個區塊,稱為「伴生物件」。在裡頭,我們能像上一節的單例存放資料與方法。

此處宣告了 findByName 方法,它可以藉由傳入的 typeName 參數,以不分大小寫的規則找出名稱相同的列舉物件,否則回傳 null。另外也宣告了一個叫做 foreignTypes 的常數,用來存放非臺幣的國外幣別。

使用伴生物件屬性與方法的做法,與單例物件相同。

println("本程式支援的外幣如下:")

val types: List<CurrencyType> = CurrencyType.foreignTypes
for (type in types) {
println(type.chineseName)
}

上一篇:【Kotlin】第10課-列舉(Enum)

下一篇:【Kotlin】第12課-資料類別(Data class)

留言