學完本章你將能夠:
使用 if、else if、else 依條件讓程式走不同的路。
使用傳統 switch 與箭頭語法的 switch,並避開落穿陷阱。
依場景選用 while、do-while 與 for 三種迴圈。
使用巢狀迴圈處理二維問題,例如九九乘法表。
使用 break 與 continue 精準控制迴圈的進行。
綜合運用條件與迴圈,寫出完整的猜數字遊戲。
到目前為止,我們寫的程式都是「一本道」:從 main 的第一行走到最後一行, 每次執行的路線一模一樣。真實世界的程式顯然不是這樣——密碼對了才能登入、 餘額不足要擋下提款、名單有幾個人就寄幾封信。讓程式「看狀況做事」與 「重複做事」的語法,統稱流程控制(control flow),它是程式從「計算機」 進化成「有邏輯的系統」的關鍵一步。
所有程式的流程都由三種結構組成:循序(由上而下依序執行,我們 一直在用)、選擇(依條件走不同分支,即 if 與 switch)、 重複(滿足條件時反覆執行,即迴圈)。理論上,任何複雜的程式 邏輯都能用這三種結構拼裝出來。本章依序攻克後兩種。
if:讓程式學會判斷
|if| 的語法直白如英文:如果(if)條件成立,就執行大括號裡的 程式;搭配 |else if| 與 |else| 可以串出多路分支。條件 必須是 boolean 運算式——上一章的比較與邏輯運算子在此全面上場。
/** * 成績等第判定:多路分支的標準示範。 */ public class GradeDemo
public static void main(String[] args)
int score = 87;
// 由高到低逐一檢查:第一個成立的分支執行,其餘全部跳過 if (score >= 90) System.out.println("甲等:表現優異!"); else if (score >= 80) System.out.println("乙等:表現良好。"); else if (score >= 70) System.out.println("丙等:還不錯,繼續努力。"); else if (score >= 60) System.out.println("丁等:低空飛過。"); else // 前面全部不成立才會走到這裡 System.out.println("不及格:需要加強。");
乙等:表現良好。
87 分同時滿足 |score >= 80|、|score >= 70| 與 |score >= 60|,但只印出「乙等」——因為多路分支是由上 而下檢查,命中一個就結束。這也代表條件的順序至關重要: 如果把 |score >= 60| 放在最上面,任何及格分數都會被它攔截, 後面的分支永遠沒有出場機會。設計多路分支時,把「最嚴格的條件」放 最前面。
即使分支裡只有一行程式,大括號也不要省略。省略大括號的 |if| 只管轄「緊接著的一個敘述」,日後有人在下面加一行程式, 就會意外脫離 if 的管轄範圍——這是史上多次重大資安漏洞的成因。 花兩個字元買保險,穩賺不賠。
巢狀 if 與衛述句
if 裡面還可以再放 if,稱為巢狀(nested)。巢狀層數一多,程式會向右 「漂移」成難以閱讀的階梯。資深工程師偏好「衛述句」(guard clause) 風格:先把不合格的情況擋掉並提早離開,主要邏輯就能保持在最外層。
/** * 巢狀 if 與衛述句的對比:同樣的邏輯,兩種寫法。 */ public class GuardClauseDemo
public static void main(String[] args)
int age = 20; boolean hasTicket = true;
// 寫法一:巢狀 if——層層縮排,條件一多就像迷宮 if (age >= 18) if (hasTicket) System.out.println("巢狀寫法:歡迎入場!"); else System.out.println("巢狀寫法:請先買票。"); else System.out.println("巢狀寫法:未滿 18 歲不能入場。");
// 寫法二:衛述句——先排除異常,主流程不縮排 if (age < 18) System.out.println("衛述句:未滿 18 歲不能入場。"); return; // 提早結束 main 方法 if (!hasTicket) System.out.println("衛述句:請先買票。"); return; System.out.println("衛述句:歡迎入場!");
巢狀寫法:歡迎入場! 衛述句:歡迎入場!
|return| 在這裡的意思是「立刻結束目前的方法」,第 6 章會正式 介紹。衛述句的哲學是:把說「不」的理由講完,剩下的就都是 「好」——條件越多,這種風格的優勢越明顯。
switch:多選一的專用語法
當分支條件是「同一個變數等於不同的值」時,|switch| 比一長串 else if 更清楚。先看傳統語法——以及它惡名昭彰的陷阱。
傳統語法與落穿陷阱
/** * 傳統 switch 的落穿陷阱:故意漏掉 break,觀察災情。 */ public class SwitchFallThrough
public static void main(String[] args)
int day = 3;
switch (day) case 1: System.out.println("星期一"); case 2: System.out.println("星期二"); case 3: System.out.println("星期三"); // 從這裡命中開始 case 4: System.out.println("星期四"); // 沒 break,繼續掉 default: System.out.println("其他日子");
星期三 星期四 其他日子
|day| 是 3,卻連星期四和 default 都印出來了!這就是「落穿」 (fall-through):傳統 switch 命中一個 case 後,會一路往下 執行到底,除非遇到 |break| 才停。正確的傳統寫法是每個 case 結尾都加 |break;|。
忘記 |break| 是傳統 switch 最著名的陷阱:程式能編譯、能執行, 只是默默多做了一堆事。如果你使用傳統語法,請把「每個 case 都要 break」當成反射動作;更好的選擇是直接使用下面的箭頭語法,從語法 層面根絕這個問題。
箭頭語法:Java 14 之後的建議寫法
Java 14 起,switch 支援 |->| 箭頭語法:不會落穿、不需要 break、多個值可用逗號並列,甚至能把整個 switch 當成「會算出值」的 運算式使用。
/** * 箭頭語法三連發:敘述形式、運算式形式、yield 區塊。 */ public class SwitchArrowDemo
public static void main(String[] args)
int month = 4;
// 用法一:敘述形式。命中哪個箭頭就只執行那一行 switch (month) case 12, 1, 2 -> System.out.println("冬天"); case 3, 4, 5 -> System.out.println("春天"); case 6, 7, 8 -> System.out.println("夏天"); case 9, 10, 11 -> System.out.println("秋天"); default -> System.out.println("月份不正確");
// 用法二:運算式形式。整個 switch 算出一個值 int quarter = switch (month) case 1, 2, 3 -> 1; case 4, 5, 6 -> 2; case 7, 8, 9 -> 3; case 10, 11, 12 -> 4; default -> 0; ; System.out.println(month + " 月是第 " + quarter + " 季");
// 用法三:分支需要多行邏輯時,用大括號加 yield 交出值 String desc = switch (month) case 4 -> String base = "春暖花開"; yield base + "的四月"; // yield:這個分支的值 default -> "其他月份"; ; System.out.println(desc);
春天 4 月是第 2 季 春暖花開的四月
注意運算式形式的兩個規則:結尾的分號不能省(整個 switch 是指定敘述 的一部分);分支必須涵蓋所有可能值(所以通常需要 default)。
while:先檢查再執行
|while| 是最單純的迴圈:條件成立就執行主體,執行完回頭再檢查, 直到條件不成立。它適合「事先不知道要跑幾圈」的場合。經典範例:把 一個整數的位數反轉。
/** * 數字反轉:1234 變 4321。 * 核心技巧: */ public class ReverseNumber
public static void main(String[] args)
int number = 1234; int reversed = 0;
System.out.println("原始數字:" + number);
// 只要還有位數沒拆完,就繼續 while (number > 0) int digit = number reversed = reversed * 10 + digit; // 接到結果尾端 number = number / 10; // 丟掉個位數 System.out.println("取出 " + digit + ",目前結果:" + reversed);
System.out.println("反轉完成:" + reversed);
原始數字:1234 取出 4,目前結果:4 取出 3,目前結果:43 取出 2,目前結果:432 取出 1,目前結果:4321 反轉完成:4321
請拿紙筆跟著跑一遍:number 依序是 1234、123、12、1、0,reversed 依序 是 4、43、432、4321。這種「人腦模擬執行」叫做 trace,是培養程式邏輯 最有效的練習,沒有之一。
do-while:先執行再檢查
|do-while| 把檢查移到迴圈的尾端:主體至少執行一次, 之後才開始檢查條件。「至少一次」的天性使它成為輸入驗證的完美選擇—— 總得先問過一次,才知道輸入合不合法:
import java.util.Scanner;
/** * 輸入驗證:不斷詢問,直到使用者輸入 1 到 5 之間的數字。 */ public class InputValidation
public static void main(String[] args)
Scanner scanner = new Scanner(System.in); int choice;
// 先執行一次(至少要問一次),再檢查要不要重來 do System.out.print("請輸入 1 到 5 之間的數字:"); choice = scanner.nextInt(); while (choice < 1 || choice > 5); // 不合法就再問一次
System.out.println("你選擇了:" + choice); scanner.close();
請輸入 1 到 5 之間的數字:9 請輸入 1 到 5 之間的數字:0 請輸入 1 到 5 之間的數字:3 你選擇了:3
與 while 的差異就一句話:while 可能一次都不執行,do-while 保證至少 執行一次。注意 do-while 結尾的 |while (...)| 後面有 分號,這是初學者常漏掉的小地方。
for:計數迴圈的標準款
已知「要跑幾圈」時,|for| 把迴圈三要素收納進一行括號: |for (初始化; 條件; 更新)|。三個部分的執行時機是理解 for 的 關鍵:
初始化:進入迴圈前執行一次,通常宣告計數器 |int i = 1|。
條件:每圈開始前檢查,成立才執行主體, 不成立就離開迴圈。
更新:每圈主體執行完才執行,通常是 |i++|,然後回到步驟 2。
/** * 用 for 迴圈計算 1 加到 100。 * 執行順序:初始化、條件、主體、更新,之後不斷重複 * 「條件、主體、更新」直到條件不成立。 */ public class ForSum
public static void main(String[] args)
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i; // 每圈把 i 累加進 sum
System.out.println("1 加到 100 = " + sum);
1 加到 100 = 5050
計數器 |i| 宣告在 for 的括號裡,作用域僅限這個迴圈——迴圈 結束後 i 就消失了,這是良好的封閉性。三種迴圈的選擇準則整理如下:
| 迴圈 | 檢查時機 | 適用場景 |
|---|---|---|
| |for| | 每圈開始前 | 已知次數的計數式重複 |
| |while| | 每圈開始前 | 次數未知,可能一次都不執行 |
| |do-while| | 每圈結束後 | 次數未知,但至少執行一次 |
巢狀迴圈:九九乘法表
迴圈裡再放迴圈,就能處理「表格狀」的問題:外層每走一格,內層完整跑完 一輪。最經典的示範就是九九乘法表——外層控制「第幾列」,內層控制 「第幾行」:
/** * 九九乘法表:巢狀迴圈的經典應用。 * 外層 i 是被乘數(列),內層 j 是乘數(行)。 */ public class MultiplicationTable public static void main(String[] args) for (int i = 1; i <= 9; i++) for (int j = 1; j <= 9; j++) // printf 的 // (printf 的完整用法見第 7 章,先會用即可) System.out.printf(" System.out.println(); // 一列印完,換行
1*1= 1 1*2= 2 1*3= 3 1*4= 4 1*5= 5 1*6= 6 1*7= 7 1*8= 8 1*9= 9 2*1= 2 2*2= 4 2*3= 6 2*4= 8 2*5=10 2*6=12 2*7=14 2*8=16 2*9=18 3*1= 3 3*2= 6 3*3= 9 3*4=12 3*5=15 3*6=18 3*7=21 3*8=24 3*9=27 4*1= 4 4*2= 8 4*3=12 4*4=16 4*5=20 4*6=24 4*7=28 4*8=32 4*9=36 5*1= 5 5*2=10 5*3=15 5*4=20 5*5=25 5*6=30 5*7=35 5*8=40 5*9=45 6*1= 6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36 6*7=42 6*8=48 6*9=54 7*1= 7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49 7*8=56 7*9=63 8*1= 8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64 8*9=72 9*1= 9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
讀懂巢狀迴圈的訣竅:盯著外層的一圈看。當 |i| 是 3 時, 內層的 |j| 從 1 完整跑到 9,印出整列「3 的乘法」,然後換行; 外層才前進到 |i = 4|。總執行次數是外層乘以內層, \(9 \times 9 = 81\) 次——巢狀迴圈的成本成長得很快,這也是效能分析中 \(O(n^2)\) 的由來。
break 與 continue
迴圈裡有兩個流程控制的好幫手:|break| 立刻「離開整個迴圈」; |continue| 跳過本圈剩餘的程式,直接進入下一圈。
找到了:105 1 到 10 的奇數和:25
標籤 break:一次跳出多層
|break| 預設只跳出「最內層」迴圈。想從巢狀迴圈一次全身而退, 可以幫外層迴圈取名字(標籤),再 |break 標籤|:
/** * 標籤 break:在巢狀迴圈中找到目標後,一次跳出兩層。 */ public class LabelBreakDemo public static void main(String[] args) // 找出乘積恰好是 6 的第一組 (i, j) outer: // 幫外層迴圈取名 for (int i = 1; i <= 3; i++) for (int j = 1; j <= 3; j++) System.out.println("檢查 " + i + " x " + j); if (i * j == 6) System.out.println("找到:" + i + " x " + j); break outer; // 直接跳出外層迴圈 System.out.println("搜尋結束");
檢查 1 x 1 檢查 1 x 2 檢查 1 x 3 檢查 2 x 1 檢查 2 x 2 檢查 2 x 3 找到:2 x 3 搜尋結束
若這裡用普通 break,只會跳出內層,外層會繼續跑 |i = 3| 的 一輪。標籤 break 使用機會不多,但在多層搜尋時非常乾淨俐落。
迴圈的兩大經典災難:無窮迴圈與差一錯誤 (off-by-one)。無窮迴圈通常是忘了更新條件變數—— |while (n > 0)| 的主體裡忘記改 n,程式就永遠繞不出來(此時 可按 Ctrl+C 強制終止)。差一錯誤則是邊界少一圈或多一圈:想跑 5 次 卻寫 |i < 4|,或想取索引 0 到 4 卻寫 |i <= 5|。防禦 方法:邊界值親手代入頭尾各驗一次——「i 第一圈是多少?最後一圈是 多少?總共幾圈?」三問答對再往下寫。
綜合範例:猜數字遊戲
本章的畢業專題:電腦從 1 到 100 隨機挑一個數字,玩家反覆猜測,程式 提示「太大」或「太小」,直到猜中。這個小遊戲用到了隨機數、while 迴圈、多路 if 與計數器——正好是本章與前兩章的總複習。
import java.util.Random; import java.util.Scanner;
/** * 猜數字遊戲:電腦出題(1 到 100),玩家猜到對為止。 */ public class GuessNumber
public static void main(String[] args)
Scanner scanner = new Scanner(System.in); Random random = new Random();
// nextInt(100) 回傳 0 到 99,加 1 平移成 1 到 100 int answer = random.nextInt(100) + 1; int guess = 0; // 初始化成不可能的值,確保進入迴圈 int count = 0; // 記錄猜測次數
System.out.println("我想好了一個 1 到 100 的數字!");
// 還沒猜中就一直問 while (guess != answer)
System.out.print("請輸入你的猜測:"); guess = scanner.nextInt(); count++;
if (guess > answer) System.out.println("太大了,再小一點。"); else if (guess < answer) System.out.println("太小了,再大一點。"); else System.out.println("恭喜答對!你總共猜了 " + count + " 次。");
scanner.close();
以下是答案恰好是 58 的一次實際遊玩過程(每次執行答案都不同):
我想好了一個 1 到 100 的數字! 請輸入你的猜測:50 太小了,再大一點。 請輸入你的猜測:75 太大了,再小一點。 請輸入你的猜測:60 太大了,再小一點。 請輸入你的猜測:55 太小了,再大一點。 請輸入你的猜測:58 恭喜答對!你總共猜了 5 次。
觀察上面的遊玩策略:每次都猜「剩餘範圍的中間值」,範圍就會對半 縮小——這正是資訊科學中大名鼎鼎的「二分搜尋」。用這個策略,1 到 100 的數字最多 7 次一定猜中(\(2^7 = 128 > 100\))。另外,產生 隨機數也可以用 |Math.random()|,它回傳 0 到 1 之間的 double;|(int) (Math.random() * 100) + 1| 的效果與本例 相同,兩種寫法都值得認識。
本章小結
程式流程由循序、選擇、重複三種結構組成。
if 與 else if 由上而下檢查,命中即止;條件順序至關重要, 最嚴格的放前面;大括號永遠不省略。
衛述句風格:先擋掉異常情況並提早 return,主邏輯不縮排。
傳統 switch 命中後會一路落穿,每個 case 都要 break; Java 14 的箭頭語法不落穿、可並列多值、可當運算式(多行 分支用 yield 交值)。
while 先檢查再執行;do-while 先執行再檢查(至少一次), 適合輸入驗證。
for 三要素:初始化執行一次;條件每圈開始前檢查;更新在 主體之後執行。
巢狀迴圈處理表格狀問題,總次數是內外層相乘。
break 跳出整個迴圈,continue 跳過本圈;標籤 break 可一次 跳出多層。
小心無窮迴圈(忘記更新條件變數)與差一錯誤(邊界多一圈或 少一圈),邊界值要親手驗證。
本章練習
觀念題:while 與 do-while 的差別是什麼?「請使用者輸入密碼, 直到正確為止」適合哪一種?為什麼?(提示:至少要問一次。)
觀念題:傳統 switch 忘記寫 break 會發生什麼事?箭頭語法 為什麼沒有這個問題?(提示:落穿。)
實作題:經典 FizzBuzz——印出 1 到 30,遇到 3 的倍數印 Fizz、5 的倍數印 Buzz、同時是 3 和 5 的倍數印 FizzBuzz。 (提示:先檢查 15 的倍數,順序錯了就全毀。)
實作題:用迴圈印出 1000 以內的費氏數列:1、1、2、3、5、8、 13……(提示:用兩個變數存「前兩項」,每圈輪流更新。)
實作題:用巢狀迴圈印出 5 層的星號直角三角形——第 1 層 1 顆 星,第 5 層 5 顆星。(提示:內層迴圈的次數取決於外層的 計數器。)
挑戰題:改良猜數字遊戲——限制最多 7 次機會,次數用完印出 「挑戰失敗,答案是 X」;並在玩家輸入不在 1 到 100 範圍時 提示「範圍錯誤」且不消耗次數。(提示:while 條件加上次數 判斷,範圍檢查用 continue。)