- Published on
Kotlin 數字型別與 Null 安全性

如果你是從 Java 或 C# 轉到 Kotlin 的開發者,可能會好奇:Kotlin 的數字型別跟原本的語言有什麼不同?還有 int 跟 Integer 的差別嗎?需要擔心 boxing / unboxing 嗎?
這篇文章會帶你了解 Kotlin 數字型別的設計哲學,以及它與 Null 安全性之間的關係
Kotlin 沒有 Primitive Type?
在 Java 裡,數字型別分成兩種
- Primitive type:
int、long、double等,效能好,但不能是null - Wrapper type:
Integer、Long、Double等,是物件,可以是null
開發者必須自己決定要用哪一種,而且要注意 boxing / unboxing 帶來的效能影響
Kotlin 的做法完全不同——從語言層面來看,所有數字型別都是物件。你只會看到 Int、Long、Double,不會看到 int 或 Integer。也因為一切都是物件,所以你可以直接對數字呼叫方法
val n = 42
println(n.toString()) // "42"
println(n.coerceIn(0, 10)) // 10
那效能不會有問題嗎?
多數情況不會。雖然語言層面都是物件,但 Kotlin 編譯器在編譯成 JVM bytecode 時,會自動將 non-nullable 的數字型別編譯為 Java 的 primitive type
val a: Int = 42 // 編譯後 → Java 的 int(primitive)
val b: Int? = 42 // 編譯後 → Java 的 Integer(wrapper)
日常開發中,大部分數字運算用的是 non-nullable 型別,編譯器會幫你處理成 primitive,不需要操心 boxing 的成本。但遇到 Int?、泛型(List<Int>)、或是型別為 Any / 介面的情境,仍然會發生 boxing
跟 Java、C# 的比較
| Primitive / Value Type | Wrapper / Reference Type | 誰決定用哪種 | |
|---|---|---|---|
| Java | int、long | Integer、Long | 開發者自己選 |
| C# | int(value type) | object(boxed) | 語言規則決定 |
| Kotlin | 語言層面不存在 | 統一用 Int、Long | 編譯器自動決定 |
Int 與 Int? 的差別
Kotlin 的 null safety 機制讓每個型別都有兩種形式
Int:保證不會是null(Non-nullable)Int?:可能是null(Nullable)
這個設計直接影響了底層的實作方式
Int→ 編譯成 primitiveintInt?→ 編譯成 wrapperInteger(因為 primitive 不能表示null)
Non-nullable 賦值給 Nullable
val x: Int = 3
val y: Int? = x
這段程式碼完全合法。Int 是 Int? 的子型別(subtype),一個確定有值的東西,可以放進「可能有值也可能沒值」的容器裡
從 JVM 的角度來看,這裡會發生 boxing:x 原本是 primitive int,賦值給 y 時會被包裝成 Integer
Nullable 賦值給 Non-nullable
val x: Int? = 3
val y: Int = x // 編譯錯誤!
這段程式碼無法通過編譯。因為 Int? 可能是 null,Kotlin 不允許你把一個可能為 null 的值,直接塞進一個保證不為 null 的變數
你必須先處理 null 的情況,常見的做法有三種
使用 if 檢查(Smart Cast)
val x: Int? = 3
val y: Int = if (x != null) x else 0
透過 if 檢查 null 之後,Kotlin 會自動將 x 的型別從 Int? 收窄為 Int,這就是 smart cast
使用 Elvis Operator(?:)
val x: Int? = 3
val y: Int = x ?: 0 // 如果 x 是 null,就用 0 作為預設值
Elvis operator 是 Kotlin 裡處理 null 最簡潔的方式,語意上等同於「如果左邊是 null,就用右邊的值」
使用 Non-null Assertion(!!)
val x: Int? = 3
val y: Int = x!! // 強制斷言 x 不是 null
用 !! 告訴編譯器「我保證這裡不會是 null」。但如果 x 真的是 null,程式會在 runtime 丟出 NullPointerException
建議:盡量避免使用
!!,除非你百分之百確定值不會是null。過度使用!!等於放棄了 Kotlin null safety 的保護
Nullable 會影響 Identity
當數字型別是 nullable 時,底層會進行 boxing,這會影響物件的 identity(參考相等性)
val x: Int = 10000
val a: Int? = x
val b: Int? = x
println(a == b) // true(值相等,structural equality)
println(a === b) // false(不是同一個物件,referential equality)
a 和 b 各自被 boxing 成不同的 Integer 物件,所以用 === 比較 identity 時會是 false
JVM Integer Cache 的陷阱
JVM 預設對 -128 到 127 範圍內的 Integer 有 cache 機制,同樣的值會回傳同一個物件。這個上限可以透過 JVM 參數 -XX:AutoBoxCacheMax 調整,但預設就是 127
val x: Int = 100 // 在 cache 範圍內
val a: Int? = x
val b: Int? = x
println(a === b) // true(因為 JVM cache,是同一個物件)
val x2: Int = 10000 // 超出 cache 範圍
val c: Int? = x2
val d: Int? = x2
println(c === d) // false(不同的物件)
這個行為來自 JVM 底層,跟 Java 的 Integer cache 機制完全一樣
建議:比較數值時,永遠使用
==(structural equality),不要用===(referential equality),除非你有明確的理由要比較物件的 identity
泛型中的數字型別
當數字型別用在泛型裡時,因為 JVM 的泛型不支援 primitive type,所以一律會使用 wrapper type
val numbers: List<Int> = listOf(1, 2, 3)
// 底層實際上是 List<Integer>
這點目前是 JVM 平台的限制。JVM 的 Valhalla 專案(value type 支援)仍在演進中,未來如果正式落地,Kotlin 也可能因此受益,但目前還沒有確定的時程
總結
- Kotlin 從語言層面統一了數字型別,沒有 primitive 和 wrapper 的區分,開發者只需要使用
Int、Long、Double等型別 - 編譯器在多數情況會自動最佳化,non-nullable 的數字型別會編譯成 primitive,但 nullable、泛型、
Any等情境仍會 boxing Int可以直接賦值給Int?(安全的),但Int?不能直接賦值給Int(必須先處理 null)- 處理 nullable 轉 non-nullable 時,優先使用 smart cast 或 Elvis operator,盡量避免
!! - 注意 nullable 數字型別的 identity 問題,比較數值時永遠用
==
圖片來源:AI 產生