Java Collections、泛型與例外處理:每天寫 Java 都會碰到的東西
List/Map/Set 怎麼選、泛型跟 TS 泛型的機制差在哪、Stream API 對照 JS 的鏈式陣列方法,還有 JS 完全沒有對應概念的 checked exception。
系列第三篇。前兩篇打了語法基礎跟物件導向核心,這篇進到「寫 Java 程式的日常」——怎麼存一組資料、泛型怎麼用、錯誤處理跟 JS 的 try/catch 有什麼不一樣。下一篇是實作 Spring Boot API 的〈Spring Boot 入門〉。
一、Collections:對照 JS 的 Array / Object / Map
JS 裡陣列用 Array,鍵值對用 Object 或 Map,不需要多想就能混用。Java 把「一組資料該怎麼存」拆成明確分工的介面跟實作類別,選型看你要的特性:
| 你想要 | Java 介面 | 常用實作 | 對照 JS |
|---|---|---|---|
| 有序、可重複、依索引存取 | List<T> | ArrayList<T>(動態陣列)、LinkedList<T>(鏈結串列) | Array |
| 鍵值對 | Map<K, V> | HashMap<K, V>、LinkedHashMap<K, V>(保留插入順序) | Object / Map |
| 不重複的集合 | Set<T> | HashSet<T>、LinkedHashSet<T> | 沒有原生對應,new Set() 概念上最接近 |
List<String> names = new ArrayList<>();
names.add("Tommy");
names.add("Nana");
System.out.println(names.get(0)); // "Tommy"
Map<String, Integer> scores = new HashMap<>();
scores.put("Tommy", 90);
scores.put("Nana", 85);
System.out.println(scores.get("Tommy")); // 90
System.out.println(scores.getOrDefault("Unknown", 0)); // 0,key 不存在時的預設值寫法
Set<String> uniqueTags = new HashSet<>();
uniqueTags.add("java");
uniqueTags.add("java"); // 重複的會被忽略
System.out.println(uniqueTags.size()); // 1
注意宣告時左邊寫的是介面(List),右邊 new 出來的才是實作(ArrayList)——這是 Java 很常見的慣例:變數型別盡量宣告成介面,之後想換成 LinkedList 只要改右邊那一個字,呼叫方的程式碼完全不用動,因為它們一開始就只認識 List 這個介面提供的方法。這跟上一篇講的「針對介面寫程式」是同一個原則的具體實踐。
二、泛型:編譯期幫你擋型別錯誤
沒有泛型之前(Java 5 以前),List 裡面只能放 Object,取出來要自己手動轉型,型別寫錯編譯器完全不會提醒你,只能等執行期炸掉:
List oldStyleList = new ArrayList(); // 沒有泛型參數,元素型別是 Object
oldStyleList.add("hello");
oldStyleList.add(123); // 沒有任何警告,String 跟 Integer 混著放
String s = (String) oldStyleList.get(1); // 執行期才爆炸:ClassCastException
泛型 List<String> 讓你在編譯期就把這種錯誤擋下來:
List<String> names = new ArrayList<>();
names.add("hello");
names.add(123); // 編譯直接失敗,不會等到跑起來才發現
概念上跟 TS 的泛型(Array<string>)幾乎一樣,語法也長得很像。差別在底層機制:TS 的泛型是純編譯期擦除,執行期完全沒有留下痕跡;Java 的泛型雖然也有「型別擦除」(執行期 bytecode 其實看到的是 raw 的 List,拿不到 <String> 這個資訊),但 Java 是先幫你在編譯期把型別檢查做完、也自動幫你插入轉型程式碼,才把泛型資訊擦掉——體感上你完全感覺不到擦除這件事,寫起來就是「泛型有在保護你」。
自己寫一個泛型方法:
// T 是型別參數,呼叫時會被推斷或指定成實際型別
static <T> T firstElement(List<T> list) {
return list.get(0);
}
String first = firstElement(names); // T 被推斷成 String
三、Stream API:很像 JS 的鏈式陣列方法
Java 8 引入的 Stream API,寫法上跟 JS 的 .map()/.filter()/.reduce() 鏈式操作神似:
List<Integer> scores = List.of(55, 90, 72, 88, 40);
List<Integer> passingScoresDoubled = scores.stream()
.filter(s -> s >= 60) // 對照 JS 的 .filter()
.map(s -> s * 2) // 對照 JS 的 .map()
.collect(Collectors.toList());
System.out.println(passingScoresDoubled); // [180, 176, 144]
int total = scores.stream()
.reduce(0, Integer::sum); // 對照 JS 的 .reduce()
System.out.println(total); // 345
s -> s >= 60 這種寫法是 lambda expression,對照 JS 的箭頭函式 s => s >= 60,語意幾乎一樣。Integer::sum 是方法參考(method reference),是「已經有現成方法可以用,不用再包一層 lambda」的簡寫,對照 JS 沒有直接對應的語法,概念上有點像 scores.reduce((a, b) => a + b) 但把 (a, b) => a + b 換成直接指名一個已存在的函式。
四、例外處理:Checked vs Unchecked,JS 完全沒有對應概念
JS 只有一種「錯誤」,try/catch 抓不抓完全是你自己的選擇,編譯器(如果有 TS)不會強迫你處理任何錯誤。Java 把例外分成兩種,而且編譯器會強制檢查其中一種:
| Checked Exception | Unchecked Exception(RuntimeException 的子類別) | |
|---|---|---|
| 編譯器會不會強制要求處理 | 會——不 catch 或宣告 throws 就編譯不過 | 不會,跟 JS 的錯誤一樣隨你 |
| 代表的意境 | 「這是可預期、呼叫方應該要有對策的狀況」,例如檔案不存在、網路斷線 | 「這通常是程式寫錯了」,例如 NullPointerException、ArrayIndexOutOfBoundsException |
| 常見例子 | IOException、SQLException | IllegalArgumentException、NullPointerException |
// Checked exception:方法簽名上要老實寫出「我可能丟出這個」
void readFile(String path) throws IOException {
Files.readString(Path.of(path)); // 這個方法本身宣告了 throws IOException
}
// 呼叫方要嘛自己 catch,要嘛也在自己的簽名上宣告 throws,往上丟給更上層處理
void caller() {
try {
readFile("data.txt");
} catch (IOException e) {
System.out.println("讀檔失敗: " + e.getMessage());
}
}
// Unchecked exception:不強制處理,寫錯了就是編譯期看不出來、執行期才爆
void divide(int a, int b) {
System.out.println(a / b); // b 是 0 的話丟出 ArithmeticException,編譯器不會提醒你
}
這個機制常被兩極評價——優點是「檔案讀不到」這種可預期的失敗情境,呼叫方沒有藉口忘記處理,強制在編譯期就想清楚錯誤路徑;缺點是寫起來確實比較囉唆,而且業界不少人(包括 Spring 生態本身)後來傾向少用 checked exception,盡量用 unchecked exception 搭配框架層級統一處理錯誤——這點在下一篇實作 Spring Boot 的時候會實際碰到。
try-with-resources 是另一個值得知道的語法,自動幫你關閉資源(檔案、資料庫連線),概念上類似但比手動寫 finally 區塊乾淨很多:
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("讀檔失敗: " + e.getMessage());
}
// reader 離開 try 區塊時會自動呼叫 .close(),不用自己寫 finally { reader.close(); }
三篇語言基礎打完,下一篇正式進到實際會拿來寫後端服務的 Spring Boot——會用今天學的 class、interface、依賴注入的概念,搭一個最小可行的 REST API,對照你已經熟悉的 Next.js API route 跟 Go Gin handler 心智模型。