Java Record 筆記

Posted by Adam on August 24, 2022
# Java Record 筆記 ## 1. 基本概念 `record` 是 Java 14(preview)/16(正式)引入的語法糖,用來定義**不可變的資料載體(immutable data carrier)**。編譯器會自動幫你產生: - 全參數建構子(canonical constructor) - 每個欄位的 accessor(注意:不是 `getXxx()`,而是跟欄位同名,例如 `name()`) - `equals()` / `hashCode()`(依所有欄位比較) - `toString()`(格式:`ClassName[field1=val1, field2=val2]`) ```java public record Point(int x, int y) {} ``` 等同於手寫一大堆樣板碼的 `final class`:欄位是 `private final`,且 record 本身隱含 `final`(不能被繼承)。 --- ## 2. 建立與使用 ```java Point p1 = new Point(3, 4); Point p2 = new Point(3, 4); p1.x(); // 3,注意不是 getX() p1.y(); // 4 p1.equals(p2); // true,逐欄位比較 p1.hashCode(); // 兩者相同 p1.toString(); // "Point[x=3, y=4]" ``` --- ## 3. Compact Constructor(緊湊建構子) 想在建構時做驗證或正規化,不用重寫整個建構子簽章: ```java public record Range(int min, int max) { public Range { // 沒有參數列,直接用欄位名稱 if (min > max) { throw new IllegalArgumentException("min > max"); } } } ``` - 這裡的 `min`、`max` 是**參數**,賦值動作(`this.min = min;`)是編譯器自動補上的,寫在 compact constructor 最後。 - 也可以在裡面做正規化,例如: ```java public record Money(String currency, BigDecimal amount) { public Money { currency = currency.toUpperCase(); // 修改參數值,最後仍會自動賦值給欄位 } } ``` 也可以完全自訂全參數建構子(非 compact 形式),但就要自己手動 `this.x = x;`: ```java public record Point(int x, int y) { public Point(int x, int y) { this.x = x; this.y = y; System.out.println("created"); } } ``` --- ## 4. 額外的建構子(Overload) record 可以有多個建構子,但**其他建構子必須顯式或隱式地呼叫 canonical constructor**: ```java public record Point(int x, int y) { public Point() { this(0, 0); // 必須 delegate 到 canonical constructor } } ``` --- ## 5. 靜態欄位與靜態方法 record 可以有 static 成員(但不能有非 static 的實例欄位,除了在標頭宣告的那些): ```java public record Point(int x, int y) { public static final Point ORIGIN = new Point(0, 0); public static Point of(int x, int y) { return new Point(x, y); } } ``` --- ## 6. 自訂方法(實例方法) 可以額外加自己的方法,就像普通 class: ```java public record Point(int x, int y) { public double distanceTo(Point other) { int dx = x - other.x; int dy = y - other.y; return Math.sqrt(dx * dx + dy * dy); } } ``` --- ## 7. 覆寫自動產生的方法 `equals()`、`hashCode()`、`toString()`、accessor 都可以自行覆寫: ```java public record Point(int x, int y) { @Override public String toString() { return "(" + x + ", " + y + ")"; } // 覆寫 accessor,加驗證或轉換邏輯 public int x() { return Math.abs(x); } } ``` --- ## 8. 實作介面 record 不能 `extends` 其他 class(隱含 extends `java.lang.Record`),但可以 `implements` 介面: ```java public interface Shape { double area(); } public record Circle(double radius) implements Shape { @Override public double area() { return Math.PI * radius * radius; } } ``` --- ## 9. 巢狀 Record 與組合 ```java public record Address(String city, String street) {} public record Person(String name, Address address) {} Person p = new Person("Adam", new Address("Taipei", "Xinyi Rd")); p.address().city(); // "Taipei" ``` --- ## 10. Record + Pattern Matching(Java 21 正式) ### 10.1 `instanceof` 搭配 record pattern ```java Object obj = new Point(3, 4); if (obj instanceof Point(int x, int y)) { System.out.println(x + y); // 直接解構取值 } ``` ### 10.2 `switch` 搭配 record pattern ```java static String describe(Object obj) { return switch (obj) { case Point(int x, int y) when x == y -> "對角線上的點"; case Point(int x, int y) -> "一般點 (" + x + "," + y + ")"; case null -> "空值"; default -> "未知型別"; }; } ``` ### 10.3 巢狀解構 ```java record Line(Point start, Point end) {} if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { // 一次解構兩層 } ``` --- ## 11. 限制整理 | 限制 | 說明 | |---|---| | 不能繼承其他 class | 隱含 extends `java.lang.Record` | | 不能被繼承 | record 隱含 `final` | | 不能宣告額外的實例欄位 | 只能有標頭列出的那些欄位(可以有 static 欄位) | | 欄位隱含 `final` | 建立後不可變(immutable) | | 不能是 abstract | record 本身不能宣告為 abstract | | 可以是 local / nested / generic | `record Pair<A, B>(A first, B second) {}` 合法 | --- ## 12. 適用場景 - **DTO / API 回傳物件**:取代大量 Lombok `@Value` 或手寫 POJO。 - **Value Object**:如 `Money`、`Range`、`Coordinate` 這類「值相等即物件相等」的資料。 - **多回傳值**:方法需要回傳一組相關資料時,取代 `Map<String,Object>` 或多個 out 參數。 - **switch pattern matching 的資料建模**:搭配 sealed interface 做代數資料型別(ADT)風格的建模。 ```java sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double w, double h) implements Shape {} static double area(Shape s) { return switch (s) { case Circle(double r) -> Math.PI * r * r; case Rectangle(double w, double h) -> w * h; }; } ``` --- ## 13. 與 Lombok 的比較 | 特性 | Java record | Lombok `@Value` | |---|---|---| | 需要額外依賴 | 不用(JDK 內建) | 需要 Lombok | | 不可變 | 是 | 是 | | 可繼承 | 不可 | class 本身仍可繼承(除非搭配其他限制) | | pattern matching 支援 | 原生支援(Java 21+) | 不支援 | | 序列化 | 支援(實作 `Serializable` 即可),欄位名稱在 JSON 序列化上與一般 POJO 相容(Jackson 原生支援) | 需視設定 | --- ## 14. 常見踩坑 1. **忘記 accessor 不是 `getXxx()`**:Jackson 反序列化時通常沒問題(會抓 canonical constructor),但如果混用其他框架的 reflection 邏輯要注意。 2. **在 compact constructor 裡忘記賦值邏輯是自動的**:不要手動再寫 `this.x = x;`,否則編譯錯誤(compact constructor 裡不能出現對 final 欄位的顯式賦值)。 3. **record 用在 JPA Entity**:目前 JPA/Hibernate 對 record 支援有限(record 不可變、無 no-arg constructor),一般建議只用在 DTO 層,不要直接當 Entity。