# 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。