第29章 Kotlin语言核心:互操作、空安全、扩展、协程与Flow

各位同学,今天我们来啃一块硬骨头——Kotlin语言的核心特性。说实话,我从Java转到Kotlin的时候,心里是有点抵触的。总觉得「又学一门语言,累不累啊?」但真正用起来之后,嗯,真香。这一章我会把Kotlin最实用的五个核心点掰开揉碎讲清楚,全是项目里天天要用的东西。

本章知识体系速览

Kotlin与Java互操作 → 空安全机制 → 扩展函数与属性 → 协程深入 → Flow响应式编程

Kotlin 核心特性知识体系 Kotlin 核心语言 Java互操作 @JvmStatic / 平台类型 空安全 ? / !! / ?: / let 扩展函数/属性 fun String.lastChar() 协程深入 launch / async / 调度器 Flow响应式 flow / collect / 背压

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%。

公众号:蓝海资料掘金营,微信deep3321