- Kotlin 空安全怎么实现??.、?:、!!、lateinit、let 分别是什么?
Kotlin 在类型层区分可空 / 不可空类型,编译期校验,从源头减少 NPE
T:不可空类型,不能赋值 null
T?:可空类型,允许 null
?. 安全调用:对象为 null 时整个表达式返回 null,不执行后面
?: Elvis 艾尔维斯运算符:左边为 null 时,返回右边兜底值
!! 非空断言:强制认为不为 null,null 时抛出 KotlinNullPointerException,项目尽量少用
lateinit var:延迟初始化,var、非空、基本类型不能用;访问前必须赋值,否则抛异常;适合 View、DI 注入
let:作用域函数,?.let{} 仅在非空时执行 lambda,it 代表对象本身
补充:Kotlin 只有几种情况会出现 NPE:!!、Java 互操作返回 null、外部 JVM 代码传入 null、手动 throw NPE。
- 协程是什么?和线程的区别?suspend 挂起函数原理?
协程是轻量级、可挂起的计算体,不是线程,运行在线程池之上;由 kotlinx-coroutines 库 + 编译器状态机实现,不是操作系统内核概念
线程:OS 内核资源,栈默认 MB 级,切换开销大;抢占式调度
协程:用户态,开销极小,几万协程可以跑在少量线程上;协作式调度,suspend 点主动让出线程,不阻塞线程
suspend:编译器标记,把函数编译成带Continuation参数的状态机;挂起时保存局部状态、返回标记COROUTINE_SUSPENDED;恢复时从上次状态继续执行
限制:suspend 函数只能在 suspend 函数 / 协程 builder 内部调用
- run、apply、let、with、also 作用域函数区别?

let/also 用 it;run/apply/with 用 this;apply/also 返回自身,其余返回 lambda 结果。这里说的lambda结果意思是lambda 大括号里面最后一行表达式的值。
- data class 数据类,和普通 class 区别?
data class 编译器自动生成:
(1)equals()、hashCode():按主构造参数比较
(2)toString():打印所有参数
(3)copy():拷贝对象,支持部分参数修改
(4)componentN():支持解构声明 val(a,b)=user
限制:
主构造至少 1 个参数;不能是 abstract、sealed、inner;自动生成只看主构造参数,类内属性不参与 equals/hashCode
- 扩展函数 / 扩展属性是什么?原理?能不能访问私有成员?成员函数和扩展同名谁优先?
扩展函数本质是静态工具方法,第一个参数是接收者类型,语法糖,没有真正修改原类源码
(1)不能访问 private、protected 成员
(2)静态分发:成员函数优先级 > 扩展函数;编译时按声明类型解析,不是运行时多态
(3)扩展属性:没有幕后字段,必须提供 get/set,不能存持久状态。
幕后字段(backing field)就是 field,对应 Java 里的成员变量,一块真实存在的内存空间,可以长期存数据。 扩展属性没有这个 field,所以它没有地方可以持久保存值。
(4)适用:工具方法、给系统类增加能力(Context.toast)
- inline、noinline、crossinline 内联函数、高阶函数?
高阶函数:参数 / 返回值包含函数类型;普通写法会生成 Function 对象,有额外对象开销。
inline:编译期把函数体 + lambda 直接拷贝到调用处,消除 lambda 对象分配,提升性能;适合小 lambda 高频调用
noinline:同一个 inline 函数里,不想内联某个 lambda,加 noinline,会生成对象
crossinline:禁止 lambda 内部 return(非局部返回),只能 return@label;适合传给其他异步回调
坑:inline 函数里面直接 return,会直接跳出外层调用函数(非局部返回)
- by lazy 委托属性,几种模式?线程安全?
val x by lazy { ... } 懒加载,第一次访问才执行 lambda 求值,之后缓存结果,只能 val
三种模式:
(1)LazyThreadSafetyMode.SYNCHRONIZED【默认】:加锁,线程安全,开销大
(2)PUBLICATION:多线程可同时执行 lambda,先计算完成的结果被缓存,无锁
(3)NONE:无锁、非线程安全,单线程场景最快
和 lateinit 对比:by lazy 是 val、懒加载、线程安全可选;lateinit 是 var、需要手动赋值、不能基础类型。
- == 和 === 的区别?
==:结构相等,调用 equals(),Kotlin 会自动做空判断;Java 的 equals
===:引用相等,判断是不是同一个对象,对应 Java ==
注意:数字缓存(-128~127)会影响 === 结果;字符串常量池同理。 即,JVM 对 Integer(装箱 Int) 有缓存池(-128 ~ 127)、字符串有字符串常量池,在这个范围内时,装箱出来会复用同一个对象,所以 === 返回 true;超出范围就新建对象,=== 返回 false。
9、Kotlin 可见性修饰符?和 Java 区别?
public(默认):全部可见
private:本类内
protected:本类 + 子类
internal:同模块(module)内可见,Java 没有这个修饰符;Java 默认是包访问权限(package-private),Kotlin 没有包私有
另外,Kotlin 类、方法默认是 final(不可继承 / 不可重写),需要加 open 才可以继承 / 重写,Java 默认可以继承。
10、sealed class 密封类,什么时候用?和 enum 区别?
密封类:子类受限,只能在同一个文件内定义;when 分支可以穷尽校验,不需要 else,适合状态、结果、事件封装(UI 状态、网络结果)
sealed:子类可以有不同属性、多实例;支持继承
enum:每个枚举是单例,属性固定
11、泛型 in /out/ * 星投影(声明处变型)
Kotlin 是声明处变型,Java 是使用处通配符
out T:协变,生产者,只读,只能输出 T,对应 Java ? extends T,如 List<out T>
in T:逆变,消费者,只写入 T,对应 Java ? super T,如 Comparator<in T>
*:星投影,List<*> 相当于 List<out Any?>,只读,不知道具体类型
12、结构化并发 Structured Concurrency
核心原则:协程生命周期由 Scope 管理,父子 Job 绑定在一起
(1)父不结束,等所有子协程完成
(2)父取消,所有子递归取消(防泄漏,ViewModelScope、lifecycleScope 就是这个目的)
(3)默认 Job:子抛异常,向上传播,父取消其他所有子;
(4)SupervisorJob /supervisorScope:子异常不传播给父,不取消兄弟协程(适合多个独立任务,比如多个并行埋点)
13、launch 和 async 区别?
launch:返回 Job,只执行、无返回值,"发后不管";异常直接向上抛
async:返回 Deferred<T>,用来获取返回结果;异常先保存在 Deferred,调用 await () 时才抛出
14、Flow、StateFlow、SharedFlow 区别?
(1)Flow:冷流,只有 collect 收集才开始生产数据;每次 collect 独立重新执行;适合单次数据流(网络请求、本地查询)
(2)StateFlow:热流,有初始值,保存最新状态;新订阅者立刻拿到最新值;自动去重相同值;适合 UI 状态(页面数据、加载状态),等价于可观察的 LiveData
(3)SharedFlow:热流,无初始值;可配置 replay、缓冲、溢出策略;适合一次性事件(toast、弹窗、导航事件)
冷流:生产者跟着消费者生命周期;热流:生产者独立运行,和 collect 无关。
15、协程取消机制?为什么是协作式取消?
协程取消是协作式,不是强制杀死:
(1)调用 cancel () 后,Job 标记为取消;只有在 suspend 函数(delay、withContext)内部会检查取消状态,抛出 CancellationException
(2)纯 CPU 死循环没有 suspend 点,不会自动停止;需要手动 ensureActive() / yield() 检测
(3)坑:不要 catch(e:Exception) 吞掉 CancellationException,会导致协程无法正常取消、内存泄漏
16、集合 List / MutableList 和 Sequence 区别?
(1)List 链式操作(map、filter):每一步都生成新集合,中间对象开销大,及早求值
(2)Sequence:懒求值,流式,只有最后终端操作(toList、forEach)才真正遍历;中间不生成临时集合;大数据量更省内存;底层是迭代器
17、companion object 伴生对象,和 Java static 区别?@JvmStatic/@JvmField
Kotlin 没有 static 关键字,companion object 是类内单例对象
(1)直接写的方法:Kotlin 里可以 Class.xxx(),Java 调用需要 Class.Companion.xxx()
(2)@JvmStatic:在伴生对象函数上加,Java 侧可以直接 Class.xxx()
(3)@JvmField:去掉 getter/setter,Java 直接访问字段
18、解构声明 componentN ()
val (name,age) = user 就是解构;编译器调用 component1()、component2();data class 自动生成 componentN,普通类自己手动实现 operator componentN。
19、@JvmOverloads 作用
给带默认参数的函数生成多个重载方法,兼容 Java 调用;Java 不支持默认参数,不加 @JvmOverloads 时 Java 只能传全参。
20、Kotlin 与 Java 核心差异
(1)空安全类型系统
(2)扩展函数、data class、密封类
(3)协程原生支持异步
(4)类默认 final,需要 open
(5)没有 checked exception
(6)区分只读集合 / 可变集合
(7)类型推断、if 是表达式、字符串模板
(8)内部类默认不是静态内部类(Java 内部类默认持有外部);Kotlin inner 关键字才是带外部引用的内部类
21、Channel 是什么?和 Flow 区别?背压
Channel
是协程之间的热流、队列,用来协程之间通信(生产者 - 消费者),类似阻塞队列,挂起式的。
- 热的:创建之后就开始发数据,有没有人接收都可以发
- 有缓冲区,支持背压:发得快、收得慢时,数据存在 buffer;满了生产者挂起
- 一对一 / 多对多:SendChannel、ReceiveChannel,可以多个发、多个收
- 基础 API:send () /receive (),都是挂起函数
Flow
冷流:只有调用 collect 收集的时候,才开始执行、发射数据;不收集就不跑。
- 每一次 collect,都会重新走一遍上游代码
- 背压:Flow 默认有背压策略(buffer、conflate、flowOn 等),是面向数据流转换(map/filter/transform),适合 UI 数据流
- 一般是单订阅(除非 SharedFlow/StateFlow)
一句话总结:协程之间互相发消息用 Channel;UI 订阅数据流、链式转换用 Flow
**22、**flowOn、withContext 线程切换区别
withContext(Dispatcher):切换当前整个协程上下文 ,上下游一起切,是挂起函数。 作用:在协程内部,切换当前这段代码的线程;上下游都受影响
flowOn(Dispatcher):只改变它上游所有 Flow 的分发器,下游不受影响! flowOn 是 Flow 的操作符,不是挂起函数。上游单独跑在指定调度器,下游继续沿用原来的上下文。 可以多次 flowOn,就近生效,只管自己上游。
23、CoroutineDispatcher 四个:Main、IO、Default、Unconfined
- Dispatchers.Main:主线程(UI 线程),Android 用来更新 UI;底层是 Handler
- Dispatchers.IO:IO 线程池,专门文件读写、网络、数据库、阻塞 IO;线程数量可扩容
- Dispatchers.Default:CPU 密集计算(排序、解析、大量循环),线程数≈CPU 核心数
- Dispatchers.Unconfined :无约束调度器。
- 一开始在调用它的线程执行;遇到挂起恢复后,在恢复的那个线程继续跑
- 不绑定固定线程池,极少用,一般业务不推荐;容易线程乱飞
24、CoroutineExceptionHandler 生效条件
CoroutineExceptionHandler 协程异常处理器
生效规则:
- 只对根协程(顶级 launch)生效
- 子协程异常会向上传播,父协程的 handler 捕获;子协程自己设置的 handler 无效
- async 不会触发 CoroutineExceptionHandler! async 的异常不会自动抛出,异常藏在 Deferred 里面,只有调用 await () 的时候才抛出异常,需要自己 try-catch await
- 协程取消(CancellationException)不算异常,不会进这个 handler,是正常取消
补充:SupervisorJob 可以让子协程失败不搞崩兄弟协程
25、suspendCancellableCoroutine 封装回调
作用:把 Java 回调风格的异步 API,转成挂起函数 suspend,是桥接老回调代码和协程的核心工具。 核心要点:
- 包在 suspend 函数内部,阻塞(挂起)当前协程,不阻塞线程
cont.resume(value)→ 成功返回cont.resumeWithException(e)→ 抛出异常- 必须处理取消!
cont.isCancelled,并且注册cont.invokeOnCancellation协程取消的时候,要取消底层回调 / 请求,防止回调回来 resume 已经取消的协程(崩溃)
26、SupervisorJob 和 Job 区别?
Job 一个子协程异常,全部子协程取消;SupervisorJob 子协程失败,不影响其他兄弟协程。
27、cancel 是怎么工作的?
协程取消是协作式,不是强制杀线程;需要在挂起点检查 isActive,或者用 yield、delay 等挂起函数才会响应取消。
ok. 整理了这些。有的知识点不熟,还需要研究下。