Java 流程控制:用紅綠燈程式學會做決定與重複
從 if/else、switch 到三種迴圈、enhanced for 與 label,並對照 Python,深入現代 switch 運算式與字串 switch 的 JVM 實作
用一段紅綠燈程式,看懂 Java 怎麼「做決定」
想像你要寫一支控制路口紅綠燈的程式:綠燈走、紅燈停、黃燈減速,而且要根據時段(尖峰、離峰、深夜)切換不同的秒數。這支程式的骨架,幾乎全由「流程控制(control flow)」撐起來——判斷現在是什麼狀態、決定接下來做什麼、重複跑這個循環直到斷電為止。
如果你讀過本專區的 Python 篇,這些概念你並不陌生:條件、迴圈、跳轉。但 Java 把同樣的事情做得很不一樣。Java 是靜態強型別(statically & strongly typed)、執行在 JVM(Java Virtual Machine) 上、骨子裡是 OOP-first 的語言。它要求你在編譯期就把型別講清楚,每個區塊都用大括號 {} 框起來,條件式必須是真正的 boolean——不能像 Python 那樣丟一個非零整數進去當「真」。這些限制不是找麻煩,而是 Java 在大型企業系統裡換來可預測性與工具支援的代價。
接下來我們就用一連串可以直接編譯執行的程式碼,把 Java 的流程控制走過一遍,並且隨時跟你熟悉的 Python 對照,讓你看見差異在哪。

if / else:條件必須是貨真價實的 boolean
最基本的決策是 if。語法跟很多語言類似,但有兩個 Java 的堅持要先記住:條件放在 () 裡、區塊用 {} 框住。
public class TrafficDemo {
public static void main(String[] args) {
int speed = 72;
int limit = 60;
if (speed > limit) {
int over = speed - limit;
System.out.println("超速 " + over + " 公里,請減速");
} else if (speed == limit) {
System.out.println("剛好在速限上");
} else {
System.out.println("速度正常");
}
}
}
// 輸出:超速 12 公里,請減速
這裡有個初學者最常踩的雷:Java 的 if 條件必須是 boolean。
int count = 3;
// if (count) { ... } // ❌ 編譯錯誤:int 不能當 boolean
if (count != 0) { /* ✅ 正確 */ }
相較於 Python 可以寫 if count:(非零即真),Java 把這條路堵死了。原因是 Java 認為「整數」和「真假」是兩種不同概念,混用容易出錯。同樣地,if (a = b) 這種把比較 == 誤打成指派 = 的經典 bug,在 Java 裡只要 b 不是 boolean 就會直接編譯失敗——這是靜態型別替你擋下的一刀。
單行陳述其實可以省略大括號,但強烈建議永遠保留 {},這是業界慣例:
// 反模式:省略大括號,日後加第二行很容易出錯
if (speed > limit)
System.out.println("超速");
System.out.println("這行其實永遠會執行!"); // 不在 if 內,縮排騙了你
// 慣例寫法
if (speed > limit) {
System.out.println("超速");
}
Java 不像 Python 用縮排決定區塊,縮排對編譯器毫無意義——上面那段「騙人」的縮排正是著名的 goto fail 類型漏洞來源。請養成隨手補上 {} 的習慣。
switch:從多重 if 進化而來
當你要對「同一個變數的多種可能值」分流時,連續的 else if 會很冗長。傳統 switch 陳述(statement)是解法之一:
public class LightDemo {
public static void main(String[] args) {
char light = 'Y';
switch (light) {
case 'R':
System.out.println("停");
break;
case 'Y':
System.out.println("減速");
break;
case 'G':
System.out.println("通行");
break;
default:
System.out.println("號誌故障");
}
}
}
// 輸出:減速
注意每個 case 後面那個 break。傳統 switch 有個惡名昭彰的特性叫 fall-through(貫穿):如果忘了 break,程式會「掉」進下一個 case 繼續執行。這常常是 bug,但偶爾也能拿來合併條件:
char grade = 'B';
switch (grade) {
case 'A':
case 'B':
case 'C':
System.out.println("及格"); // A、B、C 都會落到這裡
break;
default:
System.out.println("不及格");
}
// 輸出:及格
傳統 switch 接受的型別有限:int、char、enum,以及——這點很多人不知道——String(Java 7 之後)。我們會在深入段揭開字串 switch 背後的機關。
for / while / do-while:三種重複的姿勢
迴圈讓程式重複做事。Java 提供三種傳統迴圈,先看計數最常用的 for:
// 印出 1 到 5
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
System.out.println();
// 輸出:1 2 3 4 5
for 的三個區段是「初始化;條件;更新」。注意 int i 宣告在迴圈裡,它的作用域(scope)就限定在這個 for 之內,出了迴圈 i 就消失了——這跟 Python 的迴圈變數會洩漏到外層不同,是 Java 比較嚴謹的地方。
while 在條件先成立時才執行,適合「不確定要跑幾次」的情境:
int countdown = 3;
while (countdown > 0) {
System.out.println("倒數 " + countdown);
countdown--;
}
System.out.println("發車!");
// 輸出:
// 倒數 3
// 倒數 2
// 倒數 1
// 發車!
do-while 則是「先做一次,再檢查條件」,所以至少會執行一次。這在「先讀一筆輸入再判斷要不要繼續」的場景很實用:
int n = 10;
do {
System.out.println("這行一定會印一次,即使條件不成立");
} while (n < 5); // 條件為 false,所以只跑一次
// 輸出:這行一定會印一次,即使條件不成立
Python 沒有 do-while,要靠 while True 加 break 模擬;Java 把它做成內建語法。別忘了 do-while 結尾的那個分號 ;,漏掉會編譯錯誤。
enhanced for:走訪集合的優雅寫法
當你只是想「把陣列或集合裡每個元素拿出來看一遍」,傳統 for 配索引顯得囉嗦。Java 5 引入 enhanced for(也叫 for-each):
String[] colors = {"紅", "黃", "綠"};
// 傳統 for:要管 index,容易越界
for (int i = 0; i < colors.length; i++) {
System.out.println(colors[i]);
}
// enhanced for:直接拿元素,乾淨又不會越界
for (String c : colors) {
System.out.println(c);
}
那個冒號 : 讀作「in」——for (String c : colors) 就是「對 colors 裡的每個 String c」。這在語意上對應 Python 的 for c in colors:。
enhanced for 適用於任何陣列,以及任何實作了 Iterable 介面的集合(如 List、Set):
import java.util.List;
List<Integer> scores = List.of(85, 92, 78);
int sum = 0;
for (int s : scores) {
sum += s;
}
System.out.println("總分:" + sum);
// 輸出:總分:255
它的限制:enhanced for 只能「讀」,拿不到索引,也不能在走訪時安全地刪除元素(會丟 ConcurrentModificationException)。需要索引或要邊走邊改時,回頭用傳統 for 或迭代器(Iterator)。
break / continue / label:精準控制跳轉
break 跳出整個迴圈,continue 跳過本次剩下的部分、直接進入下一輪:
for (int i = 1; i <= 10; i++) {
if (i == 4) continue; // 跳過 4
if (i == 7) break; // 到 7 就停
System.out.print(i + " ");
}
// 輸出:1 2 3 5 6
預設情況下 break/continue 只作用於最內層的迴圈。但如果你在巢狀迴圈裡想一次跳出好幾層,Java 提供了一個許多語言沒有的工具——標籤(label):
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i * j == 4) {
System.out.println("命中 i=" + i + ", j=" + j);
break outer; // 直接跳出外層迴圈
}
}
}
// 輸出:命中 i=2, j=2
outer: 就是貼在外層迴圈上的標籤,break outer; 一口氣跳出整個外層。continue label; 同理可用來跳到外層的下一輪。
這是 Java 對「結構化 goto」需求的官方解法。請節制使用:標籤跳轉會讓控制流變難讀,多數情況用一個 boolean 旗標或把巢狀邏輯抽成方法(return 出去)會更清楚。相較於 Python 完全沒有 label,這算是 Java 保留的一點低階彈性。
動手寫一段:FizzBuzz 路口號誌計數器
把上面學到的東西組起來,寫一支完整可執行的小程式。規則:印 1 到 15,遇 3 的倍數印「紅」、5 的倍數印「黃」、同時是 3 和 5 倍數印「紅黃」,其餘印數字本身。
public class FizzBuzzLight {
public static void main(String[] args) {
for (int i = 1; i <= 15; i++) {
if (i % 15 == 0) {
System.out.println(i + " -> 紅黃");
} else if (i % 3 == 0) {
System.out.println(i + " -> 紅");
} else if (i % 5 == 0) {
System.out.println(i + " -> 黃");
} else {
System.out.println(i + " -> " + i);
}
}
}
}
// 輸出:
// 1 -> 1
// 2 -> 2
// 3 -> 紅
// 4 -> 4
// 5 -> 黃
// 6 -> 紅
// 7 -> 7
// 8 -> 8
// 9 -> 紅
// 10 -> 黃
// 11 -> 11
// 12 -> 紅
// 13 -> 13
// 14 -> 14
// 15 -> 紅黃
存成 FizzBuzzLight.java,在終端機執行 javac FizzBuzzLight.java 編譯、再 java FizzBuzzLight 執行。注意 Java 的鐵律:public 類別名稱必須跟檔名一模一樣(包含大小寫),否則 javac 會直接拒絕。這個「一個 public 類別對一個檔案」的規定,正是 Java OOP-first 設計的縮影——所有程式碼都得住在類別裡,連 main 也不例外。試著把判斷順序顛倒(先檢查 % 3),看看為什麼「15 的倍數」一定要放最前面。
常見錯誤:初學者最容易踩的五個雷
-
條件式用整數而非 boolean:
if (count)在 Java 不合法,要寫if (count != 0)。把==誤打成=在多數情況也會被編譯器擋下,這是靜態型別的紅利。 -
switch 忘了 break:少了
break會 fall-through 貫穿到下個case。除非你刻意要合併條件,否則每個 case 結尾都該有break。若用現代 switch 運算式(見下節),這個問題從根本消失。 -
字串比較用
==:if (name == "Java")比的是物件參考(reference),不是內容!比較字串內容一律用.equals():if (name.equals("Java"))。這是 Java 新手最痛的一刀,因為有時==會「碰巧」因字串池(string pool)而成立,讓 bug 時隱時現。 -
enhanced for 裡修改集合:走訪
List時呼叫list.remove(...)會丟ConcurrentModificationException。要刪除請用Iterator.remove()或removeIf()。 -
迴圈變數作用域誤解:
for (int i...)的i出了迴圈就不存在。想在迴圈外用到結果,得把變數宣告在迴圈之外。
深入探討(研究所視角)
switch 運算式:從「陳述」到「運算式」的典範轉移
前面教的是傳統 switch 陳述(statement)——它執行動作但不回傳值。從 Java 14 起正式定案的 switch 運算式(switch expression) 是一次語言設計上的躍進,它回傳一個值,並用箭頭 -> 取代 case: 加 break:
char light = 'Y';
String action = switch (light) {
case 'R' -> "停";
case 'Y' -> "減速";
case 'G' -> "通行";
default -> "故障";
};
System.out.println(action);
// 輸出:減速
這個寫法有三個關鍵改良。第一,沒有 fall-through:箭頭語法不會貫穿,徹底消滅「忘記 break」這類 bug。第二,它是運算式,可以直接賦值給變數或當作回傳值,比起傳統寫法需要先宣告變數再在每個 case 裡賦值,簡潔且不易遺漏。第三,編譯器會做窮盡性檢查(exhaustiveness):當 switch 的對象是 enum 時,若你漏掉某個列舉值且沒寫 default,編譯器會報錯——這把「漏處理某情況」的風險從執行期提前到編譯期攔截。
多行邏輯可用 yield 回傳值:
int code = 2;
String level = switch (code) {
case 1 -> "低";
case 2 -> {
String s = "中";
yield s + "(需注意)"; // 區塊用 yield 回傳
}
default -> "高";
};
System.out.println(level);
// 輸出:中(需注意)
從教育角度看,這呼應了函數式程式設計「以運算式為核心」的思維——相較於 Python 直到近年才加入有限的 match 模式比對,Java 的 switch 運算式在型別安全與窮盡檢查上走得更遠,這正是靜態型別語言的價值所在。
字串 switch 的底層實作:兩段式 hashCode 比對
switch 能接受 String 看似理所當然,但 JVM 的 switch 位元組碼指令——tableswitch 與 lookupswitch——只認整數。那字串怎麼辦?答案是編譯器在背後做了一次巧妙的「去糖(desugar)」轉換。
當你寫:
String cmd = "stop";
switch (cmd) {
case "go": System.out.println("前進"); break;
case "stop": System.out.println("停止"); break;
default: System.out.println("未知");
}
javac 會把它編譯成兩段式比對,概念上等同於:
// 編譯器自動生成的等價邏輯(示意)
int idx = -1;
switch (cmd.hashCode()) { // 第一段:用 hashCode 快速分流
case 3304: // "go".hashCode()
if (cmd.equals("go")) idx = 0;
break;
case 3540994: // "stop".hashCode()
if (cmd.equals("stop")) idx = 1;
break;
}
switch (idx) { // 第二段:用整數 index 跳到真正的分支
case 0: System.out.println("前進"); break;
case 1: System.out.println("停止"); break;
default: System.out.println("未知");
}
為什麼要分兩段?因為 hashCode() 可能碰撞(collision)——兩個不同字串算出同一個雜湊值。所以第一段先用 hashCode 把候選快速縮小(這步可用 lookupswitch 在接近 $O(1)$ 完成),再用 equals() 做精確比對確認字串內容真的相符,最後才用一個乾淨的整數 index 跳到目標分支。
這個設計揭示了一個重要觀念:字串 switch 並不比一串 if-else equals 神奇地快多少,它的優勢主要來自 hashCode 預先分桶,避免逐一比對每個 case。也因此,這也解釋了為什麼 switch 的 case 字串必須是編譯期常數(compile-time constant)——編譯器要在編譯當下就能算出每個字串的 hashCode,才能生成上面那張跳轉表。
理解這層轉換,你就明白了 Java「語法糖(syntactic sugar)」的哲學:表面給你方便的寫法,底層仍忠實地映射到 JVM 那套只認整數跳轉的指令集。這種「高階語法 → 編譯期轉換 → 位元組碼 → JVM 執行」的多層結構,配合自動記憶體回收(garbage collection)與龐大的企業生態系,正是 Java 能在大型系統裡屹立不搖的根基。下次當某個語法用起來特別順手時,不妨想想:編譯器替你在背後做了什麼?