switch 多分支选择
"优雅的代码不是炫技,而是在正确的场景使用正确的结构。" —— 白歌在审查小崔的绩效评级模块时说道,"当你需要基于单个变量的多个等值进行分支时,switch 比层层 if-else 更清晰,也更高效。"
概述
当程序需要在多个互斥的选项中根据某个变量的值选择一条执行路径时,switch 语句是比 if-else if-else 更优雅的选择。JDK 8 中 switch 支持四种类型:int、char、String、enum。
switch vs if-else if-else 定位
| 维度 | switch | if-else if-else |
|---|---|---|
| 判断方式 | 等值匹配(== / equals) | 任意布尔表达式 |
| 适用场景 | 离散值多分支选择 | 范围判断、复杂条件 |
| 性能(编译期常量) | O(1) tableswitch | O(n) 逐个判断 |
| 可读性(多分支) | 优 | 差(if-else 链过长) |
语法格式
switch (表达式) {
case 常量值1:
// 语句块
break; // 跳出 switch
case 常量值2:
// 语句块
break;
...
default:
// 所有 case 都不匹配时执行
break;
}
关键点表格
| 关键点 | 说明 |
|---|---|
| 表达式类型 | JDK 8:byte/short/int/char 及其包装类、String、enum |
| case 常量 | 必须是编译期常量(字面量或 final 变量),不能重复 |
| break | 跳出 switch 语句块;若省略则发生穿透(fall-through) |
| default | 可选;所有 case 都不匹配时执行;建议放在最后 |
| JDK 7+ String 支持 | 编译器将 switch(String) 转换为基于 hashCode 的 if-else + switch(int) |
执行流程
示例一:绩效评级分类(int 类型)
白歌为飞翔科技制定了一套绩效评级规则,小崔负责在系统中实现:
/**
* 飞翔科技绩效评级——switch(int)
* 评级标准:A(90+) / B(80-89) / C(70-79) / D(60-69) / F(<60)
*/
public class PerformanceGrade {
public static void main(String[] args) {
String employee = "朱璐";
int score = 87; // 综合评分
// 将百分制转换为等级制
int gradeLevel = score / 10;
System.out.println("===== 飞翔科技绩效评级 =====");
System.out.println("员工:" + employee);
System.out.println("评分:" + score);
switch (gradeLevel) {
case 10: // 100分
case 9: // 90-99分
System.out.println("绩效等级:A — 卓越");
System.out.println("奖金系数:1.5x");
break;
case 8: // 80-89分
System.out.println("绩效等级:B — 优秀");
System.out.println("奖金系数:1.2x");
break;
case 7: // 70-79分
System.out.println("绩效等级:C — 良好");
System.out.println("奖金系数:1.0x");
break;
case 6: // 60-69分
System.out.println("绩效等级:D — 需改进");
System.out.println("奖金系数:0.8x");
break;
default: // <60分
System.out.println("绩效等级:F — 不合格");
System.out.println("奖金系数:0x(无奖金)");
break;
}
System.out.println("评级完成。");
}
}
运行输出:
===== 飞翔科技绩效评级 =====
员工:朱璐
评分:87
绩效等级:B — 优秀
奖金系数:1.2x
评级完成。
技巧:
case 10: case 9:连续写两个 case(中间无 break),利用合法的穿透实现 100 分和 90-99 分共用同一处理逻辑。
示例二:星期几判断(String 类型)
JDK 7+ 支持 switch 直接使用 String,编译器会自动转换为 int 比较。Frank 需要根据星期几安排不同的团队会议:
/**
* 飞翔科技会议室安排——switch(String)
*/
public class MeetingSchedule {
public static void main(String[] args) {
String dayOfWeek = "周三";
System.out.println("===== 飞翔科技会议安排 =====");
switch (dayOfWeek) {
case "周一":
System.out.println("上午 9:00 — 周例会(全体)");
System.out.println("下午 2:00 — 技术评审会");
break;
case "周二":
System.out.println("上午 10:00 — 产品需求讨论");
break;
case "周三":
System.out.println("下午 3:00 — 代码走查(Code Review)");
System.out.println("下午 5:00 — 技术分享(本周:JVM 调优)");
break;
case "周四":
System.out.println("上午 10:00 — 架构评审会");
System.out.println("下午 2:00 — 跨部门协调会");
break;
case "周五":
System.out.println("下午 4:00 — 团队复盘 & Happy Hour");
break;
case "周六":
case "周日":
System.out.println("周末 — 无会议安排,请享受休息日!");
break;
default:
System.out.println("无效的日期:" + dayOfWeek);
break;
}
System.out.println("会议安排查询完毕。");
}
}
运行输出:
===== 飞翔科技会议安排 =====
下午 3:00 — 代码走查(Code Review)
下午 5:00 — 技术分享(本周:JVM 调优)
会议安排查询完毕。
示例三:订单状态流转(enum 类型)
使用 enum 作为 switch 表达式是 JDK 5+ 的推荐实践——类型安全,不会传入非法值:
/**
* 飞翔科技电商订单状态——switch(enum)
*/
enum OrderStatus {
PENDING, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
DELIVERED, // 已签收
CANCELLED, // 已取消
REFUNDED // 已退款
}
public class OrderStateMachine {
public static void main(String[] args) {
OrderStatus status = OrderStatus.SHIPPED;
System.out.println("===== 飞翔科技订单系统 =====");
System.out.print("当前状态:" + status + " → ");
switch (status) {
case PENDING:
System.out.println("操作:请尽快完成支付(30分钟内有效)");
break;
case PAID:
System.out.println("操作:仓库备货中,预计24小时内发货");
break;
case SHIPPED:
System.out.println("操作:物流运输中,物流单号:SF1234567890");
System.out.println("操作:" + OrderStatus.DELIVERED + " — 用户可确认收货");
break;
case DELIVERED:
System.out.println("操作:订单已完成,支持7天无理由退货");
break;
case CANCELLED:
System.out.println("操作:订单已取消,款项将在3个工作日内退回");
break;
case REFUNDED:
System.out.println("操作:退款已到账,订单流程结束");
break;
// default 可省略——enum 覆盖了所有可能的值
}
System.out.println("状态查询完毕。");
}
}
运行输出:
===== 飞翔科技订单系统 =====
当前状态:SHIPPED → 操作:物流运输中,物流单号:SF1234567890
操作:DELIVERED — 用户可确认收货
状态查询完毕。
深度原理:tableswitch 与 lookupswitch
这是面试中区分优秀候选人的关键问题。switch 在字节码层面有两种实现方式,JVM 根据 case 的密集程度自动选择:
两种指令对比
| 特性 | tableswitch | lookupswitch |
|---|---|---|
| 适用场景 | case 值连续或接近连续 | case 值稀疏 |
| 查找方式 | 直接计算偏移量,O(1) | 二分查找,O(log n) |
| 内存占用 | 需维护完整的跳转表 | 仅存储 case 值列表 |
| 字节码结构 | 跳转表 + 默认偏移 | (key, offset) 对列表 |
示例:tableswitch(密集 case)
public static String getSeason(int month) {
switch (month) {
case 3: case 4: case 5: return "春季";
case 6: case 7: case 8: return "夏季";
case 9: case 10: case 11: return "秋季";
case 12: case 1: case 2: return "冬季";
default: return "无效月份";
}
}
反编译字节码(javap -c):
tableswitch { // 1 to 12
1: 48 // month=1 → 偏移量48
2: 48 // month=2 → 偏移量48
3: 36 // month=3 → 偏移量36
4: 36
5: 36
6: 44
7: 44
8: 44
9: 52
10: 52
11: 52
12: 48
default: 60
}
访问方式:目标偏移 = jump_table[month - low],一次数组访问即可定位,时间复杂度 O(1)。
示例:lookupswitch(稀疏 case)
public static String getPlanetOrder(int planetCode) {
switch (planetCode) {
case 1: return "水星";
case 3: return "地球";
case 7: return "天王星";
case 8: return "海王星";
case 100: return "自定义天体";
default: return "未知";
}
}
反编译字节码:
lookupswitch { // 5
1: 28 // (key=1, offset=28)
3: 34 // (key=3, offset=34)
7: 40
8: 46
100: 52
default: 58
}
JVM 选择策略:当 high - low + 1(case 范围)与 case 数量之比不太大时,使用 tableswitch;否则使用 lookupswitch 节省空间。
String switch 的底层实现
JDK 7+ 的 switch(String) 是一个语法糖,编译器将其拆解为两个阶段:
编译器将原始代码:
switch (day) {
case "周一": ...
case "周二": ...
}
转换为等效的:
int hashCode = day.hashCode();
switch (hashCode) {
case 649148: // "周一".hashCode()
if (day.equals("周一")) { ... }
break;
case 650048: // "周二".hashCode()
if (day.equals("周二")) { ... }
break;
}
这也是为什么 switch(String) 中 case 不能为 null ——
null.hashCode()会抛出NullPointerException。
易错场景
反例一:忘记 break 导致的穿透(Fall-Through)
这是 switch 语句中最高发的错误。小崔初学时写了这样的代码:
// ❌ 错误:忘记 break,导致每个 case 都"穿透"到下一个
int weekday = 3;
switch (weekday) {
case 1:
System.out.println("周一");
case 2:
System.out.println("周二");
case 3:
System.out.println("周三");
case 4:
System.out.println("周四");
case 5:
System.out.println("周五");
}
运行输出(错误):
周三
周四
周五
原因:从匹配的 case 3 开始执行,因为没有 break,执行流"穿透"进入 case 4 和 case 5,直到 switch 结束或遇到 break。
// ✅ 正确:每个 case 末尾添加 break
int weekday = 3;
switch (weekday) {
case 1:
System.out.println("周一");
break;
case 2:
System.out.println("周二");
break;
case 3:
System.out.println("周三");
break;
case 4:
System.out.println("周四");
break;
case 5:
System.out.println("周五");
break;
}
运行输出(正确):
周三
例外:有时我们需要利用穿透——比如多个 case 共享同一处理逻辑。此时应在代码注释中明确标注
// fall-through。
反例二:case 后使用了非常量表达式
// ❌ 编译错误:case 必须是编译期常量
int x = 3;
int y = 3;
switch (x) {
case y: // 编译错误:constant expression required
System.out.println("匹配");
break;
}
y 是变量,虽然值不变,但它不是编译期常量(未声明 final)。
// ✅ 正确:使用 final 常量
final int MONDAY = 1;
int day = 1;
switch (day) {
case MONDAY:
System.out.println("周一");
break;
}
反例三:switch 表达式为 null
// ❌ 运行时错误:NullPointerException
String level = null;
switch (level) { // 调用 level.hashCode() → NPE
case "P5":
System.out.println("中级");
break;
}
// ✅ 正确:先判空
String level = null;
if (level == null) {
System.out.println("职级未设定");
} else {
switch (level) {
case "P5":
System.out.println("中级");
break;
}
}
反例四:重复的 case 值
// ❌ 编译错误:duplicate case label
int score = 85;
switch (score) {
case 85:
System.out.println("优秀");
break;
case 85: // 编译错误!重复的 case 标签
System.out.println("良好");
break;
}
面试考点
Q1:switch 支持哪些数据类型?JDK 各版本的变化?
JDK 版本 支持的类型 JDK 1.0 byte, short, int, char JDK 5 增加 enum JDK 7 增加 String JDK 12+ switch 表达式( ->箭头语法,不在本章范围)不支持的类型:long、float、double、boolean。boolean 只有两个值,用 if-else 即可。
Q2:tableswitch 和 lookupswitch 的区别是什么?JVM 如何选择?
- tableswitch:为连续范围内的每个值维护跳转表项,通过
value - min直接索引,O(1) 时间。适合 case 值密集的场景。- lookupswitch:维护 (key, offset) 键值对列表,通过二分查找定位,O(log n) 时间。适合 case 值稀疏的场景。
- 选择策略:JVM 规范未强制选择策略,由编译器实现决定。一般当 case 值的范围与数量之比小于一定阈值时用 tableswitch。
Q3:switch(String) 的实现原理是什么?
编译器将其拆解为两步:首先对 String 调用
hashCode(),然后用switch(int)匹配 hashCode 值。匹配到 hashCode 后,再用equals()做二次确认以防止哈希冲突。因此,switch(String) 的性能取决于 hashCode 的分布质量。
Q4:为什么 switch 不支持 long 类型?
switch 的设计初衷是高效的跳转表(tableswitch),需要一个可快速映射为数组索引的整数键。long 的取值范围太大(2^64),无法用跳转表高效覆盖。如果确实需要对 long 做多分支,使用 if-else 或 HashMap 分发。
Q5:switch 中的 default 可以放在任意位置吗?
可以放在任意位置(开头、中间、末尾),但强烈建议放在最后。如果放在前面且没有 break,会穿透到后续 case,极易造成逻辑错误。
小结
| 要点 | 说明 |
|---|---|
| 适合等值判断 | switch 用于少量离散值的等值匹配,范围判断用 if-else |
| break 不可忘 | 除非有意利用穿透,否则每个 case 末尾加 break |
| null 先判空 | switch(expr) 中 expr 为 null 会导致 NPE |
| case 值必须唯一 | 编译器强制检查,重复会报错 |
| tableswitch vs lookupswitch | 密集用 tableswitch(O(1)),稀疏用 lookupswitch(O(log n)) |
| String 是语法糖 | 编译器展开为 hashCode + equals 两步确认 |
| enum 最安全 | 结合 enum 使用 switch 可获得编译期类型安全检查 |