博客 / 詳情

返回

Java記錄類入門:簡化的以數據為中心的Java編程

記錄類聲明是一種在Java類中封裝數據同時減少樣板代碼的高效方式。本文將通過基礎及高級編程場景介紹其工作原理。

文件櫃中的文件記錄 圖片來源:Stokkete / Shutterstock*

Java記錄類是一種用於存儲數據的新型類。無需編寫構造方法、訪問器、equals()hashCode()toString() 的樣板代碼,只需聲明字段,Java編譯器便會自動處理其餘部分。本文將通過基礎與高級用例示例,以及不適用記錄類的場景,帶您全面瞭解Java記錄類。

注意:Java記錄類在JDK 16中正式定型。

Java編譯器如何處理記錄類

傳統Java創建簡單數據類需要大量樣板代碼。以下通過Java吉祥物Duke和Juggy的示例説明:

public class JavaMascot {
    private final String name;
    private final int yearCreated;

    public JavaMascot(String name, int yearCreated) {
        this.name = name;
        this.yearCreated = yearCreated;
    }

    public String getName() { return name; }
    public int getYearCreated() { return yearCreated; }

    // 為簡潔起見,省略equals、hashCode和toString方法
}

使用記錄類後,上述代碼可簡化為單行:

public record JavaMascot(String name, int yearCreated) {}

這一簡潔聲明自動提供了私有final字段、構造方法、訪問器方法,以及正確實現的 equals()hashCode()toString() 方法。

定義記錄類後,即可投入使用:

public class RecordExample {
    public static void main(String[] args) {
        JavaMascot duke = new JavaMascot("Duke", 1996);
        JavaMascot juggy1 = new JavaMascot("Juggy", 2005);
        JavaMascot juggy2 = new JavaMascot("Juggy", 2005);

        System.out.println(duke); // 輸出:JavaMascot[name=Duke, yearCreated=1996]
        System.out.println(juggy1.equals(juggy2)); // 輸出:true
        System.out.println(duke.equals(juggy1));   // 輸出:false
        System.out.println("吉祥物名稱:" + duke.name());
        System.out.println("創建年份:" + duke.yearCreated());
    }
}

記錄類自動提供有意義的字符串表示、基於值的等值比較,以及與組件名稱匹配的簡單訪問器方法。

自定義記錄類

雖然記錄類設計簡潔,但仍可通過自定義行為增強功能。以下是相關示例。

緊湊型構造方法

記錄類提供特殊的“緊湊型構造方法”語法,無需重複參數列表即可驗證或轉換輸入參數:

record JavaMascot(String name, int yearCreated) {
    // 帶驗證的緊湊型構造方法
    public JavaMascot {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("名稱不能為空");
        }
        if (yearCreated < 1995) {
            throw new IllegalArgumentException("Java吉祥物在1995年前不存在");
        }
    }
}

緊湊型構造方法在字段初始化後、對象完全構建前運行,非常適合用於參數驗證。此示例中省略了參數聲明,但這些參數在構造方法內仍隱式可用。

添加方法

我們還可以為記錄類添加方法:

record JavaMascot(String name, int yearCreated) {
    public boolean isOriginalMascot() {
        return name.equals("Duke");
    }
    
    public int yearsActive() {
        return java.time.Year.now().getValue() - yearCreated;
    }
}

通過添加方法,記錄類可在保持語法簡潔和不可變性的同時,封裝與其數據相關的行為。

接下來,我們探討記錄類更高級的用法。

使用 instanceofswitch 進行模式匹配

Java 21中,記錄類成為模式匹配的關鍵部分,支持switch表達式、組件解構、嵌套模式和守衞條件。

結合增強的 instanceof 運算符,記錄類可在類型驗證時簡潔地提取組件:

record Person(String name, int age) {}

if (obj instanceof Person person) {
    System.out.println("姓名:" + person.name());
}

再看一個經典示例。幾何形狀是展示密封接口如何與記錄類協同工作的典型例子,這種組合使模式匹配尤為清晰。Switch表達式(Java 17引入)的優雅性在此凸顯,它讓代碼簡潔且類型安全,類似於函數式語言中的代數數據類型:

sealed interface Shape permits Rectangle, Circle, Triangle {}

record Rectangle(double width, double height) implements Shape {}
record Circle(double radius) implements Shape {}
record Triangle(double base, double height) implements Shape {}

public class RecordPatternMatchingExample {
    public static void main(String[] args) {
        Shape shape = new Circle(5);

        // 表達性強且類型安全的模式匹配
        double area = switch (shape) {
            case Rectangle r -> r.width() * r.height();
            case Circle c    -> Math.PI * c.radius() * c.radius();
            case Triangle t  -> t.base() * t.height() / 2;
        };

        System.out.println("面積 = " + area);
    }
}

此例中,Shape 是密封接口,僅允許 RectangleCircleTriangle 實現。由於類型集合封閉,switch表達式覆蓋所有情況,無需 default 分支。

Java中的模式匹配

若想進一步探索記錄類與模式匹配,請參閲我的近期教程:《Java基礎與高級模式匹配》

將記錄類用作數據傳輸對象

記錄類在現代API設計(如REST、GraphQL、gRPC或服務間通信)中作為數據傳輸對象(DTO)表現卓越。其簡潔語法和內置等值比較特性,使其成為服務層間映射的理想選擇。例如:

record UserDTO(String username, String email, Set<String> roles) {}
record OrderDTO(UUID id, UserDTO user, List<ProductDTO> items, BigDecimal total) {}

DTO在微服務應用中無處不在。使用記錄類可使DTO更健壯(得益於不可變性),更簡潔(無需編寫構造方法、getter及 equals()hashCode() 等方法)

函數式與併發編程中的記錄類

作為不可變數據容器,記錄類完美契合函數式與併發編程需求。它們既可作為純函數的返回類型,也可用於流處理管道,還能安全地在線程間共享數據。

由於字段為final且不可變,記錄類避免了一整類線程問題。一旦構建完成,其狀態無法更改,因此無需防禦性複製或同步即可實現線程安全。參考以下示例:

transactions.parallelStream().mapToDouble(Transaction::amount).sum();

由於記錄類不可變,此並行計算天生具備線程安全性。

不適用Java記錄類的場景

至此,我們已瞭解記錄類的優勢,但它們並非萬能替代品。例如,所有記錄類隱式繼承 java.lang.Record,因此無法繼承其他類(但可實現接口)。在需要類繼承的場景中,記錄類並不適用。

以下是記錄類不適用的其他情況。

記錄類設計為不可變

記錄類組件始終為final,因此不適用於需要可變/有狀態對象的場景。以下示例展示了一個依賴狀態變化的可變類,而記錄類不允許此類操作:

public class GameCharacter {
    private int health;
    private Position position;

    public void takeDamage(int amount) {
        this.health = Math.max(0, this.health - amount);
    }

    public void move(int x, int y) {
        this.position = new Position(this.position.x() + x, this.position.y() + y);
    }
}

記錄類不適合複雜行為建模

基於可變狀態、複雜業務邏輯或策略模式、訪問者模式、觀察者模式等設計,更適合使用傳統類實現。以下是複雜邏輯不適用於記錄類的示例:

public class TaxCalculator {
    private final TaxRateProvider rateProvider;
    private final DeductionRegistry deductions;

    public TaxAssessment calculateTax(Income income, Residence residence) {
        // 複雜邏輯不適用於記錄類
    }
}

記錄類與某些框架不兼容

部分框架(尤其是ORM)可能無法良好支持記錄類。序列化或重度依賴反射的工具也可能存在問題。請務必檢查Java特性與技術棧的兼容性:

// 可能無法與某些ORM框架良好協作
record Employee(Long id, String name, Department department) {}

// 此時仍需使用傳統實體類
@Entity
public class Employee {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne
    private Department department;
    
    // Getter、setter、equals、hashCode等方法
}

這些注意事項並不意味着記錄類功能不完整,而是強調記錄類專為特定場景設計。在某些情況下,傳統類仍是更實用的選擇。

Java中的記錄類與序列化

記錄類已在Java生態中被廣泛採用,其不可變性使其在持久化、配置和數據傳輸中極具吸引力。記錄類可像普通類一樣實現 Serializable 接口。可序列化的記錄類組件天然適用於保存配置、恢復狀態、網絡傳輸數據或緩存值等場景。

由於記錄類字段為final且不可變,它們有助於避免可變狀態在序列化與反序列化之間發生變化引發的問題。例如:

import java.io.Serializable;

record User(String username, int age, Profile profile) implements Serializable {}

class Profile {
    private String bio;
}

此例中,Stringint 可序列化,但 Profile 不可序列化,因此 User 無法序列化。若將 Profile 也改為實現 Serializable,則 User 將完全可序列化:

class Profile implements Serializable {
    private String bio;
}

除序列化基礎外,Java生態對記錄類的支持已迅速成熟。Spring Boot、Quarkus和Jackson等流行框架均與記錄類無縫協作,大多數測試工具也是如此。

得益於這種廣泛採納,記錄類在實際API中作為DTO表現卓越:

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public OrderView getOrder(@PathVariable UUID id) {
        // 實際應用中,此數據應來自數據庫或服務
        return new OrderView(
            id,
            "Duke",
            List.of(new ItemView(UUID.randomUUID(), 2)),
            new BigDecimal("149.99")
        );
    }

    // 用於API響應的記錄類DTO
    record OrderView(UUID id, String customerName, List<ItemView> items, BigDecimal total) {}
    record ItemView(UUID productId, int quantity) {}
}

如今,大多數主流Java庫和工具已將記錄類視為一等公民。早期的質疑已基本消散,開發者正因其清晰性與安全性而廣泛接納記錄類。

結語

記錄類是Java演進過程中的重大進步。它們降低了數據類的冗餘度,並確保了不可變性和行為一致性。通過消除構造方法、訪問器及 equals()hashCode() 等方法的樣板代碼,記錄類使代碼更簡潔、表達力更強,在保持類型安全的同時契合現代實踐。

記錄類並非適用於所有場景,但在處理不可變數據時優勢顯著。結合模式匹配,它們能讓代碼意圖更清晰,同時由Java編譯器處理樣板代碼。

隨着記錄類、密封類和模式匹配等技術的進步,Java正穩步邁向更以數據為中心的編程風格。掌握這些工具是編寫現代、高表達力Java代碼的最清晰路徑之一。


【注】本文譯自:Introduction to Java records: Simplified data-centric programming in Java

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.