前言
電腦程式預設的執行順序是由上而下,逐條敘述執行。透過控制結構,可以改變程式執行的順序。本文介紹 Java 的控制結構。
if
敘述
if
敘述是基本的選擇控制結構 (selection control structure)。最簡單的 if
敘述為一元結構。只有在 condition
為真時才會執行區塊內的程式碼:
if (condition) {
/* Run code if `condition` is true. */
}
除了一元敘述外,可以加上選擇性的 else
敘述,形成二元選擇結構。當 condition
為真時,執行 if
敘述部分的程式碼;反之,則執行 else
敘述內的程式碼:
if (condition) {
/* Run code if `condition` is true. */
}
else {
/* Run code if `condition` is not true. */
}
承上,還可以再加上選擇性的 else if
敘述,形成多元選擇結構。範例虛擬碼如下:
if (condition_a) {
/* Run code if `condition_a` is true. */
}
else if (condition_b) {
/* Run code if `condition_b` is true. */
}
else {
/* Run code if all conditions are false. */
}
由於 else if
敘述和 else
敘述都是選擇性的,以下虛擬碼也是合理的 Java 控制結構:
if (condition_a) {
/* Run code if `condition_a` is true. */
}
else if (condition_b) {
/* Run code if `condition_b` is true. */
}
看完虛擬碼後,來看一下實際範例。此範例以 java.util.Random 物件建立隨機整數,然後後 if
敘述判斷該隨機數是奇數還是偶數:
import java.util.Random;
public class MainProgram
{
public static void main (String[] args)
{
var max = 10;
var min = 1;
var random = new Random();
var n = random.nextInt(max - min + 1) + min;
if (n % 2 == 0) {
System.out.println(n + " is even");
}
else {
System.out.println(n + " is odd");
}
}
}
(選擇性) if
敘述的排版方式
大部分 C 家族語言使用以下方式來排版:
if (a) {
/* Some code here. */
} else if (b) {
/* Some code here. */
} else {
/* Some code here. */
}
Golang 甚至把這種風格強制寫進編譯器裡,沒得調整。
但筆者在寫 C 家族語言的 if
敘述時,會採用以下方式:
if (a) {
/* Some code here. */
}
else if (b) {
/* Some code here. */
}
else {
/* Some code here. */
}
這是因為 if
敘述、else if
敘述、else
敘述在概念上屬於同一層。寫巢狀結構時,這樣的排版方式會比傳統的方式更加清楚。
或者是參考 C# (C sharp) 的排版方式:
if (a)
{
/* Some code here. */
}
else if (b)
{
/* Some code here. */
}
else
{
/* Some code here. */
}
這種方式也和筆者的排版方式有相同效果。C# 會採用這種排版方式是因為 C# 承襲 Pascal 的慣例。
switch
敘述
switch
敘述是另一種選擇控制結構。這種控制結構僅限於值比較。以下是 switch
的虛擬碼:
switch (value) {
case a:
/* Run code if `a`. */
break;
case b:
/* Run code if `b`. */
/* Fallthrough. */
case c:
/* Run code if `c`. */
break;
default:
/* Run code if all cases are false. */
break;
}
switch
的雷是忘了在 case
敘述的末端加上 break
敘述,就會穿過該 case
敘述,到達下一個 case
敘述。這樣的特性稱為 fallthrough。有時候 fallthough 是程式設計者刻意安排的,但常常是 bug 的來源。
看完虛擬碼後,來看一下實際範例。此範例程式求得今日是星期幾 (day of week),然後根據該值印出相對應的評語:
import java.util.Date;
import java.util.GregorianCalendar;
public class MainProgram
{
public static void main (String[] args)
{
var now = new Date();
var c = GregorianCalendar.getInstance();
c.setTime(now);
var dow = c.get(GregorianCalendar.DAY_OF_WEEK);
switch (dow) {
case 6:
case 7:
System.out.println("Weekend");
break;
case 5:
System.out.println("Thanks God. It's Friday");
break;
case 3:
System.out.println("Hump day");
break;
default:
System.out.println("Week");
break;
}
}
}
注意這裡用到一次 fallthrough,這不是 bug,而是符合該程式預期行為的安排。
while
敘述
while
敘述是基本的迭代控制結構。只要 condition
符合條件,while
迴圈會持續執行同一段程式碼區塊。以下是 while
的虛擬碼:
while (condition) {
/* Run code while `condition` is true. */
}
看過虛擬碼後,來看實際的程式碼:
var i = 1;
while (i <= 10) {
System.out.println(i);
++i;
}
由於 while
迴圈會持續執行同一段程式碼,若結束條件寫錯,會變成無窮迴圈 (infinite loop)。有時候無窮迴圈是程式設計者刻意設計的,但時常是 bug 的來源。
for
敘述
使用計數器 (Counter)
傳統的 for
迴圈使用計數器來控制執行次數。使用計數器的 for
迴圈虛擬碼如下:
for (start; end; step) {
/* Run code if not `end`. */
}
看過虛擬碼後,來看實際範例:
for (var i = 1; i <= 10; ++i) {
System.out.println(i);
}
使用迭代器 (Iterator)
使用迭代器的 for
迴圈需要搭配容器 (collection) 或迭代器,故留到介紹容器時才一併說明。
改變迴圈行進的敘述
break
敘述
break
敘述可提早中斷迴圈。參考以下例子:
for (var i = 1; i <= 10; ++i) {
if (i > 5)
break;
System.out.println(i);
}
先前提到的 switch
敘述也是用 break
敘述來區隔 case
敘述。
continue
敘述
continue
敘述用來跳過該次迭代剩下的程式碼。可參考以下例子:
for (var i = 1; i <= 10; ++i) {
if (i % 2 != 0)
continue;
System.out.println(i);
}
(選讀) 終極密碼 (Da Vinci Code)
先前的範例偏短,較難體會使用控制結構寫程式的感覺。本節使用一個稍長的範例來看如何使用 Java 控制結構。這個範例程式是一個常見的桌遊。
此程式的執行過程如下:
$ java MainProgram
Input a number (1, 100): 50
Too small
Input a number (50, 100): 75
Too big
Input a number (50, 75): 63
Too big
Input a number (50, 63): 57
Too big
Input a number (50, 57): 53
You guess right
小伙伴可以試著先不要看範例程式碼,自己實作看看。以下是參考範例,我們會在後文說明此範例:
import java.util.Random; /* 1 */
public class MainProgram /* 2 */
{ /* 3 */
public static void main (String[] args) /* 4 */
{ /* 5 */
/* Set the initial range of a answer. */ /* 6 */
var max = 100; /* 7 */
var min = 1; /* 8 */
/* Get a random answer
between `min` and `max`. */
var random = new Random(); /* 9 */
var answer = /* 10 */
random.nextInt(max - min + 1) + min; /* 11 */
while (true) { /* 12 */
/* Try to receive a valid input
from the user. */
int guess; /* 13 */
while (true) { /* 14 */
/* Prompt the user for an input. */ /* 15 */
var input = System /* 16 */
.console() /* 17 */
.readLine( /* 18 */
"Input a number " /* 19 */
+ "(" + min + ", " /* 20 */
+ max + "): "); /* 21 */
try { /* 22 */
/* Parse the input. */ /* 23 */
guess = Integer.parseInt(input); /* 24 */
} /* 25 */
catch (Exception e) { /* 26 */
/* It fails to parse the input.
Skip to next iteration. */
System.out /* 27 */
.println( /* 28 */
"Invalid number: " /* 29 */
+ input); /* 30 */
continue; /* 31 */
} /* 32 */
/* The guess out of range.
Skip to next iteration. */
if (!(min <= guess && guess <= max)) { /* 33 */
System.out /* 34 */
.println( /* 35 */
"Number out of range: " /* 36 */
+ guess); /* 37 */
continue; /* 38 */
} /* 39 */
/* The guess is valid.
Exit the input loop. */
break; /* 40 */
} /* 41 */
if (guess < answer) { /* 42 */
System.out.println("Too small"); /* 43 */
min = guess; /* 44 */
} /* 45 */
else if (answer < guess) { /* 46 */
System.out.println("Too big"); /* 47 */
max = guess; /* 48 */
} /* 49 */
else { /* 50 */
System.out.println("You guess right"); /* 51 */
/* The guess is equal to the answer.
Exit the game loop. */
break; /* 52 */
} /* 53 */
} /* 54 */
} /* 55 */
} /* 56 */
為了簡化範例,此程式的上下限是固定的 (第 7、8 行)。根據此上下限求出一隨機答案 (第 9 至 11 行)。
大部分的程式碼是一個遊戲迴圈 (game loop) (第 12 至 54 行)。該迴圈可分為取得使用者輸入 (第 13 至 41 行) 和判讀答案 (第 42 至 53 行) 兩部分。
程式無法預知使用者是否輸入合理的數字,所以再用一個無窮迴圈,直到使用者輸入合理的數字才跳出此迴圈 (第 40 行)。使用 readLine() 函式取得使用者輸入 (第 16 至 21 行)。但使用者可能輸入錯誤的數字,故使用 parseInt() 函式試著解析使用者輸入,若使用者輸入錯誤的輸入,則略過剩下的程式碼 (第 22 至 32 行)。即使使用者輸入合理的數字,也可能超出上下限,所以要進行檢查 (第 33 至 39 行)。
確認使用者輸入合理的數字後,判讀使用者是否猜對 (第 42 至 53 行)。若使用者猜對,則中止遊戲迴圈 (第 50 至 53 行)。反之,則更新程式的上下限 (第 42 至 49 行)。
若讀者想繼續練習,以下是幾個加強此程式的方向:
- 由使用者決定答案的上下限
- 限定猜測次數。超過猜測次數算失敗
- 給予使用者起始分數,猜對加分,猜錯扣分。當分數歸零時結束遊戲,當分數超過閥值時判定使用者獲勝
這些部分就留給小伙伴自行嘗試,這裡不再展示範例程式碼。