Skip to content

流程控制 ​

Java 初级 · 基础语法

流程控制决定代码的执行顺序。你写的 if、for、switch,在字节码层面都变成了「比较 + 条件跳转 + 标签」的组合。理解分支与循环的本质,既能帮你写出正确逻辑,也能解释一些怪现象(比如 switch 为什么能匹配 String,浮点做循环条件为什么可能死循环)。

一、分支:根据条件选路 ​

if-else ​

java
int score = 85;
if (score >= 90) {
    System.out.println("优秀");
} else if (score >= 60) {
    System.out.println("及格");
} else {
    System.out.println("不及格");
}

if 编译为「比较指令 + 条件跳转」:

text
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 存成「值→地址」对,用二分查找匹配。
java
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 ​

java
// 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 的来回跳转:

text
for 循环编译示意:
    i = 0
LOOP:
    if (i >= 5) goto END    // 条件不成立就跳出
    ... 循环体 ...
    iinc i 1                 // i++
    goto LOOP                // 回到判断
END:

while 与 for 几乎一样;do-while 的区别只是条件判断在循环体之后,所以循环体至少执行一次——这正对应字节码里「判断跳转放在末尾」。

增强 for(for-each) ​

遍历数组/集合时更简洁:

java
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:可一次跳出多层嵌套循环(标签写在循环前,: 结尾)。
java
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) break outer;   // 直接跳出外层循环
    }
}

标签虽好,但别滥用

多层循环需要整体跳出时,标签很方便;但更「干净」的做法通常是把嵌套逻辑抽成一个方法,用 return 直接结束——代码结构更清晰,也避免标签带来的跳转混乱。

四、一个真实的坑:浮点做循环条件 ​

java
// ❌ 危险: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,需要小数步长就用「整数计数器 + 比例换算」:

java
// ✅ 用整数控制次数
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。

六、最佳实践 ​

  1. 明确次数用 for,不确定次数(如「读到文件尾」「收到 quit」)用 while。
  2. 新项目优先用 switch -> 表达式写法,消除贯穿 bug;需要返回值直接当表达式用。
  3. 多层循环需要整体跳出时用标签,但更推荐抽成方法用 return。
  4. 循环体过长时封装成方法,保持可读性;循环条件务必能向前推进,避免死循环。
  5. 遍历并删除元素,用迭代器 it.remove() 或 list.removeIf(...)。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 用 for 循环打印 1 到 100 中所有能被 3 整除的数(用整数计数,别用浮点)。
  2. 用 switch 新写法把数字 1-7 映射为「星期一」到「星期日」,并思考:若用 String 类型的星期名做 switch,编译器底层靠什么实现?
  3. 写一个 while 循环读取 Scanner 输入,直到输入 quit 为止;故意用浮点 d += 0.1 写个死循环加深印象,再改成整数计数修复。
  4. 用带标签的 break 实现「在 3×3 矩阵中找到第一个值为 5 的元素后立刻结束全部两层循环」,并解释若不用标签该怎么写。

本节小结

分支(if/switch)在底层是「比较 + 条件跳转」;switch 对连续值用 tableswitch、稀疏值用 lookupswitch,匹配 String 时靠 hashCode + equals 实现。循环(for/while/do-while)是 goto 的来回跳转,do-while 至少执行一次。增强 for 对集合走迭代器,遍历中别用集合自身 remove。break 跳出、continue 跳过本轮,标签用于跳出多层。浮点绝不能做循环条件——精度会让你死循环。