第29章 Kotlin语言核心:互操作、空安全、扩展、协程与Flow
各位同学,今天我们来啃一块硬骨头——Kotlin语言的核心特性。说实话,我从Java转到Kotlin的时候,心里是有点抵触的。总觉得「又学一门语言,累不累啊?」但真正用起来之后,嗯,真香。这一章我会把Kotlin最实用的五个核心点掰开揉碎讲清楚,全是项目里天天要用的东西。
本章知识体系速览
Kotlin与Java互操作 → 空安全机制 → 扩展函数与属性 → 协程深入 → Flow响应式编程
29.1 Kotlin与Java互操作:和平共处五项原则
Kotlin和Java可以无缝混编,这是它最大的杀手锏。说白了,你可以在同一个项目里,一部分用Kotlin写,一部分用Java写,两者互相调用。我接手过一个老项目,里面全是Java,我就是一点一点把新功能用Kotlin写,慢慢替换的。
核心互操作机制:
- @JvmStatic:让Kotlin伴生对象的方法变成真正的静态方法
- @JvmOverloads:自动生成Java重载方法(针对默认参数)
- 平台类型(Platform Type):Java来的类型可能是null,Kotlin用「!」标记
- @JvmName:解决Kotlin扩展函数在Java中调用时的命名冲突
我的经验:在Java中调用Kotlin的扩展函数时,函数名会变成「文件名+函数名」的形式。比如你在StringUtils.kt里定义了fun String.isEmail(),Java里要写成StringUtilsKt.isEmail(str)。我建议用@JvmName注解显式指定,不然团队里新人容易懵。
// Kotlin 侧
class User(val name: String) {
companion object {
@JvmStatic
fun createDefault(): User = User("默认用户")
}
}
// Java 侧调用
User user = User.createDefault(); // 没有@JvmStatic的话,得写 User.Companion.createDefault()
29.2 空安全:Kotlin最让我安心的设计
Java里最烦人的是什么?NullPointerException!我当年线上出过一个事故,就是因为一个接口返回的字段为null,没判空直接调方法,导致整个服务挂了。Kotlin的空安全机制,说白了就是在编译期就把这个问题扼杀掉。
空安全三件套:
| 操作符 | 含义 | 示例 |
|---|---|---|
| ? | 可空类型声明 | var name: String? = null |
| !! | 非空断言(慎用!) | name!!.length |
| ?: | Elvis操作符,提供默认值 | name?.length ?: 0 |
| ?.let | 安全调用+作用域函数 | name?.let { 处理非空逻辑 } |
避坑指南:我曾经在项目里看到有人疯狂用「!!」,觉得反正自己知道这里不会为null。结果呢?上线第三天就崩了——因为服务端改了字段名,返回了null。记住:能用?.就别用!!,能用?:就别写if else。空安全是Kotlin给你的礼物,别把它当摆设。
// 优雅的空安全写法
data class Address(val city: String?, val street: String?)
fun printAddress(address: Address?) {
val city = address?.city ?: "未知城市"
val street = address?.street ?: "未知街道"
println("地址:$city $street")
}
// 链式调用 + 空安全
address?.city?.let {
println("城市是:$it")
} ?: println("城市信息缺失")
29.3 扩展函数与属性:给别人的类加方法
扩展函数是Kotlin最酷的特性之一。你想想看,你没法修改String类的源码,但你可以给它加一个方法。我在项目中经常用扩展函数来封装工具方法,比写一堆静态工具类清爽多了。
核心语法:
// 扩展函数:给String类加一个方法
fun String.isValidPhoneNumber(): Boolean {
return this.matches(Regex("^1[3-9]\\d{9}$"))
}
// 扩展属性:给List加一个属性
val <T> List<T>.secondOrNull: T?
get() = if (size >= 2) this[1] else null
// 使用
val phone = "13800138000"
println(phone.isValidPhoneNumber()) // true
val list = listOf(1, 2, 3)
println(list.secondOrNull) // 2
我的习惯:我会把项目中常用的扩展函数放在一个叫 Extensions.kt 的文件里。比如 View的扩展(显示/隐藏)、Context的扩展(dp转px)、String的扩展(判空、脱敏)。这样团队里所有人都能复用,代码量直接砍掉30%。
29.4 协程深入:轻量级线程的正确打开方式
协程不是线程,它是「可以挂起和恢复的计算」。说白了,它让异步代码写起来像同步代码一样直观。我刚开始学协程的时候,总觉得「这不就是回调的语法糖吗?」后来深入理解了调度器和挂起函数,才发现自己太天真了。
协程三要素:
- Job:协程的句柄,可以取消、等待完成
- Dispatcher:调度器,决定协程在哪个线程执行
- Scope:协程作用域,管理协程的生命周期
| 调度器 | 用途 | 示例场景 |
|---|---|---|
| Dispatchers.Main | 主线程,更新UI | 显示网络请求结果 |
| Dispatchers.IO | IO密集型任务 | 网络请求、数据库读写 |
| Dispatchers.Default | CPU密集型任务 | 图片压缩、数据解析 |
| Dispatchers.Unconfined | 不指定线程(极少用) | 测试或特殊场景 |
// 协程典型用法
viewModelScope.launch {
// 切到IO线程做网络请求
val result = withContext(Dispatchers.IO) {
apiService.fetchUserData()
}
// 回到主线程更新UI
textView.text = result.name
}
// async/await 并发请求
viewModelScope.launch {
val user = async(Dispatchers.IO) { apiService.getUser() }
val posts = async(Dispatchers.IO) { apiService.getPosts() }
// 等待两个请求都完成
val userResult = user.await()
val postsResult = posts.await()
// 合并展示
showData(userResult, postsResult)
}
避坑指南:我曾经在Activity里直接用GlobalScope.launch,结果Activity销毁了协程还在跑,导致内存泄漏。记住:协程一定要绑定生命周期!用viewModelScope或者lifecycleScope,或者自己实现CoroutineScope。
29.5 Flow响应式编程:数据流的优雅处理
Flow是Kotlin协程家族里的响应式编程组件。它和RxJava有点像,但更轻量、更自然。说白了,Flow就是一个「异步数据流」,你可以往里面发射数据,然后别人来收集。我在做搜索框防抖、数据库监听、网络轮询这些场景时,Flow简直是神器。
Flow的核心概念:
- 冷流 vs 热流:Flow是冷流(没人收集就不执行),StateFlow/SharedFlow是热流
- 中间操作:map、filter、debounce、catch等
- 终端操作:collect、toList、first等
- 背压处理:Flow默认没有背压,用buffer或conflate处理
// 搜索框防抖示例
fun search(query: Flow<String>) = query
.debounce(300) // 防抖:300ms内没有新输入才触发
.filter { it.length >= 2 } // 过滤掉太短的查询
.map { searchApi(it) } // 调用搜索接口
.catch { emit(emptyList()) } // 异常处理
.flowOn(Dispatchers.IO) // 指定上游在IO线程执行
// 在ViewModel中使用
class SearchViewModel : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchResult: StateFlow<List<Item>> = _searchQuery
.debounce(300)
.flatMapLatest { query ->
flow { emit(repository.search(query)) }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun onQueryChanged(query: String) {
_searchQuery.value = query
}
}
我的经验:StateFlow和LiveData怎么选?我个人建议:在ViewModel里统一用StateFlow,因为它是协程原生的,支持冷启动、支持测试。LiveData在View层用用还行,但到了复杂场景(比如多个数据源合并)就力不从心了。
29.6 实战:用协程+Flow实现一个网络请求重试
光说不练假把式。我们来看一个真实场景:网络请求失败后自动重试3次,每次间隔递增。这个需求在项目中太常见了。
// 带重试机制的Flow
fun <T> Flow<T>.retryWithDelay(
maxRetries: Int = 3,
initialDelay: Long = 1000,
maxDelay: Long = 5000,
factor: Double = 2.0
): Flow<T> = this.retryWhen { cause, attempt ->
if (attempt >= maxRetries) {
false // 超过最大重试次数,不再重试
} else {
val delay = (initialDelay * Math.pow(factor, attempt.toDouble()))
.toLong().coerceAtMost(maxDelay)
delay(delay) // 等待后重试
true
}
}
// 使用
fun fetchData(): Flow<Data> = flow {
val result = apiService.getData()
emit(result)
}.retryWithDelay(
maxRetries = 3,
initialDelay = 1000
).catch {
emit(Data.error("网络异常,请稍后重试"))
}
// 在ViewModel中收集
viewModelScope.launch {
fetchData()
.flowOn(Dispatchers.IO)
.collect { data ->
_uiState.value = when {
data.isSuccess -> UiState.Success(data)
else -> UiState.Error(data.message)
}
}
}
本章核心总结:
- Kotlin与Java互操作:用注解解决兼容问题,平台类型要小心
- 空安全:?. ?: ?.let 三件套,少用!!
- 扩展函数:给已有类加方法,代码更简洁
- 协程:轻量级并发,绑定生命周期,用对调度器
- Flow:响应式数据流,适合异步数据场景
好了,这一章的内容就到这里。Kotlin的这些特性,说白了就是让你写更少的代码、出更少的bug。我个人觉得,空安全和协程是Kotlin最值得学的两个点,你只要把这两个吃透了,开发效率至少提升50%。