1. 擴充 record 類別:額外的方法
可以在 record 中新增方法嗎?
當然可以!Record 就像一間已裝潢好的公寓:牆面與地板都已完成,不能更改,但擺設傢俱則是自由的。你可以在 record 內宣告一般方法、靜態方法,甚至儲存常數。這表示商業邏輯不一定要放到額外的「工具」類別中 —— 也可以乾淨地直接內嵌在 record 本身。
範例:計算兩點距離的方法
假設我們有一個平面座標點的 record:
public record Point(int x, int y) {
// 額外的方法
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
現在可以這樣做:
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceTo(p2)); // 5.0
如你所見,record 類別可以用自己的方法來「擴充」—— 這非常實用!
範例:靜態方法
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
public static Rectangle square(int size) {
return new Rectangle(size, size);
}
}
現在可以用一次呼叫建立「正方形」:
Rectangle r = Rectangle.square(5);
System.out.println(r.area()); // 25
2. 精簡建構子與資料驗證
為什麼需要「精簡」建構子?
record 的典型(canonical)建構子會自動產生並將參數指派給欄位。但有時我們想加入輸入資料的檢查(例如:禁止負的座標)。
在一般類別中我們會這樣寫:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
if (x < 0 || y < 0) throw new IllegalArgumentException();
this.x = x;
this.y = y;
}
// ...
}
在 record 類別中可以宣告精簡建構子 —— 不需要重複參數清單,也不需要顯式指派欄位(編譯器會替我們完成)。
精簡建構子的語法
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative");
}
// 不需要寫:this.x = x; this.y = y;
}
}
- 建構子的參數會自動與 record 的元件對應。
- this.x = x 與 this.y = y 的指派會在建構子主體執行完畢(或成功離開)後由編譯器自動完成。
- 如果拋出例外,物件就不會被建立。
帶有檢查的例子
Point p1 = new Point(3, 5); // OK
Point p2 = new Point(-1, 2); // 會拋出 IllegalArgumentException!
能否宣告「一般」建構子?
可以!如果需要建立帶有不同參數列表或額外邏輯的建構子,請明確宣告:
public record Range(int from, int to) {
public Range(int size) {
this(0, size); // 呼叫主要建構子
}
}
3. record 類別的限制
與一般類別不同,record 類別有一系列限制。記住這點,以免對編譯錯誤感到意外。
只有元件 —— 不能新增額外的非靜態欄位
在 record 類別中不能宣告新的非靜態欄位:
public record Person(String name, int age) {
// int id; // 編譯錯誤!不可新增非靜態欄位。
}
可以宣告靜態欄位和方法:
public record Person(String name, int age) {
public static final String SPECIES = "Homo sapiens";
}
Record 總是 final
Record 類別不能作為父類別(不可被繼承),也不能顯式繼承其他類別(除了隱式繼承自 java.lang.Record)。這表示 record 類別永遠是「終點」結構。
public record User(String login) { }
// public class Admin extends User {} // 錯誤:不可繼承自 record!
可以實作介面
Record 類別可以實作介面:
public interface Printable {
void print();
}
public record Invoice(int amount) implements Printable {
@Override
public void print() {
System.out.println("金額:" + amount);
}
}
4. 範例:在實際任務中的進階 record
我們來看幾個實用範例,說明額外的方法與精簡建構子如何讓 record 類別更有用。
具有可計算方法的 record
public record Circle(double x, double y, double radius) {
public double area() {
return Math.PI * radius * radius;
}
public double distanceTo(Circle other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
具有驗證的 record
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("不正確的 email:" + value);
}
}
}
現在無法建立不正確的 email:
Email e1 = new Email("test@example.com"); // OK
Email e2 = new Email("not-an-email"); // 會拋出 IllegalArgumentException
具有附加靜態方法的 record
public record Temperature(double celsius) {
public static Temperature fromFahrenheit(double fahrenheit) {
return new Temperature((fahrenheit - 32) * 5 / 9);
}
public double toFahrenheit() {
return celsius * 9 / 5 + 32;
}
}
用法:
Temperature t = Temperature.fromFahrenheit(98.6);
System.out.println(t.celsius()); // 37.0
System.out.println(t.toFahrenheit()); // 98.6
5. 精簡建構子:細節與限制
何時該使用精簡建構子?
- 當你需要檢查資料正確性(驗證)。
- 當你需要在儲存前調整數值(例如將數字四捨五入或將字串轉為大寫)。
- 當你想避免重複列出參數清單。
注意事項
- 在精簡建構子中不可顯式指派元件值(this.x = ...),否則會導致編譯錯誤;因為編譯器會在建構子主體執行後自動完成指派。
- 在精簡建構子中不可修改參數名稱 —— 它們必須與 record 的元件名稱一致。
範例:自動四捨五入
public record Money(double amount) {
public Money {
amount = Math.round(amount * 100) / 100.0; // 四捨五入到小數點後兩位
}
}
6. 實作:擴充教學應用程式
假設我們在教學應用程式中實作銀行交易。設有一個 record 類別 Transaction,用來保存金額、匯款人與收款人。
public record Transaction(String from, String to, double amount) {
public Transaction {
if (amount <= 0) throw new IllegalArgumentException("金額必須為正數");
if (from == null || to == null) throw new IllegalArgumentException("欄位不能為 null");
}
public String description() {
return String.format("轉帳 %.2f,從 %s 到 %s", amount, from, to);
}
}
用法:
Transaction t = new Transaction("Alice", "Bob", 150.0);
System.out.println(t.description()); // 轉帳 150.00,從 Alice 到 Bob
嘗試建立不正確的交易會造成錯誤:
Transaction t2 = new Transaction("Alice", "Bob", -10.0); // IllegalArgumentException
表格:record 類別中能做與不能做的事
| 在 record 類別中可以 | 在 record 類別中不可以 |
|---|---|
| 一般方法 | 新的非靜態欄位 |
| 靜態方法與欄位 | 繼承其他類別 |
| 實作介面 | 作為其他類別的超類別 |
| 精簡與一般建構子 | 在建立後修改元件 |
| 覆寫方法 | 使用 setter |
7. 使用具非標準主體的 record 類別時的常見錯誤
錯誤 № 1:嘗試加入非靜態欄位。
新手常會嘗試在 record 類別中加入「內部邏輯的另一個欄位」—— 例如計數器或快取。這不會成功:編譯器會立刻報錯。如果你需要保存額外狀態,很可能 record 類別並不適合。
錯誤 № 2:在精簡建構子中遺漏驗證。
如果你希望物件永遠是有效的,請在精簡建構子中進行檢查。不要指望「使用者自己不會亂輸入」。
錯誤 № 3:嘗試在建立後修改元件。
record 類別的欄位是 final —— 不能直接修改,也不能透過方法修改。如果你需要可變的結構,請使用一般類別。
錯誤 № 4:在方法與建構子中重複邏輯。
有時會把檢查邏輯與計算邏輯同時放在方法與建構子裡重複一遍。最好將所有驗證集中在建構子中,而讓方法保留「純」商業邏輯。
錯誤 № 5:忽略繼承限制。
Record 類別永遠是 final —— 不能從它繼承。如果你在設計需要子類別的階層,請使用一般類別。
GO TO FULL VERSION