Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 28 · 臺南 AQI 24 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

流程控制

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 對照,讓你看見差異在哪。

Java 流程控制概念示意圖

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 接受的型別有限:intcharenum,以及——這點很多人不知道——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 Truebreak 模擬;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 介面的集合(如 ListSet):

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 的倍數」一定要放最前面。

常見錯誤:初學者最容易踩的五個雷

  1. 條件式用整數而非 booleanif (count) 在 Java 不合法,要寫 if (count != 0)。把 == 誤打成 = 在多數情況也會被編譯器擋下,這是靜態型別的紅利。

  2. switch 忘了 break:少了 break 會 fall-through 貫穿到下個 case。除非你刻意要合併條件,否則每個 case 結尾都該有 break。若用現代 switch 運算式(見下節),這個問題從根本消失。

  3. 字串比較用 ==if (name == "Java") 比的是物件參考(reference),不是內容!比較字串內容一律用 .equals()if (name.equals("Java"))。這是 Java 新手最痛的一刀,因為有時 == 會「碰巧」因字串池(string pool)而成立,讓 bug 時隱時現。

  4. enhanced for 裡修改集合:走訪 List 時呼叫 list.remove(...) 會丟 ConcurrentModificationException。要刪除請用 Iterator.remove()removeIf()

  5. 迴圈變數作用域誤解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 位元組碼指令——tableswitchlookupswitch——只認整數。那字串怎麼辦?答案是編譯器在背後做了一次巧妙的「去糖(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 能在大型系統裡屹立不搖的根基。下次當某個語法用起來特別順手時,不妨想想:編譯器替你在背後做了什麼?

AI 共讀助教正在陪你讀:Java 流程控制:用紅綠燈程式學會做決定與重複
嗨!我是這篇文章的共讀助教,只根據〈Java 流程控制:用紅綠燈程式學會做決定與重複〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。