KOTLIN CORE · 108
Null Safety、Collections、Data Class:把「可能沒有」變成型別的一部分
真實 mobile app 很多資料不保證立即存在:最後 GPS fix、使用者名稱、server response、permission result。Kotlin 的 nullable type 逼你在使用前處理這種不確定性。
Learning outcomes
- 能區分
String與String?。 - 能正確使用 safe call、Elvis、null check,而不是濫用
!!。 - 能用 data class 表達 domain snapshot。
- 能用 collection transform 處理歷史資料。
1. Nullable type 是不同的型別
var lastStreetName: String? = null
// val length = lastStreetName.length
// compile error
Compiler 不允許你把「可能為 null」當成「一定有 String」使用。
2. 三種處理 null 的方式
// Safe call
val length = lastStreetName?.length
// Elvis fallback
val displayName =
lastStreetName ?: "Unknown road"
// Explicit branch
if (lastStreetName != null) {
println(lastStreetName.length)
}
進入 null check 後,compiler 可以 smart cast,把該 branch 內的值視為 non-null。
3. !! 的真正意思:把風險從 compiler 推回 runtime
val length = lastStreetName!!.length
!! 不是「處理 null」,而是「我保證不是 null;如果我錯了,就允許 runtime crash」。除非你有非常強的 invariant,否則不應拿它當預設寫法。
4. Data class:把 domain data 聚合成一個明確模型
data class TripPoint(
val latitude: Double,
val longitude: Double,
val speedKmh: Double?,
val timestampMs: Long
)
speedKmh: Double? 明確表達「有些 sample 可能沒有可信速度」。這比用 -1.0 當神秘 sentinel value 更容易維護。
5. copy():建立修改後的新值
data class TripState(
val distanceKm: Double,
val elapsedSeconds: Long,
val running: Boolean
)
val old = TripState(
distanceKm = 1.2,
elapsedSeconds = 300,
running = true
)
val updated = old.copy(
distanceKm = 1.5
)
data class 常配合 immutable state model:不是到處 mutate 同一個 object,而是產生下一個 state value。
6. Collections:不是只背 map/filter
val points: List<TripPoint> = ...
val validSpeeds =
points
.mapNotNull { it.speedKmh }
.filter { it >= 0.0 }
val average =
validSpeeds.average()
List<TripPoint>mapNotNullList<Double>filtervalid speedsaverageDouble
7. Read-only collection 不等於永遠 immutable
val mutable = mutableListOf(1, 2)
val readOnly: List<Int> = mutable
mutable.add(3)
println(readOnly) // 仍可能看到 3
Kotlin 的 List interface 是 read-only view;不要自動把它理解成深度 immutable object graph。
8. Project checkpoint:Trip Meter Domain Core v2
data class TripState(
val distanceKm: Double,
val elapsedSeconds: Long,
val lastPoint: TripPoint?
)
fun displaySpeed(state: TripState): String =
state.lastPoint
?.speedKmh
?.let { "%.1f km/h".format(it) }
?: "--"
fun acceptedPoints(
points: List<TripPoint>
): List<TripPoint> =
points.filter { point ->
point.speedKmh == null
|| point.speedKmh >= 0.0
}
下一課會處理「location / network 不是立刻回來」這件事:coroutines。
9. Debug evidence:NullPointerException 出現時先找誰把 nullable 假裝成 non-null
如果 stack trace 落在 !!,真正問題通常不是「Kotlin null safety 失效」,而是程式碼主動繞過了 null safety。
Knowledge check
String與String?對 compiler 有什麼不同?- 為什麼
!!不是 validation? List為什麼不代表 underlying object 永遠 immutable?- 設計一個 nullable GPS accuracy 欄位,決定顯示 fallback 的規則。