Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

流程控制

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 流程控制:用紅綠燈程式學會做決定與重複〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。