現代 Java FAQ(Java 9 – 21)
範圍 — Java 8 之後改了什麼:
var、record、sealed 型別、模式比對、text block、模組系統,以及虛擬執行緒。 另見:java_functional.md— Java 8 的 lambda/stream 工具箱;java_multi_thread.md— 平台執行緒與執行緒池。
多數說自己「還在 Java 8」的團隊,其實早就在 11 或 17 上了;而面試官想問的是:如果 不是 8,你會用什麼。下面這份短清單,是真正改變寫法的那些東西。
1) 版本地圖 優先度 3/5 — 值得會 — 多半是必備模式的變形
| 版本 | 狀態 | 代表性功能 |
|---|---|---|
| 8(2014) | LTS,到處都還在用 | Lambda、stream、Optional、新的日期時間 API |
| 9 | — | 模組系統(JPMS)、List.of、Stream.takeWhile |
| 10 | — | 區域變數的 var |
| 11(2018) | LTS | String.strip/repeat/lines、HttpClient、單檔原始碼直接執行 |
| 14–16 | — | switch 運算式、record、text block、instanceof 模式 |
| 17(2021) | LTS | Sealed class,上述功能正式定案 |
| 21(2023) | LTS | 虛擬執行緒、switch 的模式比對、record pattern、sequenced collection |
Java 9 之後改成每半年一個版本,所以版本號能說明的能力比以前少得多;真正重要的是 你鎖定哪一個 LTS。
2) var —— 區域變數型別推論 優先度 3/5 — 值得會 — 多半是必備模式的變形
// java
var users = new HashMap<String, List<Order>>(); // no repeated type on the left
for (var entry : users.entrySet()) { ... }
- 只能用在區域變數、
for的變數與try-with-resources —— 不能用在欄位、參數或 回傳型別。 - 型別依然是靜態且固定的;這是推論,不是動態型別。
- 右邊已經寫出型別時就用它。會把讀者需要的型別藏起來時就不要用
(
var result = service.process(x);)。
3) Record 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
Record 是不可變資料的透明載體 —— 就是你以前靠 IDE 幫你產生的那種類別。
// java
public record Money(long cents, String currency) {
public Money { // compact constructor: validate
Objects.requireNonNull(currency);
if (cents < 0) throw new IllegalArgumentException("negative");
}
public Money plus(Money other) { // ordinary methods are fine
return new Money(cents + other.cents, currency);
}
}
編譯器會幫你生成:private final 欄位、正規建構子、名為 cents() / currency()
的存取子(沒有 get 前綴),以及 equals、hashCode 和 toString。
| 性質 | 後果 |
|---|---|
隱含 final,不能繼承類別 |
它不是用來取代類別階層的 |
欄位都是 final |
值語意,可以安全地跨執行緒共享 |
equals/hashCode 由所有成分算出 |
可以直接當 map 的 key |
| 成分只是「淺層」不可變 | record Team(List<Player> players) 還是可以透過那個 list 被改動 —— 在 compact 建構子裡複製一份 |
適合 DTO、API 的 request/response 主體、值物件,以及回傳多個值的場合。 需要身分(identity)或可變性的地方就不要用(例如 JPA entity)。
4) Sealed 型別與模式比對 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
sealed 把允許的子型別固定下來 —— 一個編譯器推論得動的封閉階層。
// java
public sealed interface Shape permits Circle, Square, Rectangle {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
再配上(21 的)switch 模式比對,編譯器能證明這個 switch 是窮盡的,
於是「新增一個子型別後執行期靜悄悄地做錯事」就變成了編譯錯誤:
// java
double area = switch (shape) { // no default needed — sealed + exhaustive
case Circle c -> Math.PI * c.r() * c.r();
case Square s -> s.side() * s.side();
case Rectangle(double w, double h) -> w * h; // record pattern: destructures
};
同一組功能裡比較小的幾塊:
// java
if (obj instanceof String s && !s.isBlank()) { // pattern: binds `s`, no cast
System.out.println(s.length());
}
String label = switch (day) { // switch EXPRESSION: yields a value,
case SAT, SUN -> "weekend"; // arrow form, no fall-through, no break
default -> "weekday";
};
這就是代數資料型別的寫法:sealed interface + 用 record 當各個變體 +
對它們 switch。它讓你不用 visitor 也能表達「N 種情況中的一種」。
5) Text block 與字串的新方法 優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問
// java
String query = """
SELECT id, name
FROM users
WHERE age > ?
"""; // incidental indentation stripped
" hi ".strip(); // Unicode-aware trim (11)
"ab".repeat(3); // "ababab" (11)
text.lines().filter(...) // Stream<String> (11)
String.join(", ", parts);
"%s is %d".formatted(name, age); // (15)
6) 集合與 API 的新東西 優先度 3/5 — 值得會 — 多半是必備模式的變形
// java
List<String> fixed = List.of("a", "b"); // immutable, rejects null (9)
Map<String, Integer> m = Map.of("a", 1, "b", 2);
List<String> copy = List.copyOf(other);
var first = list.getFirst(); // sequenced collections (21)
var reversed = list.reversed();
Files.readString(path); // (11)
HttpClient.newHttpClient().send(request, ofString()); // built-in HTTP/2 client (11)
List.of 回傳的是真正不可變的 list(add/remove/set 全都丟例外),
不像 Arrays.asList(長度固定但可以 set)或
Collections.unmodifiableList(只是某個別人還改得動的 list 的視圖)。
7) 模組系統(JPMS)優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問
// java — module-info.java
module com.acme.orders {
requires com.acme.common;
exports com.acme.orders.api; // only this package is visible outside
}
它提供 package 層級的強封裝,加上明確宣告的依賴;接著 jlink 就能只用你會用到的
模組建出一個 runtime image。實務上多數應用還是留在 classpath 上,只在
「illegal reflective access」警告與 --add-opens 旗標上感受到 JPMS 的存在。
知道它是什麼、以及 JDK 自己為什麼要模組化,就夠了。
8) 虛擬執行緒(Project Loom,21)優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
虛擬執行緒是一個由 JVM 排程到少量 OS carrier 執行緒上的 Thread。當它因為 I/O
阻塞時會被卸載(unmount),把 carrier 釋放出來 —— 所以阻塞式的程式碼不必重寫,
就能有非同步程式碼的擴展性。
// java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (var url : urls) {
executor.submit(() -> fetch(url)); // a million of these is fine
}
} // close() waits for all tasks
| 平台執行緒 | 虛擬執行緒 | |
|---|---|---|
| 背後是 | 一條 OS 執行緒 | 配置在 heap 上、跑在 carrier 執行緒上的 stack |
| 成本 | 約 1 MB stack,建立很貴 | 約幾 KB,很便宜 —— 每個任務開一條 |
| 合理的數量 | 數百條 | 數百萬條 |
| 阻塞 I/O | 白白佔著 OS 執行緒 | 卸載,carrier 去做別的事 |
| 池化 | 必要 | 反模式 —— 每個任務開一條就好 |
實務上會改變的事:
- **執行緒池不再是限制並發的手段。**要限制對資料庫或下游服務的存取,改用
Semaphore。 - 在 21 上,
synchronized會把虛擬執行緒釘(pin)在它的 carrier 上,只要它在 區塊裡阻塞,就可能把一池 carrier 榨乾;ReentrantLock不會,這就是 21 上建議在 阻塞呼叫外圍用它的原因。(21 之後的補充:Java 24 的 JEP 491 移除了這個 pinning, 所以 24 以上直接用synchronized又沒問題了。) ThreadLocal還能用,但「建立很貴,所以每條執行緒快取一份」這個理由不成立了; scoped value 是設計上的替代品。- CPU 密集的工作一點好處也沒有 —— Loom 講的是並發(很多在等待的任務), 不是平行。
結構化並發(structured concurrency)把一個任務的子任務做成一個範圍,整組一起
失敗、一起取消 —— 和 Python 的 TaskGroup 是同一個想法。它目前還是預覽 API:
在 21 上編譯與執行都要加 --enable-preview,而且它的形狀在各版本之間改過,
所以別把它放進你沒辦法重新編譯的正式程式碼裡。
// java
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> fetchUser(id));
var order = scope.fork(() -> fetchOrders(id));
scope.join().throwIfFailed();
return new Page(user.get(), order.get());
}
9) 面試常見問答
Q:為什麼要從 8 升到 17/21?
record 與 sealed 型別去掉樣板碼,也讓不合法的狀態根本表達不出來;switch 運算式與
模式比對消掉一整類 fall-through 的 bug;G1/ZGC 與 JIT 的改進壓低了停頓時間;21 再加上
虛擬執行緒。另外,8 在多數廠商那邊已經沒有免費的公開支援了。
Q:Record 和 Lombok 的 @Value 差在哪?
Record 是語言功能 —— 不需要 annotation processor、不需要 IDE 外掛,而且編譯器懂它
(模式、窮盡性檢查)。如果你需要可變性或 builder,而那個類別又不能做成 record,
Lombok 還是贏。
Q:sealed、final 與 package-private 建構子怎麼選?
final 完全禁止繼承;package-private 建構子把繼承限制在同一個 package,但對編譯器
什麼都沒說。sealed 明確列出允許的子型別,這才是窮盡性檢查能成立的前提。
Q:虛擬執行緒會取代 reactive 框架嗎? 對大多數「一個請求一個任務」的服務來說,是的 —— 用普通、可除錯、有完整 stack trace 的阻塞式程式碼就能有同樣的擴展性。需要串流背壓語意的地方,reactive 仍然勝出。
Q:var 會讓可讀性變差嗎?
只有在右邊看不出型別的時候。var list = new ArrayList<String>() 很清楚;
var x = compute() 就不清楚。
10) 重點檢查表
[ ] LTS versions: 8, 11, 17, 21 — and what each added
[ ] var: locals only, still statically typed
[ ] Records: what is generated, and where they do NOT fit
[ ] Sealed + records + switch = exhaustive modelling of "one of N"
[ ] instanceof pattern, switch expression vs statement
[ ] List.of vs Arrays.asList vs unmodifiableList
[ ] Virtual threads: unmounting, no pooling, Semaphore for limits, pinning
[ ] Structured concurrency as scoped fork/join
參考資料
- JDK 21 release notes
- JEP 444 — Virtual Threads
- JEP 395 — Records
java_functional.md— lambda、stream、Optional