Logo
Published on

Kotlin 數字型別與 Null 安全性

如果你是從 Java 或 C# 轉到 Kotlin 的開發者,可能會好奇:Kotlin 的數字型別跟原本的語言有什麼不同?還有 intInteger 的差別嗎?需要擔心 boxing / unboxing 嗎?

這篇文章會帶你了解 Kotlin 數字型別的設計哲學,以及它與 Null 安全性之間的關係

Kotlin 沒有 Primitive Type?

在 Java 裡,數字型別分成兩種

  • Primitive typeintlongdouble 等,效能好,但不能是 null
  • Wrapper typeIntegerLongDouble 等,是物件,可以是 null

開發者必須自己決定要用哪一種,而且要注意 boxing / unboxing 帶來的效能影響

Kotlin 的做法完全不同——從語言層面來看,所有數字型別都是物件。你只會看到 IntLongDouble,不會看到 intInteger。也因為一切都是物件,所以你可以直接對數字呼叫方法

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 TypeWrapper / Reference Type誰決定用哪種
JavaintlongIntegerLong開發者自己選
C#int(value type)object(boxed)語言規則決定
Kotlin語言層面不存在統一用 IntLong編譯器自動決定

Int 與 Int? 的差別

Kotlin 的 null safety 機制讓每個型別都有兩種形式

  • Int:保證不會是 null(Non-nullable)
  • Int?:可能是 null(Nullable)

這個設計直接影響了底層的實作方式

  • Int → 編譯成 primitive int
  • Int? → 編譯成 wrapper Integer(因為 primitive 不能表示 null)

Non-nullable 賦值給 Nullable

val x: Int = 3
val y: Int? = x

這段程式碼完全合法。IntInt? 的子型別(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)

ab 各自被 boxing 成不同的 Integer 物件,所以用 === 比較 identity 時會是 false

JVM Integer Cache 的陷阱

JVM 預設對 -128127 範圍內的 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 的區分,開發者只需要使用 IntLongDouble 等型別
  • 編譯器在多數情況會自動最佳化,non-nullable 的數字型別會編譯成 primitive,但 nullable、泛型、Any 等情境仍會 boxing
  • Int 可以直接賦值給 Int?(安全的),但 Int? 不能直接賦值給 Int(必須先處理 null)
  • 處理 nullable 轉 non-nullable 時,優先使用 smart castElvis operator,盡量避免 !!
  • 注意 nullable 數字型別的 identity 問題,比較數值時永遠用 ==

圖片來源:AI 產生