第075篇 when 表达式:比 switch 强在哪里

简介: Kotlin 的 `when` 是强大表达式,远超 Java `switch`:支持值、区间、类型、集合、任意条件五种分支;具备智能转换、顺序匹配、穷举检查(对 sealed/enum 编译期报错);作为表达式需覆盖所有路径,慎用空 `else`。核心价值:将分支逻辑结构化、可赋值、可审查。

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

下一篇预告:集合三大类与只读可变两套体系

有任何问题欢迎在评论区留言交流。

相关文章
|
13小时前
|
存储 安全 编译器
第070篇 扩展函数原理:它到底是不是给类加方法
Kotlin扩展函数本质是**静态方法**,编译后以接收者为首个参数,置于文件类中;调用依**静态类型决议**,不参与多态,**不访问private成员**,且**成员方法永远优先于同名扩展**——它是语法糖,而非真正的方法增强。
21 1
|
15小时前
|
Java 编译器 Android开发
第014篇 重载与重写:编译期与运行期的分野
重载与重写是Java多态的两大基石:重载发生于同一类,编译期按参数列表静态绑定;重写发生于父子类,运行期按实际类型动态分派。二者易混淆,但本质分属编译期与运行期,规则、边界与典型坑(如忘加`@Override`、static隐藏、泛型擦除冲突)须清晰辨析。
20 0
|
15小时前
|
安全 Java 编译器
第011篇 this 与 super:最容易混淆的两个指向
面试官爱考`this`与`super`,因它是一面“能力试纸”:背概念者答定义,用过者讲场景,踩过坑者补边界。本文从原理、实战、避坑三维度拆解——`this`指当前实例,用于解遮蔽、构造转发、链式调用;`super`指父类部分,专用于调父构、访父成员。
19 0
|
13小时前
|
SQL Java 编译器
第040篇 volatile:可见性、有序性与禁止重排
`volatile` 是Java轻量级同步机制,核心解决**可见性**与**有序性**(靠内存屏障实现),但**不保证原子性**。典型适用:状态标志、引用发布、DCL单例;禁用场景:`count++`、多变量一致性、检查后动作——这些须用原子类或锁。
15 0
|
16小时前
|
存储 SQL 安全
第006篇 String 不可变性:为什么字符串要设计成不可变
Android面试高频考点“String不可变性”,远不止“被final修饰”这么简单。它关乎源码设计(final类+私有不可变char数组)、内存优化(常量池复用)、线程安全(天然无锁共享)及工程实践(避免+=拼接、必用equals比较)。理解透,才能避开静默bug、性能陷阱与并发误区。
16 0
|
16小时前
|
Java API Android开发
第004篇 循环 for/while/do-while:遍历与终止的工程课
Android面试中,循环看似简单,实则暗藏细节:终止条件、步长控制、对象分配、GC压力、快速失败机制、浮点误差、多层跳出等均是高频追问点。掌握for/while/do-while本质差异、增强for底层原理及工程避坑(如遍历中禁用list.remove),方能稳过此关。
15 0
|
14小时前
|
消息中间件 Java 调度
第038篇 Thread 与 Runnable:线程生命周期全解
Thread与Runnable本质是“任务”与“执行单元”的分离:Runnable仅定义要做的事(无返回、不抛检异常),Thread负责调度执行(含状态、优先级、生命周期控制)。`start()`才真正启新线程,`run()`只是普通方法调用。常见坑包括误调`run`导致伪并发、异常静默终止、持有Activity引发内存泄漏。工程中应优先使用线程池而非裸Thread。
18 0
|
16小时前
|
安全 Java 编译器
第003篇 流程控制 if-else 与 switch:分支逻辑规范写法
Android面试高频题:if-else与switch如何选?关键不在语法,而在场景——卫语句早返回保主逻辑扁平,switch表达式(箭头语法)防穿透、提可读;String判空需前置,枚举漏分支无警告。真懂=讲清“什么场景用、为什么这样用、踩过什么坑”。
16 0
|
14小时前
|
缓存 JSON Java
第035篇 反射基础:Class 对象与运行时类型信息
Java反射是运行期类型自省机制,通过Class对象动态获取并操作类成员(Method/Field/Constructor),支撑注解处理、DI、序列化等框架。需注意:`getDeclaredXxx`获取全部成员(含私有),`setAccessible(true)`突破访问限制(JDK9+受模块系统约束),`invoke`返回Object需手动强转,且应缓存Method以避免重复查找开销。
16 0
|
13小时前
|
缓存 安全 Android开发
第065篇 Elvis 运算符与 let:空值处理的组合拳
Kotlin 中 `?:`(Elvis)与 `let` 是被低估却极富表现力的空安全利器:`?:` 是懒求值表达式,专用于优雅兜底或提前返回;`let` 是内联作用域函数,在非空时以参数形式安全处理值。二者常组合为 `x?.let { ... } ?: default`,将“有则处理、无则兜底”升华为可组合、可读、零开销的声明式逻辑。关键在理解求值时机与意图选型——缺失用 `?:`,非空处理用 `let`,状态语义复杂时请用 sealed class 显式建模。
16 0