上篇算法面完二叉树和贪心,这篇回到Kotlin专项。美团对Kotlin考察比字节更深,不光考语法糖,还考底层——协程异常传播、Flow和Channel区别、编译器插件原理。中级岗要求用好特性,高级岗要求理解编译器层面发生了什么。
今天8道题覆盖美团Kotlin面试核心考点。
Q1:协程异常处理怎么做?CoroutineExceptionHandler管用吗?
CoroutineExceptionHandler只能捕获协程作用域内未处理的异常,且只在根协程上生效。子协程的异常向上传播到父协程,最终由根协程的handler兜底。用法:创建handler后加入CoroutineScope的上下文,scope.launch内的未捕获异常由handler接收。
SupervisorJob vs Job:SupervisorJob下子协程失败不影响兄弟协程。普通Job下任何一个子协程失败会取消整个作用域。
追问:async的异常处理?async异常封装在Deferred里,调用await()时才抛出。不await则异常被静默吞掉。
Q2:Channel和Flow有什么区别?
Channel是协程间通信的热通道——发送端send,接收端receive,类似阻塞队列。数据不会缓存,发送端和接收端必须同时在线。适合生产者-消费者模型。
Flow是冷流——只有collect时才开始执行。支持背压(backpressure)、操作符链(map/filter/flatMap)、生命周期感知。适合数据流场景。Channel是热流(创建即执行),Flow是冷流(collect才执行)。
追问:StateFlow和SharedFlow呢?StateFlow有初始值、只保存最新值、新订阅者立即收到最新值(类似LiveData)。SharedFlow支持replay缓存、多订阅者共享,更适合事件流(如Toast、导航)。
Q3:Kotlin编译器插件有哪些?怎么工作的?
Kotlin编译器插件在编译期修改AST(抽象语法树),在字节码生成前插入代码。常见的:
kotlin-parcelize:@Parcelize注解自动生成Parcelable序列化代码(writeToParcel/createFromParcel)。
kotlin-serialization:为@Serializable类生成序列化代码,比Gson/Moshi更快更安全(编译期生成,无反射)。
all-open / no-arg:让指定注解的类变为open或生成无参构造函数,Spring和Room常用。
追问:编译器插件和KSP区别?编译器插件直接操作AST,能力更强但开发复杂。KSP通过注解处理器生成代码,更易开发但只能生成新文件不能修改已有代码。
Q4:KSP是什么?比KAPT好在哪?
KSP(Kotlin Symbol Processing)是Google推出的Kotlin原生注解处理框架,替代KAPT(Kotlin Annotation Processing Tool)。
KAPT的问题:KAPT把Kotlin代码先编译成Java stub再交给Java注解处理器(如javac的APT)。这个过程慢(额外编译stub)、有兼容性问题(Kotlin特性在stub里丢失)。
KSP的优势:直接处理Kotlin代码,不需要stub生成。速度快2-5倍,完整保留Kotlin特性。Room、Hilt、Moshi等库都已支持KSP。开发者实现SymbolProcessor接口,通过Resolver获取注解符号并生成代码。
追问:项目中怎么从KAPT迁移到KSP?把kapt依赖改为ksp,确保所有注解处理器都支持KSP。Room 2.4+、Hilt 2.48+已原生支持。混合使用期间KAPT和KSP可以共存。
Q5:协程的Dispatchers有哪些?Dispatchers.Main和Dispatchers.IO有什么区别?
Dispatchers.Main:主线程,用于UI更新。Android上通过Handler.post实现。
Dispatchers.IO:IO线程池,用于网络请求、文件读写。线程池大小默认64(或CPU核心数,取大者)。
Dispatchers.Default:CPU密集线程池,用于计算任务。线程数等于CPU核心数。
Dispatchers.Unconfined:不指定线程,在调用者线程启动,挂点后在恢复线程继续。生产环境少用。
典型用法:viewModelScope.launch(Dispatchers.IO)内做网络请求,withContext(Dispatchers.Main)切回主线程更新UI。
追问:withContext和async区别?withContext切换上下文等待结果(串行),async并发启动返回Deferred,多个并行后一起await。
Q6:Kotlin的inline、noinline、crossinline是什么?
inline:函数内联——编译器把函数体直接插入调用处,消除函数调用开销。主要用于高阶函数(lambda参数),避免lambda创建对象。
noinline:inline函数里某个lambda参数不想内联时用noinline标记。noinline的lambda可以作为参数传递或存储。
crossinline:inline函数里的lambda不允许非局部return(从外层函数return),但允许局部return。用于lambda可能在其他上下文中执行(如另一个lambda里)。inline函数中crossinline标记的lambda不能非局部返回,noinline标记的lambda可以传递存储。
追问:reified关键字呢?reified让泛型在运行时保留类型信息(普通泛型在运行时被擦除)。必须配合inline使用,因为内联后编译器知道具体类型。常用于inline fun <reified T> findType(): Boolean = obj is T。
Q7:Kotlin的作用域函数let、run、apply、also、with怎么选?
let:it引用,返回lambda结果。适合判空后操作obj?.let { ... }。
run:this引用,返回lambda结果。适合对象初始化+取值。
apply:this引用,返回对象本身。适合对象配置Person().apply { name = "Tom" }。
also:it引用,返回对象本身。适合链式调用中做副作用操作(日志、校验)。
with:非扩展函数,this引用,返回lambda结果。适合对同一对象多次操作。
常见用法:User().apply { name = "张三"; age = 25 }配置对象,user?.let { viewModel.load(it.id) }判空操作。
追问:经验法则——配置对象用apply,判空用let,需要返回值用run,做日志用also。团队统一风格即可。
Q8:Kotlin的密封类和密封接口有什么用?
sealed class/interface:限制继承层次——子类必须在同一文件或同一模块中声明。编译器知道所有可能的子类,配合when表达式可以做穷举检查(不需要else分支)。
典型例子:sealed class Result包含Success(data)、Error(exception)、Loading三个子类。when(result)不需要else分支,编译器自动校验所有情况。
sealed interface(Kotlin 1.5+):比sealed class更灵活,可以多继承。适合定义状态机、事件类型。
追问:和枚举有啥区别?枚举的每个值是单例,密封类可以有多个实例且可以带不同参数。密封类比枚举更灵活但占用更多内存。
面试Tips:美团Kotlin面喜欢追问底层原理——协程异常传播机制、Channel和Flow的设计差异、编译器插件和KSP的工作区别。准备时不能只会用,还要知道为什么这么设计。inline相关(noinline、crossinline、reified)是高频考点。作用域函数和密封类相对简单但必须熟练。
下一篇进入性能优化专项——美团APM实践、列表滑动优化、图片加载优化、编译加速。
Kotlin面试被问过inline和reified的同学扣1,你能讲清楚协程异常传播吗?
本系列连载中,关注不迷路,下一篇:美团高级Android(性能优化专项)面试真题
系列简介:Android大厂面经连载,覆盖字节跳动、腾讯、阿里、美团等40+企业,从初级到架构师全岗位覆盖。每篇文章包含真实面试题+详细答案+代码示例,帮你拿到大厂Offer。