流程控制
Java 初级 · 基础语法
流程控制决定代码的执行顺序。你写的 if、for、switch,在字节码层面都变成了「比较 + 条件跳转 + 标签」的组合。理解分支与循环的本质,既能帮你写出正确逻辑,也能解释一些怪现象(比如 switch 为什么能匹配 String,浮点做循环条件为什么可能死循环)。
一、分支:根据条件选路
if-else
int score = 85;
if (score >= 90) {
System.out.println("优秀");
} else if (score >= 60) {
System.out.println("及格");
} else {
System.out.println("不及格");
}if 编译为「比较指令 + 条件跳转」:
iload score
bipush 90
if_icmplt ELSE_IF // score < 90 则跳到 else if
... 优秀分支 ...
ELSE_IF: ...也就是说,所谓分支,就是「算条件 → 不成立就 goto 到另一段字节码」。JVM 没有专门的 if 指令,全是靠比较 + 跳转拼出来的。
switch:两种字节码实现
switch 在字节码里有两种实现,JVM 按 case 的「密度」自动选:
tableswitch:当 case 值连续(如 1,2,3,4),JVM 建一张跳转表,O(1) 直接定位,性能极好。lookupswitch:当 case 值稀疏(如 1,10,100),JVM 存成「值→地址」对,用二分查找匹配。
int day = 2;
String name = switch (day) {
case 1 -> "周一";
case 2 -> "周二";
case 7 -> "周日";
default -> "无效";
};Java 14+ 的 switch 表达式
新写法 case L -> 表达式 自带「不贯穿」语义,不用写 break;还能作为表达式返回值(如上 name 直接接结果)。旧写法 case L: ... break; 若漏 break 会「贯穿」执行后续分支,是经典 bug 来源,新项目优先用 ->。
switch 为什么能匹配 String?(原理)
int 的 switch 好理解,但 String 在字节码里没有 switchstring 指令。编译器偷偷做了变换:
因为 String.hashCode() 返回 int,所以编译器先用 hashCode 做一次 switch(int),再用 .equals() 在可能产生哈希碰撞的 case 间做精确区分。这就是为什么 String 能进 switch,以及为什么常量必须 final 且编译期可知——它要被当作 hashCode 和字面量固化进字节码。
二、循环:重复执行的本质
for / while / do-while
// for:初始化; 条件; 步进
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
// while:先判断后执行
int n = 3;
while (n > 0) {
System.out.println(n--);
}
// do-while:先执行一次,再判断(至少跑一轮)
int x = 0;
do {
System.out.println(x);
} while (x > 0); // 条件假,但上面已经执行过一次循环在底层也是 goto 的来回跳转:
for 循环编译示意:
i = 0
LOOP:
if (i >= 5) goto END // 条件不成立就跳出
... 循环体 ...
iinc i 1 // i++
goto LOOP // 回到判断
END:while 与 for 几乎一样;do-while 的区别只是条件判断在循环体之后,所以循环体至少执行一次——这正对应字节码里「判断跳转放在末尾」。
增强 for(for-each)
遍历数组/集合时更简洁:
int[] arr = {1, 2, 3};
for (int v : arr) { // 编译器翻译成普通 for + 索引
System.out.println(v);
}对数组,编译器把它翻译成带索引的 for;对 Iterable 集合,翻译成 iterator() + hasNext() + next()。注意:for-each 内部用迭代器,所以不能在遍历中通过集合自己的 remove 删元素(会 ConcurrentModificationException),要用迭代器的 remove 或 removeIf。
三、break / continue / 标签
break:跳出当前循环。continue:跳过本轮剩余代码,进入下一轮条件判断。- 带标签的 break:可一次跳出多层嵌套循环(标签写在循环前,
:结尾)。
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) break outer; // 直接跳出外层循环
}
}标签虽好,但别滥用
多层循环需要整体跳出时,标签很方便;但更「干净」的做法通常是把嵌套逻辑抽成一个方法,用 return 直接结束——代码结构更清晰,也避免标签带来的跳转混乱。
四、一个真实的坑:浮点做循环条件
// ❌ 危险:d 永远到不了精确的 1.0
for (double d = 0; d != 1.0; d += 0.1) {
System.out.println(d);
}因为 0.1 二进制无法精确表示(见变量一节),d 累加后和 1.0 总有微小误差,d != 1.0 永远为 true,循环停不下来。循环计数一律用 int,需要小数步长就用「整数计数器 + 比例换算」:
// ✅ 用整数控制次数
for (int i = 0; i <= 10; i++) {
double d = i * 0.1; // 0.0, 0.1, ..., 1.0
}五、常见误区与注意点
WARNING
switch旧写法漏break:会「贯穿」执行后续分支,新写法用->可彻底避免。- 浮点作为循环条件:如
d != 1.0会因精度永不结束,用整数计数。 while忘记更新条件变量:while (n > 0)里不改n就是死循环。break/continue混淆:break完全跳出当前循环,continue只跳过本轮。- for-each 中改集合结构:直接
list.remove(x)会ConcurrentModificationException。
六、最佳实践
- 明确次数用
for,不确定次数(如「读到文件尾」「收到 quit」)用while。 - 新项目优先用
switch ->表达式写法,消除贯穿 bug;需要返回值直接当表达式用。 - 多层循环需要整体跳出时用标签,但更推荐抽成方法用
return。 - 循环体过长时封装成方法,保持可读性;循环条件务必能向前推进,避免死循环。
- 遍历并删除元素,用迭代器
it.remove()或list.removeIf(...)。
📚 官方文档与参考资料
- Java SE 21 文档:https://docs.oracle.com/en/java/javase/21/
- Java 语言规范 · 语句:https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html
- OpenJDK 项目:https://openjdk.org/
✏️ 练习与自测
- 用
for循环打印 1 到 100 中所有能被 3 整除的数(用整数计数,别用浮点)。 - 用
switch新写法把数字 1-7 映射为「星期一」到「星期日」,并思考:若用String类型的星期名做switch,编译器底层靠什么实现? - 写一个
while循环读取Scanner输入,直到输入quit为止;故意用浮点d += 0.1写个死循环加深印象,再改成整数计数修复。 - 用带标签的
break实现「在 3×3 矩阵中找到第一个值为 5 的元素后立刻结束全部两层循环」,并解释若不用标签该怎么写。
本节小结
分支(if/switch)在底层是「比较 + 条件跳转」;switch 对连续值用 tableswitch、稀疏值用 lookupswitch,匹配 String 时靠 hashCode + equals 实现。循环(for/while/do-while)是 goto 的来回跳转,do-while 至少执行一次。增强 for 对集合走迭代器,遍历中别用集合自身 remove。break 跳出、continue 跳过本轮,标签用于跳出多层。浮点绝不能做循环条件——精度会让你死循环。