when 这题在 Kotlin 里的地位,接近 Java 的 switch,但差别极大:Java 的 switch 是语句、能穿透、case 里必须 break;Kotlin 的 when 是表达式、不会穿透、且在穷举时能编译期查错。更关键的是它的分支条件不止"等于某个值"——支持值、区间、类型、集合成员、以及任意布尔条件。这一篇把这五种分支形态和各自的坑讲清。
先把结论放在前面:when 的核心价值是把分支逻辑表达成可赋值的表达式。五种常用形态:① 值匹配(when (code) { 200 -> ok });② 区间匹配(in 1..9 -> 正常);③ 类型匹配(is String ->);④ 集合成员(in listOf(...) ->);⑤ 任意条件(else 或 !in)。when 能否有返回值,取决于它是否被当作表达式使用——被赋值或 return 时,编译器要求所有路径都有结果,这时不写 else 就必须依赖 sealed 类型的穷举性。
机制拆解
先讲编译期行为。when 顺序求值,从上到下匹配第一个成立的分支,命中即执行,不再往下走(这一点与 Java switch 一致,但 Java 需显式 break 而 Kotlin 不需要)。当 when 作为表达式且存在 else 分支时,它总能返回值;当没有 else 时,编译器会检查是否穷尽了所有可能——对 sealed class、enum、以及基本类型(Int/Byte/Boolean/Char)Kotlin 知道取值全集,对 null 需要显式 null -> 分支,对普通类则要求有 else。
类型匹配的分支有个重要细节:is String 这种智能转换只在分支内生效。也就是说:
when (v) {
is String -> v.length; else -> 0 } // v 在此分支被当作 String
这个智能转换在 if (v is String) 里同样有效,但它有前提——v 必须是稳定的(val 或不可被并发修改的成员)。若 v 是自定义 getter 的 var,智能转换失效,需要先赋给局部变量。
集合成员匹配值得单独说:in listOf(1,2,3) 编译后是依次调用 contains,在 Kotlin 1.4 之后编译器会优化成 switch(针对 when 主体是表达式时),但对大集合或非字面量集合仍是线性查找。这是性能上容易被忽略的一点。
这些坑的正确绕法
最常见的坑是 when 作为表达式必须穷举,缺 else 编译报错,但对非密封类型穷举只是形式完备。 表现有两面:好的一面是编译期强制补全(这正是 sealed 的价值);坏的一面是对普通类写了 else -> {} 空分支后,编译器不再报错,而运行期落进空分支静默什么都不做。这比编译报错危险得多——问题被推迟到线上。修法是:when 用于业务分支时优先匹配 sealed 状态并删掉 else;确实需要对未知情况兜底时,else 分支里要显式抛异常或记日志,不要写空实现。
其次是 when 里写复杂条件做线性匹配,条件顺序有依赖。 表现是把 in 1..9 写在 in 1..5 -> 高级 之后,1~5 会先命中第一个分支,第二个分支形同虚设。根因是 when 是顺序求值,不是最长匹配也不是最具体匹配。修法是把范围更窄的分支放前面,或者改用显式的条件表达式让顺序变成显式可读的逻辑。
还有一个更隐蔽的坑:把 when 的 is 分支用在可空值上,智能转换后仍可能为空。 表现是 when (v) { is String -> v.length } 在 v 被判定为 String 时安全,但把 v: Any? 传进来时 null 分支缺失导致 else 兜底,业务上"null 应该走默认值"这个语义被淹没。另一个更常见的坑是is 判断不匹配 null——is String 对 null 返回 false,所以 null 必须单独写 null -> 分支。Android 上从 Intent 取数据、从 Bundle 取值时最容易踩这个。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 1) 五种分支形态一次看全
fun describe(code: Int, name: String?, level: Int): String = when {
code in 200..299 -> "成功" // 区间
code in listOf(408, 502, 503) -> "可重试" // 集合成员
code !in 100..599 -> "非法状态码" // 任意条件
else -> "未知"
}
fun typeName(v: Any?): String = when (v) {
// 类型 + null
null -> "空"
is Int -> "整型 ${v * 2}" // 分支内智能转换生效
is String -> "文本 ${v.length}"
is List<*> -> "列表 ${v.size}"
else -> "其他"
}
// 2) 作为表达式:每条路径都必须有值
val label: String = when (level) {
// 不写 else 也能过
1 -> "初级"
2 -> "中级"
3 -> "高级"
else -> "未知"
}
// 3) 顺序依赖:窄的分支必须放前面
fun grade(score: Int) = when {
score >= 90 -> "A" // 放前面
score >= 60 -> "B"
else -> "C"
}
// 4) sealed 状态 + 不写 else:漏分支编译报错
sealed interface NetState {
data object Idle : NetState; data class Done(val n: Int) : NetState }
fun render(s: NetState) = when (s) {
// 新增状态忘了补 → 编译失败
NetState.Idle -> "加载中"
is NetState.Done -> "完成 ${s.n}"
}
// 5) 反例:else 空实现,问题被推迟到线上
fun renderSilent(s: NetState): String = when (s) {
NetState.Idle -> "加载中"
is NetState.Done -> "完成 ${s.n}"
else -> "" // 静默吞掉
}
// 6) when 不穿透,无需 break
fun kind(n: Number) = when (n) {
1 -> "一" // 命中即返回,不会继续往下匹配
2 -> "二"
else -> "多"
}
这段代码值得盯三处:第一处,第 1 段把五种分支形态写在两个函数里,区间/集合/任意条件/类型/_null 都覆盖到;第二处,render 与 renderSilent 的一字之差——去掉 else 编译期强制补全,加上空 else 就把问题推给线上,这个对比是本篇最值得记住的;第三处,grade 里用 when { } 无参形式表达从高到低的顺序依赖,注释明确标出"窄的放前面"。
这题在面试里怎么问、怎么答
"when 和 if-else 链有什么区别?"答:①when 支持值/区间/类型/集合多形态匹配,if 只能写布尔条件;②when 可以是表达式,if 只能是语句;③when 有穷举性检查,if 没有;④when 的分支不会穿透,语义更干净。什么时候用 if?如果条件之间有复杂逻辑组合(a && b || c),when {} 无参形式反而更清楚。
"when 的集合成员匹配是 O(n) 还是 O(1)?"答:取决于集合类型。when 主体是表达式且集合是 vararg/字面量常量时,Kotlin 1.4+ 会编译成 switch(字符串常量走 hashCode 判定);对 listOf 里装非编译期常量的元素、或对 Set 之外的任何集合,仍是逐个 contains 线性查找。所以集合很大时应改用 Set 或先 toHashSet()。
"sealed 的穷举性检查是怎么实现的?"答:编译器知道 sealed 类的所有直接子类(受同文件/同模块限制),所以在 when 作为表达式且无 else 时能校验覆盖率。普通类则无法保证,于是要求有 else。
"使用中遇到过什么问题?"案例一:网络状态用 sealed 建模,但 when 写了 else -> {},新增一种 Timeout 状态后没补分支,页面卡在空白;修复为删掉 else,让编译器把遗漏点暴露出来。案例二:is 分支漏了 null,接口返回空值时走了"其他"分支,展示的是类型名而不是默认文案。
再补一个工程上值得讲清的一点:when 在 Android 上最实用的三个位置。 ①状态渲染——把 sealed 的 UiState 映射成界面动作(显示骨架屏/显示列表/显示错误),集中在一处便于审查;②错误码映射——when (code) { in 500..599 -> "服务异常" ... } 给用户可读文案,同时补日志;③多类型分支的差异化处理——比如分享渠道返回不同类型时用 is 分支分别处理。共同点是把"一堆 if 散在各处"收敛成一个可枚举的决策点,这本身就是价值。面试里能说出"when 的收益不只是语法糖,而是把决策集中成一处可审查的结构",说明理解到位了。
给正在准备面试的你
把 when 画成一张"五种分支 + 两条路径"的结构图:左侧是输入(代码/类型/状态对象),向右分出五条分支线,分别标"值匹配→equals"、"区间→in 运算符"、"类型→is + 智能转换"、"集合→contains(可能是线性)"、"任意条件→布尔表达式"。右侧汇成两个出口:一个标"作为表达式:有返回值,穷举性可检查",一个标"作为语句:没有 else 也能编过"。图下方画一个红色叉号框,写"else -> {} 空实现:把编译期检查换成线上静默失败"。这张图覆盖了本篇的知识点。
再补工程案例与踩坑——应用落点是全项目搜 when 的 else -> {} / else -> "" 空分支,逐个判断该分支是"合法兜底"还是"掩盖缺失",前者补日志后者删掉;给 is 分支补 null -> 分支;把散在多个方法里的错误码映射收敛成一处 when。
复习时别孤立刷题:sealed class——when 的穷举性检查是 sealed 存在的直接理由,两者应当一起讲。
划两句重点:when 是表达式,五种分支形态;作为表达式要穷举,删掉 else 换取编译期检查;when 顺序求值,窄分支放前面;is 不匹配 null,null 要单独分支;else -> {} 是把编译错误换成线上静默失败。
下一篇聊集合三大类与只读可变两套体系——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:区间与遍历:until、downTo-与-step
下一篇预告:集合三大类与只读可变两套体系
有任何问题欢迎在评论区留言交流。