Kotlin Annotations (註解)
Annotations (註解) 是一種將 metadata (元數據) 附加到程式碼上的機制。這些數據本身不直接影響程式碼的執行邏輯,但可以被編譯器 (Compiler) 或執行期 (Runtime) 的工具讀取,用來進行程式碼檢查、生成 boilerplate code、或是改變程式執行的行為(透過 Reflection)。
如果你寫過 Java,對 @Override、@Deprecated 或 Spring Framework 的 @Autowired 一定不陌生,這些都是 Annotations。
宣告註解 (Declaring Annotations)
在 Kotlin 中,使用 annotation class 關鍵字來定義一個註解:
annotation class Fancy
定義好後,就可以將它標記在類別、函式、參數或變數上:
@Fancy
class Foo {
@Fancy
fun baz(@Fancy args: Int) { ... }
}
元註解 (Meta-Annotations)
在定義自定義註解時,我們通常需要說明這個註解「可以用在哪裡」以及「存活多久」。這時就需要用到 元註解 (Meta-Annotations),也就是「標記註解的註解」。
最常用的元註解有以下幾個:
@Target
指定你的註解可以標記在哪些程式碼元素上(例如:只能用在類別,或是只能用在函式)。
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
annotation class CustomMarker
常見的 AnnotationTarget 包括:
CLASS:類別、介面、物件FUNCTION:函式 (不含建構式)PROPERTY:屬性FIELD:欄位 (Backing field)VALUE_PARAMETER:參數CONSTRUCTOR:建構式
@Retention
指定註解的存活週期,決定該註解會保留到哪個階段。
@Retention(AnnotationRetention.RUNTIME)
annotation class Inspectable
SOURCE:只保留在原始碼中,編譯時會被丟棄。適用於編譯器檢查工具(如@Suppress)。BINARY:保留在編譯後的 class 檔案中,但執行期 (Runtime) 無法透過 Reflection 讀取。RUNTIME:保留在 class 檔案中,且執行期可透過 Reflection 讀取。
@Retention 的預設值是 RUNTIME。這點與 Java 不同(Java 預設是 CLASS,即 BINARY)。@Repeatable
允許在同一個元素上重複使用該註解。
@MustBeDocumented
表示這個註解應該被包含在生成的 API 文件中。
註解的建構式與參數
註解可以擁有建構式並接收參數,讓我們在使用時傳遞額外的資訊。
annotation class Special(val why: String)
@Special("Example")
class Foo
註解的參數型別是有限制的,只能是以下幾種:
- 對應 Java 的基本型別 (Int, Long, Double, Boolean 等)
- String
- Classes (
KClass) - Enums
- 其他 Annotation
- 上述型別的 Array
null 存為註解屬性的值。參數使用範例
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith("") // 預設值
)
@Deprecated("Use newFunction instead", ReplaceWith("newFunction()"))
fun oldFunction() { ... }
如果是 Class 作為參數,需要使用 ::class:
annotation class TestRunner(val kClass: KClass<*>)
@TestRunner(String::class)
fun testString() { ... }
註解的使用處目標 (Use-site Targets)
這是在 Kotlin 中非常重要的一個概念。
當你在 Kotlin 宣告一個屬性 (Property) 時,編譯器通常會產生多個 Java 元素:一個 backing field、一個 getter,如果是 var 還會有一個 setter。
如果你在屬性上加註解,編譯器怎麼知道這個註解是要加在 field、getter 還是 setter 上?這時候就需要 Use-site Targets。
語法格式為:@target:AnnotationName
class Example(@field:Ann val foo: String, // 標記在 Java field
@get:Ann val bar: String, // 標記在 Java getter
@param:Ann val quux: String) // 標記在 Java 建構式參數
常見的目標清單:
field:Backing fieldget:Property getterset:Property setterparam:建構式參數setparam:Setter 的參數delegate:委派屬性的儲存欄位 (Delegate field)file:檔案級別 (通常寫在檔案最上方,package 宣告之前)
實務範例:JUnit 與 Dependency Injection
在使用 Java 的框架時,這非常常見。例如 JUnit 的 @Rule 必須標記在 public field 上,但 Kotlin 的屬性預設是 private field 加上 getter/setter。如果不指定 target,JUnit 會找不到該 field。
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class MyTest {
// 錯誤寫法:JUnit 會報錯,因為註解預設可能不在 field 上或是 field 是 private
// @Rule val tempFolder = TemporaryFolder()
// 正確寫法:強制註解在 field 上,並透過 @JvmField 讓 field 變為 public
@get:Rule
val tempFolder = TemporaryFolder()
}
(註:上述 JUnit 4 範例中,通常配合 @JvmField 會更簡單,直接 @JvmField @Rule val ...)
另一個常見例子是將整個檔案標記名稱(更改生成的 Facade Class 名稱):
@file:JvmName("StringUtil")
package com.example.utils
fun join(strings: List<String>) { ... }
這樣 Java 呼叫時就會是 StringUtil.join() 而不是預設的 FileNameKt.join()。
常見內建註解
Kotlin 標準庫提供了一些好用的註解:
@Deprecated
用來標示某個功能已過時。最強大的是它支援 ReplaceWith,讓 IDE 可以提供「一鍵快速修正 (Quick Fix)」,自動將舊程式碼替換成新寫法。
@Deprecated(
message = "Use newLog instead",
replaceWith = ReplaceWith("newLog(msg)")
)
fun log(msg: String) { ... }
fun newLog(msg: String) { ... }
@JvmStatic
用在 object 或 companion object 的成員上,告訴編譯器產生真正的 static method 或 field,方便 Java 呼叫。
@Throws
Kotlin 沒有 Checked Exceptions,所以不需要宣告 throws。但如果你的 Kotlin 函式會拋出例外,且需要讓 Java 呼叫端知道必須 catch 這個例外,就需要加上 @Throws。
@Throws(IOException::class)
fun readFile(path: String) { ... }
透過 Reflection 讀取註解
通常我們會定義註解,然後透過 Reflection API 在 Runtime 讀取它們來做一些依賴注入或路由控制。
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Runner
class Tests {
@Runner
fun test1() = println("Test 1")
fun test2() = println("Test 2")
}
fun main() {
val tests = Tests()
// 取得所有方法
val kClass = tests::class
for (func in kClass.members) {
// 檢查是否有 @Runner 註解
if (func.annotations.any { it is Runner }) {
func.call(tests)
}
}
}
這個範例展示了如何打造一個超迷你的測試框架,只執行被 @Runner 標記的函式。
小提醒
- 註解雖然強大,但過度依賴反射 (Reflection) 讀取註解可能會影響效能,尤其是在 Android 等資源受限的平台上。
- 使用像是 Dagger / Hilt 或 Room 這類框架時,它們通常使用 KAPT (Kotlin Annotation Processing Tool) 或 KSP (Kotlin Symbol Processing) 在「編譯時期」處理註解並生成程式碼,這樣就不會有 Runtime 效能損耗。