首页 / Android 入门教程 / 空安全与类型转换

Android 入门教程

空安全与类型转换

本教程共 100 篇 · 第 16 篇 · 更新于 2026-07-28 · 约 8 分钟阅读

AndroidAndroid 入门教程Kotlin空安全Elvis安全调用作用域函数类型转换

16. 空安全与类型转换

本节目标:彻底搞懂 Kotlin 怎么处理 null、安全调用怎么写、Elvis 运算符、作用域函数(let/also/run/apply/with/takeIf)怎么选、类型检查与智能转换怎么做。

为什么空安全重要

NullPointerException(NPE)是 Java 应用最常见的崩溃,被戏称为「十亿美元错误」。Kotlin 在类型系统层面把「可空」和「非空」区分开,把大量 NPE 挪到编译期解决。

可空类型

默认类型不接受 null,要存 null 加 ?

var name: String = "Android"
// name = null          // 编译报错

var nickname: String? = "Andy"
nickname = null         // OK

安全调用 ?.

可空类型用 . 直接访问成员会报错,要用安全调用 ?.

val len = nickname?.length    // nickname 不为 null 返回 length,为 null 返回 null
// 类型是 Int?

链式安全调用:

val city = user?.address?.city     // 任何一环为 null,整体返回 null

Elvis 运算符 ?:

?: 给 null 提供默认值:

val len = nickname?.length ?: 0       // 为 null 时用 0
val displayName = nickname ?: "匿名"   // 为 null 时用"匿名"

// 也常用于提前返回
val user = getUser() ?: return         // 为 null 直接 return
val data = response?.data ?: throw IllegalStateException("无数据")
Tip
  • Elvis 在 Android 里特别有用:取 Intent extra 给默认值、解析 JSON 给默认值、UI 显示空字符串代替 null,都用它。

非空断言 !!

!! 强制把可空转非空,如果真是 null 就抛 NPE:

val len = nickname!!.length    // 断言 nickname 非 null
Warning
  • 慎用 !!。它等于放弃了 Kotlin 的空安全保护,把 NPE 重新请回来。只在「逻辑上确定不为 null 但类型是可空」时用,并加注释说明。能用 ?: 给默认值或 ?.let { } 处理的就别用 !!

安全转换 as?

as 强转类型不匹配会抛 ClassCastExceptionas? 失败时返回 null:

val obj: Any = "hello"
val len: Int? = obj as? Int     // null,不会崩
val str: String? = obj as? String  // "hello"

配 Elvis 给默认值:

val num = (obj as? Int) ?: 0

类型检查 is / !is

if (obj is String) {
    // 在这个分支里 obj 自动是 String 类型,不用强转
    println(obj.length)
}

if (obj !is String) return
// 这里 obj 也是 String
println(obj.length)

is 判断后编译器自动做智能转换(Smart Cast),不用手动强转。这是 Kotlin 比 Java 顺手的地方。

Note
  • 智能转换只对 val 或经编译器确认不会变的变量有效。var 在多线程下可能被改,智能转换不生效,要手动 as

作用域函数

Kotlin 有 6 个作用域函数,用来简化对对象的操作。它们区别在引用方式和返回值:

函数引用方式返回值典型场景
letitLambda 结果可空处理、转换
runthisLambda 结果初始化并返回结果
withthisLambda 结果对同一对象多次操作
applythis对象本身配置对象(Builder 模式)
alsoit对象本身副作用(日志、调试)
takeIfit对象或 null条件过滤

let

最常用于可空处理:

nickname?.let {
    // 只有 nickname 非 null 才执行,it 是非空的 String
    println("名字长度 ${it.length}")
}

// 转换
val length = name?.let { it.length } ?: 0

apply

配置对象,返回对象本身,适合 Builder 模式:

val intent = Intent().apply {
    action = "android.intent.action.VIEW"
    data = Uri.parse("https://example.com")
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}

val dialog = AlertDialog.Builder(context).apply {
    setTitle("提示")
    setMessage("确定删除吗?")
    setPositiveButton("确定") { _, _ -> /* 删除 */ }
}.create()

also

做副作用,不影响链式调用:

val user = User("张三", 18).also {
    log("创建了用户:${it.name}")    // 打日志,返回 user
}

run / with

// run:初始化并返回结果
val greeting = StringBuilder().run {
    append("Hello, ")
    append(name)
    append("!")
    toString()
}

// with:对同一对象多次操作
with(textView) {
    text = "Hello"
    setTextColor(Color.RED)
    textSize = 16f
}

takeIf / takeUnless

条件过滤:

val file = File("config.json")
val valid = file.takeIf { it.exists() }?.takeIf { it.length() > 0 }
// file 不存在或为空,valid 为 null

val even = num.takeIf { it % 2 == 0 }   // 偶数返回自身,奇数返回 null
Tip
  • 记不住没关系,记住两个最常用:可空用 let,配置对象用 apply。其他用到再查。

Android 实战

// 处理 Intent extra
val userId: Long = intent.getLongExtra("user_id", 0L)
val name: String = intent.getStringExtra("name") ?: "匿名"
val avatar: String? = intent.getStringExtra("avatar")  // 可能没传

// 处理网络响应
response.body()?.let { body ->
    showData(body)
} ?: run {
    showError("响应为空")
}

// Activity 里 findViewById 配合 apply
val textView = TextView(context).apply {
    text = "Hello"
    setPadding(16, 16, 16, 16)
}

// ViewModel 处理可空状态
fun getUser(id: String) {
    viewModelScope.launch {
        val user = repo.getUser(id)      // 返回 User?
        user?.let {
            _state.value = UiState.Success(it)
        } ?: run {
            _state.value = UiState.Error("用户不存在")
        }
    }
}

空安全的常见陷阱

1. 平台类型

调 Java 代码时,Java 返回的类型没有可空信息,Kotlin 把它当「平台类型」(String!),可能为 null 也可能不为 null。

// Java 方法返回 String,Kotlin 调用
val name = javaObj.getName()     // 类型是 String!,不确定可空
println(name.length)             // 如果 name 实际是 null,NPE
Warning
  • 调 Java API 返回的值,要么当可空处理(?.),要么明确赋给可空类型变量。别直接当非空用。

2. lateinit 延迟初始化

有些属性(如 Activity 里的 View)不能在构造时初始化,又不想用可空类型。用 lateinit

class MainActivity : AppCompatActivity() {
    private lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button = findViewById(R.id.button)   // 这里初始化
        button.setOnClickListener { /* ... */ }
    }
}

lateinit 只能用于 var,不能是基本类型。访问前没初始化会抛 UninitializedPropertyAccessException

判断是否初始化(仅 lateinit var 支持):

if (::button.isInitialized) {
    button.setOnClickListener(null)
}

3. lazy 惰性初始化

只读属性第一次访问时才初始化:

val database by lazy { Room.databaseBuilder(this, AppDb::class.java, "app.db").build() }

lazy 是线程安全的(默认 LazyThreadSafetyMode.SYNCHRONIZED),单例初始化常用。

小结

类型默认非空,加 ? 才能存 null。?. 安全调用,?: 给默认值,!! 谨慎用。as? 安全转换,is 后智能转换。作用域函数记住 let(可空处理)和 apply(配置对象)。调 Java 代码注意平台类型。lateinitlazy 处理延迟初始化。下一节学高阶函数与 Lambda。