类型系统与空安全¶
Kotlin 的类型系统是它区别于 Java 的核心特性之一。你可以把 Kotlin 的类型理解为“编译器参与设计 API 契约”的工具:哪些值可以为空、哪些值不能为空、什么时候需要显式处理异常路径,都通过类型表达出来。
一切都像对象¶
在 Kotlin 中,你可以对数字、布尔值、字符等基础类型调用成员函数:
val number = 42
println(number.toString())
println(number.inc())
这不代表 Kotlin 在运行时一定把所有值都装箱成对象。对于 JVM 后端,数字、布尔值等类型在很多情况下会使用高效的原始类型表示。你在源码层面看到的是统一的对象模型,编译器会负责优化运行时表示。
Java 对比:
- Java 有
int和Integer的区别。 - Kotlin 源码中通常只写
Int。 - 当泛型、可空类型或 Java 互操作需要时,Kotlin 可能发生装箱。
常见基础类型¶
| Kotlin | Java 接近类型 | 说明 |
|---|---|---|
Byte |
byte / Byte |
8 位整数 |
Short |
short / Short |
16 位整数 |
Int |
int / Integer |
32 位整数 |
Long |
long / Long |
64 位整数 |
Float |
float / Float |
32 位浮点数 |
Double |
double / Double |
64 位浮点数 |
Boolean |
boolean / Boolean |
布尔值 |
Char |
char / Character |
字符 |
String |
String |
字符串 |
Array<T> |
T[] |
数组 |
非空类型与可空类型¶
Kotlin 普通类型默认非空:
val name: String = "Ada"
println(name.length)
下面的代码不能编译:
// val name: String = null
如果变量确实可能为空,类型后面加 ?:
val name: String? = null
此时不能直接访问成员:
// println(name.length) // 编译错误
编译器要求你先处理空值。这就是 Kotlin 空安全的核心:把 Java 中经常运行时才暴露的 NullPointerException,尽量提前到编译期暴露。
显式空值判断与智能转换¶
fun printLength(text: String?) {
if (text != null) {
println(text.length)
}
}
在 if (text != null) 成立的分支里,编译器知道 text 不再是 String?,而是 String。这叫智能转换。
智能转换依赖编译器能确认变量在检查后没有被改变。局部 val 最容易被智能转换;可变属性、开放属性或跨线程可变值通常不一定能被安全转换。
安全调用:?.¶
val name: String? = null
println(name?.length) // null
name?.length 的含义是:
- 如果
name不为空,返回name.length。 - 如果
name为空,整个表达式返回null。
类型也会变化:String.length 是 Int,但 name?.length 是 Int?。
安全调用可以链式使用:
val cityName = user?.address?.city?.name
只要链条中任何一步是 null,结果就是 null。
Elvis 运算符:?:¶
?: 用于为空时给默认值:
fun displayName(name: String?): String {
return name ?: "匿名用户"
}
常见组合:
val length = name?.length ?: 0
因为 return 和 throw 在 Kotlin 中也是表达式,所以可以写:
fun requireName(name: String?): String {
return name ?: throw IllegalArgumentException("name 不能为空")
}
或提前返回:
fun sendEmail(email: String?) {
val validEmail = email ?: return
println("发送邮件到 $validEmail")
}
非空断言:!!¶
val name: String? = getName()
println(name!!.length)
!! 的意思是:“我确信它不是 null,如果错了就抛异常。”这会把 Kotlin 的空安全优势交还给运行时。
建议:
- 业务代码中尽量少用
!!。 - 如果能用
?: return、?: throw、?.let {},优先使用这些表达更明确的写法。 !!更适合测试代码、临时迁移代码或你确实能证明不为空但编译器无法推断的边界。
let 处理非空值¶
val email: String? = findEmail()
email?.let {
println("发送邮件到 $it")
}
?.let {} 表示只有左侧不为空时才执行代码块。在代码块中,it 是非空类型。
如果逻辑很简单,let 很方便;如果代码块很长,显式 if (email != null) 可能更清晰。
安全类型转换:as?¶
普通转换失败会抛异常:
val value: Any = "Kotlin"
val text = value as String
安全转换失败返回 null:
fun lengthOfString(value: Any): Int {
val text = value as? String
return text?.length ?: -1
}
Java 对比:
int lengthOfString(Object value) {
if (value instanceof String text) {
return text.length();
}
return -1;
}
可空集合元素¶
集合本身可空和元素可空是两回事:
val a: List<String>? = null // 列表引用可能为空
val b: List<String?> = listOf(null) // 列表不为空,但元素可能为空
val c: List<String?>? = null // 两者都可能为空
过滤空元素:
val names: List<String?> = listOf("Ada", null, "Grace")
val nonNullNames: List<String> = names.filterNotNull()
Kotlin 中仍可能出现 NPE 的情况¶
Kotlin 大幅减少空指针,但不是绝对杜绝。常见来源:
- 显式
throw NullPointerException()。 - 使用
!!。 - 构造或初始化顺序问题,例如在对象未初始化完成时泄露
this。 - Java 互操作,尤其是没有空性注解的 Java API。
- Java 代码向 Kotlin 期望非空的泛型集合中放入
null。
实践建议¶
- 公共 API 的可空性要认真设计,不要随手写
String?。 - 能用非空类型就不要用可空类型。
- 可空值进入函数后尽早处理,例如提前返回、抛出明确异常或转成默认值。
- 不要把
!!当作 IDE 提示的“快速修复”。 - 与 Java API 交互时,尽量补充
@Nullable/@NotNull/ JSpecify 注解。
参考¶
- 官方类型概览:https://kotlinlang.org/docs/types-overview.html
- 官方空安全:https://kotlinlang.org/docs/null-safety.html
- Java 与 Kotlin 空性对比:https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html