Java記錄類入門:簡化的以數(shù)據(jù)為中心的Java編程

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

95-0.png

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

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

Java編譯器如何處理記錄類

傳統(tǒng)Java創(chuàng)建簡單數(shù)據(jù)類需要大量樣板代碼。以下通過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字段、構(gòu)造方法、訪問器方法,以及正確實現(xiàn)的 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("創(chuàng)建年份:" + duke.yearCreated());
    }
}

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

自定義記錄類

雖然記錄類設(shè)計簡潔,但仍可通過自定義行為增強(qiáng)功能。以下是相關(guān)示例。

緊湊型構(gòu)造方法

記錄類提供特殊的“緊湊型構(gòu)造方法”語法,無需重復(fù)參數(shù)列表即可驗證或轉(zhuǎn)換輸入?yún)?shù):

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

緊湊型構(gòu)造方法在字段初始化后、對象完全構(gòu)建前運行,非常適合用于參數(shù)驗證。此示例中省略了參數(shù)聲明,但這些參數(shù)在構(gòu)造方法內(nèi)仍隱式可用。

添加方法

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

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

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

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

使用 instanceofswitch 進(jìn)行模式匹配

Java 21中,記錄類成為模式匹配的關(guān)鍵部分,支持switch表達(dá)式、組件解構(gòu)、嵌套模式和守衛(wèi)條件。

結(jié)合增強(qiáng)的 instanceof 運算符,記錄類可在類型驗證時簡潔地提取組件:

record Person(String name, int age) {}

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

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

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);

        // 表達(dá)性強(qiáng)且類型安全的模式匹配
        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 是密封接口,僅允許 Rectangle、CircleTriangle 實現(xiàn)。由于類型集合封閉,switch表達(dá)式覆蓋所有情況,無需 default 分支。

Java中的模式匹配

若想進(jìn)一步探索記錄類與模式匹配,請參閱我的近期教程:《Java基礎(chǔ)與高級模式匹配》

將記錄類用作數(shù)據(jù)傳輸對象

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

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

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

函數(shù)式與并發(fā)編程中的記錄類

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

由于字段為final且不可變,記錄類避免了一整類線程問題。一旦構(gòu)建完成,其狀態(tài)無法更改,因此無需防御性復(fù)制或同步即可實現(xiàn)線程安全。參考以下示例:

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

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

不適用Java記錄類的場景

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

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

記錄類設(shè)計為不可變

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

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);
    }
}

記錄類不適合復(fù)雜行為建模

基于可變狀態(tài)、復(fù)雜業(yè)務(wù)邏輯或策略模式、訪問者模式、觀察者模式等設(shè)計,更適合使用傳統(tǒng)類實現(xiàn)。以下是復(fù)雜邏輯不適用于記錄類的示例:

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

    public TaxAssessment calculateTax(Income income, Residence residence) {
        // 復(fù)雜邏輯不適用于記錄類
    }
}

記錄類與某些框架不兼容

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

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

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

這些注意事項并不意味著記錄類功能不完整,而是強(qiáng)調(diào)記錄類專為特定場景設(shè)計。在某些情況下,傳統(tǒng)類仍是更實用的選擇。

Java中的記錄類與序列化

記錄類已在Java生態(tài)中被廣泛采用,其不可變性使其在持久化、配置和數(shù)據(jù)傳輸中極具吸引力。記錄類可像普通類一樣實現(xiàn) Serializable 接口??尚蛄谢挠涗涱惤M件天然適用于保存配置、恢復(fù)狀態(tài)、網(wǎng)絡(luò)傳輸數(shù)據(jù)或緩存值等場景。

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

import java.io.Serializable;

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

class Profile {
    private String bio;
}

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

class Profile implements Serializable {
    private String bio;
}

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

得益于這種廣泛采納,記錄類在實際API中作為DTO表現(xiàn)卓越:

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

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

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

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

結(jié)語

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

記錄類并非適用于所有場景,但在處理不可變數(shù)據(jù)時優(yōu)勢顯著。結(jié)合模式匹配,它們能讓代碼意圖更清晰,同時由Java編譯器處理樣板代碼。

隨著記錄類、密封類和模式匹配等技術(shù)的進(jìn)步,Java正穩(wěn)步邁向更以數(shù)據(jù)為中心的編程風(fēng)格。掌握這些工具是編寫現(xiàn)代、高表達(dá)力Java代碼的最清晰路徑之一。


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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容