Learning Objectives
- 說明 for 迴圈三個部分(初始化、條件、更新)的執行順序,推算迴圈會跑幾圈
- 分辨 while 與 do-while:while 可能一次都不執行,do-while 至少執行一次
- 用 for-each 依序取出陣列的每個元素,用巢狀迴圈處理「列 × 行」這類問題
- 用 break、continue 與標籤 break 控制迴圈提早結束或跳過某一圈
- 避開常見錯誤:差一錯誤、無窮迴圈、迴圈變數的有效範圍與同名變數
為什麼需要迴圈
要印出 1 到 100、把 50 位學生的分數加總、反覆詢問直到使用者輸入正確為止,這些工作都是「同樣的步驟做很多次」。 把同一行程式複製 100 次既費力又容易出錯,迴圈(loop)讓你只寫一次,交給電腦重複執行。
Java 有四種迴圈:
| 迴圈 | 適合的情況 |
|---|---|
for |
事先知道要跑幾圈,或要用一個計數器 |
while |
不知道要跑幾圈,只知道「條件成立就繼續」 |
do-while |
和 while 一樣,但本體至少要執行一次 |
| for-each | 依序取出陣列或集合裡的每個元素,不需要索引 |
學過 Python 的話:Java 的 for (int i = 0; i < 5; i++) 相當於 Python 的 for i in range(5);
Java 的 for-each for (int s : scores) 相當於 Python 的 for s in scores。
不同的是 Java 用大括號 { } 圍出迴圈本體,不靠縮排;條件一定要放在小括號裡,而且結果必須是 boolean。
for 迴圈的三個部分
for (int i = 1; i <= 4; i++) {
System.out.println(i);
}
小括號裡用兩個分號隔成三個部分,執行順序是:
- 初始化
int i = 1:只在最開始執行一次 - 條件
i <= 4:每一圈開始前檢查;true就執行本體,false就離開迴圈 - 執行本體(大括號裡的程式)
- 更新
i++:本體執行完才做,做完回到第 2 步
所以順序是「初始化 → 條件 → 本體 → 更新 → 條件 → 本體 → 更新 → …」,直到條件為 false。
最後一次檢查時 i 已經是 5,條件不成立,本體不會再執行。
更新不一定是 i++:i += 2 每次加 2、k-- 倒數都可以。在動畫裡,本體跑完之後執行箭頭會回到 for 那一行,那一步就是「更新、再檢查條件」。
for 迴圈逐步執行
盯著左邊的 i 和 sum。箭頭每回到第 5 行一次,就是做完 i++ 再檢查條件;i 變成 5 時條件不成立,跳到第 9 行,而且 i 從變數區消失了:在 for 小括號裡宣告的變數,只在迴圈裡有效。
while 與 do-while
while 只有條件,沒有初始化和更新的位置:
int n = 20;
while (n > 1) { // 條件成立才執行本體
n = n / 2;
}
不知道要跑幾圈、只知道「什麼時候停」的時候用 while 比較自然,例如「一直除以 2,直到剩下 1」「一直讀取輸入,直到使用者輸入 0」。
條件一開始就不成立的話,本體一次都不會執行。
do-while 把條件放在最後面,所以本體至少執行一次,然後才檢查條件決定要不要再來一次:
do {
System.out.print("請輸入 1~5:");
choice = sc.nextInt();
} while (choice < 1 || choice > 5); // 注意結尾的分號
「先做一次再說」的情況(例如顯示選單、要求輸入,輸入不合法就再問一次)很適合 do-while。
while (…) 後面那個分號是 do-while 語法的一部分,忘了寫會編譯錯誤。
while 可能一次都不跑,do-while 至少跑一次
前半段看 n 怎麼從 20 一路除到 1,count 記錄除了幾次。後半段的 x 是 100:第 13 行的 while (x < 10) 一開始就不成立,箭頭直接跳過本體;第 18 行的 do-while 卻會先執行一次本體,到第 21 行才檢查條件。
想一想:迴圈會跑幾圈
下面的程式會印出什麼?
int n = 0;
do {
n += 3;
} while (n < 10);
System.out.println(n);
下面的程式會印出什麼?
int c = 0;
for (int i = 0; i < 5; i++) {
for (int j = i; j < 5; j++) {
c++;
}
}
System.out.println(c);
下面的程式會印出什麼?
for (int i = 0; i < 3; i++) {
// 什麼都不做
}
System.out.println(i);
for-each 與巢狀迴圈
for-each(也叫加強型 for)依序取出陣列或集合裡的每個元素,不需要自己管索引:
int[] scores = {72, 85, 90};
for (int s : scores) { // 讀作「對 scores 裡的每一個 s」
System.out.println(s);
}
冒號左邊是變數(型別要和元素一樣),右邊是要走訪的陣列。陣列的細節(怎麼建立、索引、長度)在第 06 課介紹。
for-each 只能從頭到尾依序讀取;需要知道「現在是第幾個」、要倒著走,或要修改陣列裡的元素時,還是要用一般的 for 搭配索引。
巢狀迴圈是在迴圈本體裡再放一個迴圈。外層每跑一圈,內層就從頭到尾完整跑一輪,
適合處理「列 × 行」的資料,例如乘法表、表格、二維陣列。外層跑 3 圈、內層每次跑 3 圈,內層本體一共執行 3 × 3 = 9 次。
內外層的計數器要用不同名稱(習慣用 i、j),否則會編譯錯誤。
for-each 加總與小型乘法表
前半段看 s:for-each 每一圈把陣列的下一個元素放進 s,你不會看到索引。後半段是巢狀迴圈:注意 j 每次都從 1 重新開始,跑完 3 圈後 j 消失,i 才加 1。
break、continue 與標籤
break:立刻離開整個迴圈,跳到迴圈後面的第一行continue:跳過這一圈剩下的程式,直接進入下一圈(for迴圈會先執行更新,再檢查條件)
break 與 continue 只影響最內層的那個迴圈。在巢狀迴圈裡想一次離開外層,可以替外層迴圈加上標籤(label):
outer: // 標籤:名稱加冒號,放在迴圈前面
for (int r = 1; r <= 3; r++) {
for (int c = 1; c <= 3; c++) {
if (r * c == 4) {
break outer; // 直接離開外層迴圈
}
}
}
continue outer; 也可以用:結束內層,直接進入外層的下一圈。標籤只用在真的需要一次跳出多層的時候;
迴圈太複雜時,把它拆成一個方法、找到答案就 return(第 07 課),通常更好讀。
無窮迴圈:條件永遠成立的迴圈,例如 while (true) { … } 或 for (;;) { … }。刻意寫成無窮迴圈時,本體裡一定要有 break(或 return)作為出口;
不小心寫出來的無窮迴圈,最常見的原因是忘了更新計數器,例如 while (i < 10) 的本體裡沒有 i++。
continue 跳過、break 離開、標籤 break 跳出兩層
第一個迴圈:i 是偶數時第 6 行 continue,箭頭直接回到第 4 行,不會印出;i 是 5 時第 9 行 break,6 根本不會被檢查。第二個迴圈:r * c == 4 時 break outer,內外兩層一起結束,直接跳到第 23 行。
常見錯誤
差一錯誤(off-by-one error):迴圈多跑或少跑一圈,是寫迴圈最常見的錯誤。
int[] a = {10, 20, 30}; // 索引是 0、1、2,a.length 是 3
for (int i = 0; i <= a.length; i++) { // 錯:i 會跑到 3
System.out.println(a[i]); // i 是 3 時拋出 ArrayIndexOutOfBoundsException
}
從 0 開始數、要跑 n 次時,條件寫 i < n;從 1 開始數到 n,才寫 i <= n。不確定時,代入最小的情況(n = 1)想一遍迴圈跑幾次。
變數的有效範圍(scope):
- 在
for小括號裡宣告的變數,只在迴圈裡有效;迴圈結束後再用會編譯錯誤cannot find symbol - 在迴圈本體大括號裡宣告的變數,每一圈都是新的,離開本體就不存在;要在迴圈外累加的變數(例如
sum),一定要宣告在迴圈前面 - 迴圈裡不能宣告和外面同名的區域變數:
int i = 0;
for (int i = 0; i < 3; i++) { } // 編譯錯誤:variable i is already defined in method main(String[])
Python 允許這樣寫(迴圈會直接改掉外面的 i),Java 不允許,可以防止你不小心蓋掉外面的變數。
兩個前後並列的迴圈各自宣告 int i 則沒有問題,因為第一個 i 在第二個迴圈開始前就已經消失了。
分號放錯位置:for (int i = 0; i < 3; i++); 小括號後面多一個分號,迴圈本體變成空的,下面的大括號只會執行一次。while (…); 更糟,可能變成無窮迴圈。
想一想:break、continue 與常見錯誤
下面的程式會印出什麼?
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) continue outer;
System.out.print(i + "" + j + " ");
}
}
System.out.println();
下面的程式執行到最後會拋出什麼例外?
int[] a = {1, 2, 3};
for (int i = 0; i <= a.length; i++) {
System.out.println(a[i]);
}
下面的程式會印出什麼?
int i = 0;
for (int i = 0; i < 3; i++) {
System.out.print(i);
}
動手寫:星號三角形
用巢狀迴圈印出 4 列星號:第 1 列 1 顆、第 2 列 2 顆……第 4 列 4 顆,最後印出一共用了幾顆。外層迴圈已經寫好,請完成內層迴圈與計數。
重點回顧
for (初始化; 條件; 更新):初始化只做一次,之後每圈「條件 → 本體 → 更新」,條件為false時離開while先檢查條件,可能一次都不執行;do { … } while (條件);先執行再檢查,至少執行一次,結尾要有分號- for-each
for (int s : scores)依序取出每個元素,不需要索引;要索引或要修改元素時用一般for - 巢狀迴圈:外層每跑一圈,內層完整跑一輪
break離開迴圈、continue跳過這一圈,都只影響最內層;加上標籤(break outer;)可以一次跳出外層- 常見錯誤:差一錯誤(
<和<=)、忘了更新計數器造成無窮迴圈、迴圈結束後使用迴圈內宣告的變數、在迴圈裡宣告和外面同名的變數