Kotlin 语法与空安全:Android 开发第一课

📅 2026/7/17 5:56:14
Kotlin 语法与空安全:Android 开发第一课
文章目录第 1 章 为什么需要空安全NPE、样板代码与类型契约原理补充踩坑动手练第 2 章 变量、类型推断与字符串模板原理补充踩坑动手练第 3 章 空安全操作符?.、?:、!! 与智能转换原理补充踩坑动手练第 4 章 data class模型、copy 与解构原理补充踩坑动手练第 5 章 sealed class、穷尽 when 与扩展函数入门原理补充踩坑动手练第 6 章 综合工程模型层与 ViewModel 边界原理补充踩坑动手练第 7 章 编译器视角可空类型的 JVM 映射概念原理补充踩坑动手练第 8 章 常见错误汇总、面试强化与实战练习原理补充踩坑动手练面试速查 · 追问链追问链 #1空安全本质 追问链 #2data class 与 copy 追问链 #3sealed 与 when ⭐追问链 #4Java 互操作破口 完整链路一句通相关推荐第 1 章 为什么需要空安全NPE、样板代码与类型契约Android 工程里和类型、模型相关的问题在 Code Review 中很常见NPE可空引用未处理线上崩溃。样板代码数据类手写equals/hashCode/toString维护成本高。可空语义不清API 返回值、导航参数、网络字段「可能有、可能没有」类型上却写成非空。Kotlin 用T与T?把「可能没有」写进类型系统配合data class、?.、?:在编译期拦住大部分空指针问题。列表/详情页里常在映射层把 DTO 可空字段转为非空 Domaintips ?: UI 只绑定非空模型。可空类型显式标注String非空String?可空赋值时编译器检查。默认不可变val只读引用var可变但优先val。表达式优先if/when可作表达式返回值。原理补充Kotlin 编译器在 JVM 上对非空类型插入Intrinsics.checkNotNull或等价逻辑可空类型在调用非空 API 前必须显式处理。这是编译期契约 运行时双保险不是魔法消除 null。踩坑把可空当非空用继续依赖!!硬解。用!!代替设计合理的默认值或错误分支。在 Java 边界接收String!平台类型却不做校验。动手练A将 3 段嵌套 null 检查改写为?./?:链。D在所在工程搜索?.列出 2 处并说明该链若不用安全调用会怎样写。第 2 章 变量、类型推断与字符串模板局部变量与字符串展示可以写得更简洁用val固定引用用$name、${expr}嵌入表达式减少拼接错误。valimmutable:String浙江// 只读引用推荐默认varcounter:Int0// 可变能 val 则 valvalinferredlistOf(1,2,3)// 推断 ListIntvalname西湖vallabel景点$name${name.length}字// 模板模型层字段多为非空类型时展示文案也可写在扩展里funProvince.displayTitle():String$nameCn$abbreviation注意val只保证引用不变val list mutableListOf(1)仍可list.add(2)。不可变集合用listOf需修改用mutableListOf并评估是否应var。原理补充类型推断由编译器前端完成生成字节码与显式类型声明一致不影响运行时性能。踩坑该val却用var增加误赋值风险。字符串模板中复杂表达式不加大括号$ab与${ab}不同。公开 APIpublic fun省略返回类型导致签名难读。动手练A为Scenery写一个displayLine()扩展返回$nameCn · $city。第 3 章 空安全操作符?.、?:、!! 与智能转换空安全是本章核心。未处理的 null 分散在调用链各处Kotlin 用操作符把**「可能没有」**压缩成表达式配合智能转换在分支内把T?提升为T。操作符含义典型用途?.安全调用左侧 null 则整链短路为 nulluser?.address?.city?:Elvis左侧 null 则取右侧name ?: 未知!!非空断言null 则抛 NPE尽量避免as?安全转型失败返回 nullintent.getSerializableExtra(k) as? MyData智能转换funprintLen(s:String?){if(s!null){println(s.length)// 此分支 s 已是 String}}?.let { }在 lambda 内it为非空是 Android 工程最常用的映射入口作用域函数篇再展开。ExploreViewModel从SavedStateHandle读取可选category参数private val category: Category? savedStateHandle.getString(category)?.let { name - runCatching { Category.valueOf(name) }.getOrNull() }链路解读getString(category)返回String?——导航参数可能缺失。?.let有值才进入映射无参数则category为nullrefresh()走全量列表。runCatching { Category.valueOf(name) }非法枚举名不崩溃.getOrNull()降级为null。这是中级工程模式可空边界 安全解析 失败降级而非!!或裸valueOf。原理补充?.编译为 null 检查 条件调用?:为条件表达式。智能转换依赖不可变局部变量val与明确控制流可变var在多线程下可能失效需用局部val快照。踩坑滥用!!user!!.name把类型契约打回运行时赌运气。?:右侧藏副作用getData() ?: loadDefault().also { save() }难读难测。对平台类型String!Java 来直接当非空未在边界校验。动手练B坏代码重构// 坏代码!! 与嵌套 iffunbad(handle:SavedStateHandle):Category{valnamehandle.getString(category)!!returnCategory.valueOf(name)}改写为ExploreViewModel同款可空 runCatching链并说明崩溃点在哪。Capi.fetchProvince(id)?.let { mapper.toUi(it) } ?: ProvinceUiState.Empty默写。第 4 章 data class模型、copy 与解构Android 数据层充斥 DTO/Entity/UI State。手写equals/hashCode/toString维护成本高字段增删时copy语义易漏。data class把值语义与不可变更新标准化。dataclassProvinceUi(valid:String,valnameCn:String,valsceneryCount:Int,)valpProvinceUi(33,浙江,120)valupdatedp.copy(sceneryCountp.sceneryCount1)// 不可变更新val(id,name,_)p// 解构编译器自动生成equals/hashCode/toString/copy/componentN()。Province模型data class Province( val id: String, val nameEn: String, val nameCn: String, val abbreviation: String, val capital: String, val coverImageUrl: String, val description: String, val history: String, val famousFigures: ListString, val cuisine: String, val sceneryCount: Int, )ExploreUiState用copy驱动单向更新常见配合StateFlow暴露给 UI_uiState.update{it.copy(isLoadingtrue)}// ..._uiState.update{it.copy(scenerieslist,isLoadingfalse)}模式data classUI State copy 可预测的状态迁移利于测试与调试。原理补充data class主构造中声明的属性参与equals/hashCode类体中的属性默认不参与。copy是编译器生成的方法非反射。踩坑在data class里放可变var字段破坏值语义。期望类体属性进入equals不会。copy只改部分字段却忘记其余字段的业务约束。动手练A为「风景卡片 UI」定义data class SceneryCardUi含copy切换isFavorite。D阅读Province.kt列出data class自动生成、你无需手写的方法。第 5 章 sealed class、穷尽 when 与扩展函数入门网络/本地结果常为「成功 / 失败 / 加载中」有限集合。用常量或继承易漏分支。sealed class让受限层次在编译期穷尽扩展函数在不改源码前提下为模型加行为如Province.displayTitle()。sealedinterfaceLoadStateoutT{dataobjectLoading:LoadStateNothingdataclassSuccessT(valdata:T):LoadStateTdataclassError(valmessage:String):LoadStateNothing}funTLoadStateT.isSuccess():Booleanwhen(this){isLoadState.Success-trueLoadState.Loading,isLoadState.Error-false}// when 表达式穷尽无需 else// 扩展函数接收者即 ProvincefunProvince.displayTitle():String$nameCn$abbreviation上述模型未使用sealed class但ExploreUiState可演进为sealedinterfaceExploreScreenState{dataobjectLoading:ExploreScreenStatedataclassContent(valsceneries:ListScenery):ExploreScreenStatedataclassError(valmessage:String):ExploreScreenState}UI 层when (state)强制处理 Loading/Content/Error避免「忘了 isLoading」类 bug。Scenery保留次构造 兼容 getter便于与既有调用方共存// 兼容 getter供旧调用方使用 fun getLocation(): String addressFull fun getCounty(): String district fun getCoverImageUrl(): String mainImageUrl fun getBestTimeToVisit(): String bestVisitTime fun getTravelTips(): String tips原理补充sealed子类须在同一模块、同一文件或嵌套声明Kotlin 2.x 规则以官方文档为准。扩展函数编译为静态方法displayTitle(province)无运行时开销。踩坑sealed class子类散落在多文件导致无法穷尽。扩展函数定义在Fragment内造成作用域混乱。when用else掩盖新增分支失去穷尽价值。动手练A为「收藏切换结果」定义sealed class 穷尽when。B把SceneryUtil.getDisplayName(s)工具类改为Scenery.displayName()扩展。第 6 章 综合工程模型层与 ViewModel 边界语法孤立学习无法落地。中级工程师须分清模型非空契约、导航参数可空、UI State 不可变更新三层边界。Entity/DTOdata class字段尽量非空 ↓ 映射层处理 null/默认值 UI Statedata class copy ↓ StateFlow 暴露 Compose/Fragment 只读 state事件上行层级文件要点模型Province.kt非空data class列表字段ListString模型Scenery.kt主构造非空 次构造兼容旧 APIViewModelExploreViewModel.ktSavedStateHandle可空链 UiState.copy// 用途Repository 返回 ListScenery映射层兜底可空网络字段示意funSceneryDto.toDomain():SceneryScenery(idid,nameEnnameEn,nameCnnameCn,// ...tipstips?:,// 映射层一次兜底非 UI 层重复 ?:)原理补充ViewModel 中category: Category?是合法可空业务状态无分类全量与「把可空泄漏到 UI 每个 TextView」不同。设计原则是可空有意义则保留类型可空无意义则在边界消灭。踩坑Entity 字段全标String?「以防万一」导致 UI 处处?:。ViewModel 里!!解析 Navigation 参数。data classState 含var在多处直接突变。动手练C写fun SavedStateHandle.parseCategory(): Category?行为对齐ExploreViewModel第 2830 行。D在项目中找 1 个data classUiState画出字段是否都应非空。第 7 章 编译器视角可空类型的 JVM 映射概念面试常问「Kotlin 空安全是否零成本」。需建立编译期检查 必要运行时断言的正确心智而非「Kotlin 没有 NPE」。非空类型调用前编译器插入 null 检查如checkNotNull。可空类型T?在 JVM 上仍是引用可能为 null类型区别主要在编译期。泛型可空ListString?表示列表元素可空与ListString?列表本身可空不同。从 Java 调用 Kotlin 时平台类型T!失去可空信息——互操作场景须单独注意本篇记住Java 边界是空安全体系破口须显式校验。原理补充funrequireName(name:String?){valnname?:returnprintln(n.length)// 智能转换}挂起点与状态机属于协程机制此处只需理解「可空是类型系统合约不是运行时擦除的幻觉」。踩坑认为「用了 Kotlin 就不会 NPE」——!!、Java 互操作、JNI 仍会。混淆ListString?与ListString?。动手练A解释ListString?与ListString?在 UI 绑定时的不同判空策略。第 8 章 常见错误汇总、面试强化与实战练习这些是后续学习异步与 API 选型时的类型基础典型反模式不纠正会带入全项目。团队可执行清单生产代码默认禁止!!可空字段在 Mapper 层收敛公开 API 显式返回类型UI State 用data classcopy重构优先级建议检查 ViewModel 导航参数解析是否均有失败降级。新增 DTO 优先data class字段非空 映射兜底。新 UI 状态考虑sealed interface替代isLoadingerror多布尔。原理补充suspend与状态机如何映射到Continuation属于协程机制不在本篇展开。踩坑!!滥用— 最大回退点。平台类型当非空— Java API 返回值未校验。data class可变字段— 破坏copy语义。when用else掩盖穷尽— sealed 优势丧失。可空泄漏到 UI 层— 应在 ViewModel/Mapper 收敛。动手练B重构本章前文bad(handle)去掉!!对齐ExploreViewModel可空链。D在项目中找 1 个data classUiState评估是否应改为sealed interface。面试速查 · 追问链追问链 #1空安全本质 标准回答≤200 字Kotlin 用T与T?在编译期区分可空与非空?.?:等操作符把 null 处理压缩为表达式配合智能转换减少守卫代码。不能完全消除 NPE!!、Java 互操作、JNI 仍会但把大多数问题前移到编译期。追问 1?.和?:能嵌套到什么程度答语法上可以但超过 2 层建议提取中间变量或let链否则可读性下降可结合let链整理。追问 2!!和requireNotNull区别答二者 null 都抛异常requireNotNull可带 lazy 消息且意图更明确适合不变式!!无消息且易滥用生产 UI 路径默认禁用。追问链 #2data class 与 copy 标准回答data class为数据载体自动生成 equals/hashCode/toString/copy/解构copy支持不可变更新适合 UI State。主构造属性才参与生成方法类体属性默认不参与。追问 1data class能继承普通类吗答可以继承普通类但父类不能有抽象open成员冲突规则需注意data class自身不能是 open/abstract/sealed。追问 2Room Entity 用data class有何注意答主构造属性映射列默认值与可空列一致var慎用多表关系另篇讲。追问链 #3sealed 与 when ⭐标准回答sealed限制子类集合编译器对when做穷尽检查适合 UI State、Result 等有限状态。与enum比子类可携带不同数据。追问 1为什么子类要同文件/模块答保证封闭集可知编译器才能验证穷尽分散则无法静态检查漏分支。追问链 #4Java 互操作破口 标准回答Java 类型在 Kotlin 中为平台类型T!可空信息丢失从 Java 调 Kotlin 时 Kotlin 注解可生成 JSR-305 提示。边界层应显式?:、requireNotNull或可空返回。追问 1Scenery.getTravelTips()从 Kotlin 看返回什么答非空StringKotlin 声明若 Java 侧可能返回 null应标Nullable或在 Kotlin 侧仍做防御。完整链路一句通可空类型T?与操作符→data class承载模型与 UI State→ViewModel/Mapper 边界收敛 null→工程里可预测的空安全习惯。相关推荐Android 高级工程师面试Kotlin 语法基础 近1年高频追问 22 题Android 高级工程师面试Kotlin 协程 近1年高频追问 22 题