App下載

帶你認(rèn)識新的 Java 類型:記錄類型

販賣月光的小女孩 2021-09-17 10:59:17 瀏覽數(shù) (2661)
反饋

在本文中,我們將看到 Oracle with Java 16 如何正式引入除類、接口、枚舉和注釋之外的第五種 Java 類型:記錄類型。記錄是使用非常綜合的語法定義的特定類。它們旨在實(shí)現(xiàn)表示數(shù)據(jù)的類。 

特別是,記錄旨在表示不可變的數(shù)據(jù)容器。記錄語法可幫助開發(fā)人員專注于設(shè)計(jì)數(shù)據(jù),而不會迷失在實(shí)現(xiàn)細(xì)節(jié)中。

句法

記錄的語法是最小的:

?[modifiers] record identifier (header) {[members]}?

術(shù)語?header?是指由逗號分隔的變量聲明列表,它將代表記錄的實(shí)例變量。一條記錄隱式定義了一個(gè)構(gòu)造函數(shù),該構(gòu)造函數(shù)將標(biāo)頭作為參數(shù)列表,定義標(biāo)頭中聲明的所有字段的訪問器方法,并提供?toString?,?equals?和?hashCode?方法的默認(rèn)實(shí)現(xiàn)。

讓我們馬上看一個(gè)例子。因此,假設(shè)我們要編寫一個(gè)拍賣畫作銷售應(yīng)用程序。這些將被理解為不可變對象。事實(shí)上,一旦它們被出售,它們就無法改變。例如,一幅畫在被定義后就不能改變它的標(biāo)題。然后我們可以創(chuàng)建?Painting?記錄:

public record Painting(String title, String author, int price) { }

我們可以實(shí)例化這條記錄,就好像它是一個(gè)類,它有一個(gè)用頭參數(shù)列表定義的構(gòu)造函數(shù):

Painting painting = new Painting("Camaleón", "Leonardo Furino", 1000000);

由于記錄也自動定義了toString 方法,以下代碼片段:

System.out.println(painting);

將產(chǎn)生輸出:

Painting[title=Camaleón, author=Leonardo Furino, price=1000000]

因此,記錄的明顯優(yōu)勢之一是極其綜合的語法。

記錄、枚舉和類

記錄類型和枚舉類型之間有明顯的相似之處。這兩種類型都在特定情況下替換了類。枚舉旨在表示相同類型的定義數(shù)量的常量實(shí)例。另一方面,記錄應(yīng)該代表不可變的數(shù)據(jù)容器。與枚舉一樣,記錄也通過提供比類更少冗長的語法和簡單、清晰的規(guī)則來簡化開發(fā)人員的工作。

這些記錄僅在 Java 14 中作為功能預(yù)覽引入,并在 Java 16 中正式發(fā)布。與往常一樣,Java 通過將將記錄轉(zhuǎn)換為類的任務(wù)委托給編譯器以保持與舊程序的向后兼容性來減輕這一新功能的影響。具體來說,當(dāng)枚舉被編譯器轉(zhuǎn)換成擴(kuò)展抽象java.lang.Enum類的類時(shí),記錄被編譯器轉(zhuǎn)換成擴(kuò)展抽象java.lang.Record類的類。

對于Enum類,編譯器將不允許開發(fā)人員創(chuàng)建直接擴(kuò)展Record類的類。事實(shí)上,它也是一個(gè)特殊的類,專門為支持記錄的概念而創(chuàng)建。

當(dāng)我們編譯Painting.java文件時(shí),我們會得到Painting.class文件。在這個(gè)文件中,編譯器將插入一個(gè)Painting類(記錄轉(zhuǎn)換的結(jié)果):

  • 被聲明final;
  • 定義一個(gè)將標(biāo)頭作為參數(shù)列表的構(gòu)造函數(shù)。
  • 定義標(biāo)頭中聲明的所有字段的訪問器方法。
  • 覆蓋Object方法:toString,equals和hashCode。

實(shí)際上,JDK javap 工具允許我們Painting.class使用以下命令通過自省讀取生成的類的結(jié)構(gòu):

javap Painting.class
Compiled from " Painting.java"
public final class Painting extends java.lang.Record {
  public Painting(java.lang.String, java.lang.String, int);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.String title();
  public java.lang.String author();
  public int price();
}
請注意,訪問器方法標(biāo)識符不遵循我們迄今為止使用的通常約定。而不是被調(diào)用getTitle,  getAuthor并且getPrice它們被簡單地稱為title,author和price,但功能保持不變。

因此,我們可以使用以下語法對記錄的各個(gè)字段進(jìn)行讀取訪問:

String title = painting.title(); 
String author = painting.author();

如果記錄不存在

如果我們創(chuàng)建了一個(gè)Painting與記錄等效的類,我們將不得不手動編寫以下代碼:

public final class Painting {
    private String title;
    private String author;
    private int price;

    public Painting(String title, String author, int price) {
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public String title() {
        return title;
    }

    public String author() {
        return author;
    }

    public int price() {
        return price;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((author == null) ? 0 : author.hashCode());
        result = prime * result + price;
        result = prime * result + ((title == null) ? 0 : title.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Painting other = (Painting) obj;
        if (author == null) {
            if (other.author != null)
                return false;
        } else if (!author.equals(other.author))
            return false;
        if (price != other.price)
            return false;
        if (title == null) {
            if (other.title != null)
                return false;
        } else if (!title.equals(other.title))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Painting [title=" + title + ", author=" + author + ", price=" 
            + price + "]" ;
    }
}

顯然,在這種情況下,定義記錄而不是類無疑更方便,盡管 IDE 仍然允許我們對此類進(jìn)行半自動開發(fā)。 

繼承與多態(tài)

記錄旨在表示攜帶不可變數(shù)據(jù)的對象。因此,記錄繼承是不可實(shí)現(xiàn)的。特別是,記錄不能擴(kuò)展,因?yàn)橛涗浭亲詣勇暶鞯膄inal。此外,記錄不能擴(kuò)展類(顯然不能擴(kuò)展記錄),因?yàn)樗呀?jīng)擴(kuò)展了Record類。

這是一個(gè)看似有限的選擇,但它符合使用記錄的理念。記錄必須是不可變的,并且繼承與不變性不兼容。但是,通過隱式擴(kuò)展Record類,記錄繼承了該類的方法。實(shí)際上,Record該類僅覆蓋了從Object該類繼承的 3 個(gè)方法:toString、equals和hashCode,并沒有定義新方法。

在記錄中,我們還可以覆蓋訪問器方法和Object編譯器在編譯時(shí)生成的三個(gè)方法。事實(shí)上,如果需要,在我們的代碼中顯式聲明它們以自定義和優(yōu)化它們可能很有用。例如,我們可以自定義記錄中的toString方法Painting如下:

public record Painting(String title, String author, int price) { 
    @Override 
    public String toString() {
        return "The painting " +  title + " by " + author + " costs " + price;
    }
}

我們也已經(jīng)知道記錄和枚舉一樣,不能擴(kuò)展,也不能擴(kuò)展其他類或記錄。但是,記錄可以實(shí)現(xiàn)接口。

與枚舉一樣,記錄也是隱式的final,因此abstract不能使用修飾符。所以,當(dāng)我們在一個(gè)記錄中實(shí)現(xiàn)一個(gè)接口時(shí),我們必須實(shí)現(xiàn)所有繼承的方法。

自定義記錄

不可能在記錄中聲明實(shí)例變量和實(shí)例初始值設(shè)定項(xiàng)。這是為了不違反記錄的作用,記錄應(yīng)該代表不可變數(shù)據(jù)的容器。

相反,你可以聲明靜態(tài)方法、變量和初始值設(shè)定項(xiàng)。事實(shí)上,這些是靜態(tài)的,由記錄的所有實(shí)例共享,并且不能訪問特定對象的實(shí)例成員。

但是自定義記錄最有趣的部分是能夠創(chuàng)建構(gòu)造函數(shù)。

我們知道,在一個(gè)類中如果不添加構(gòu)造函數(shù),編譯器會添加一個(gè)無參數(shù)的構(gòu)造函數(shù),稱為默認(rèn)構(gòu)造函數(shù)。當(dāng)我們在類中顯式添加構(gòu)造函數(shù)時(shí),無論其參數(shù)數(shù)量是多少,編譯器都將不再添加默認(rèn)構(gòu)造函數(shù)。

然而,在記錄中,自動添加編譯器的構(gòu)造函數(shù)將記錄頭中定義的變量定義為參數(shù)。此構(gòu)造函數(shù)稱為規(guī)范構(gòu)造函數(shù)。在它的特性中,它是唯一允許設(shè)置記錄的實(shí)例變量的構(gòu)造函數(shù)(我們很快就會看到)。也就是說,我們定義構(gòu)造函數(shù)的選項(xiàng)如下:

  • 顯式地重新定義規(guī)范構(gòu)造函數(shù),最好使用其緊湊形式。
  • 定義一個(gè)調(diào)用規(guī)范構(gòu)造函數(shù)的非規(guī)范構(gòu)造函數(shù)。

規(guī)范構(gòu)造函數(shù)

我們可以顯式聲明一個(gè)規(guī)范的構(gòu)造函數(shù)。例如,如果我們想在設(shè)置實(shí)例變量的值之前添加一致性檢查,這會很有用。例如,考慮以下抽象照片概念的記錄,我們向其顯式添加規(guī)范構(gòu)造函數(shù):

public record Photo(String format, boolean color) {
    public Photo(String format, boolean color) {
        if (format.length() < 5) throw new 
            IllegalArgumentException("Format description too short");
        this.format = format;
        this.color = color;
    }
}

注意初始化實(shí)例變量是必須的,否則編譯器會報(bào)錯(cuò)。例如,如果我們不初始化格式變量,我們將收到以下錯(cuò)誤:

error: variable format might not have been initialized
    }
    ^
1 error

在這種情況下,我們顯式地創(chuàng)建了一個(gè)規(guī)范構(gòu)造函數(shù),它必須定義在記錄頭中定義的相同參數(shù)列表。但是,我們可以通過使用其緊湊形式來更輕松地創(chuàng)建顯式規(guī)范構(gòu)造函數(shù)。 

緊湊規(guī)范構(gòu)造函數(shù)

確實(shí)可以創(chuàng)建一個(gè)緊湊的規(guī)范構(gòu)造函數(shù)。它的特點(diǎn)是不聲明參數(shù)列表。這并不意味著它將有一個(gè)空的參數(shù)列表,而是圓括號不會出現(xiàn)在構(gòu)造函數(shù)的標(biāo)識符旁邊。因此,讓我們重寫一個(gè)與前面示例等效的構(gòu)造函數(shù):

public Photo {
    if (format.length() < 5) throw new IllegalArgumentException(
        "Format description too short");
}

緊湊規(guī)范構(gòu)造函數(shù)的使用應(yīng)被視為在記錄中顯式定義構(gòu)造函數(shù)的標(biāo)準(zhǔn)方法。請注意,甚至不需要初始化自動初始化的實(shí)例變量。更準(zhǔn)確地說,如果我們嘗試在緊湊的規(guī)范構(gòu)造函數(shù)中初始化實(shí)例變量,我們將得到一個(gè)編譯時(shí)錯(cuò)誤。

非規(guī)范構(gòu)造函數(shù)

也可以定義一個(gè)參數(shù)列表不同于規(guī)范構(gòu)造函數(shù)的構(gòu)造函數(shù),即非規(guī)范構(gòu)造函數(shù)。在這種情況下,我們正在執(zhí)行構(gòu)造函數(shù)重載。事實(shí)上,與類中默認(rèn)構(gòu)造函數(shù)的情況不同,添加具有不同參數(shù)列表的構(gòu)造函數(shù)無論如何都不會阻止編譯器添加規(guī)范構(gòu)造函數(shù)。此外,非規(guī)范構(gòu)造函數(shù)必須調(diào)用另一個(gè)構(gòu)造函數(shù)作為其第一條語句。事實(shí)上,如果我們添加如下構(gòu)造函數(shù):

public Photo(String format, boolean color, boolean msg) {
    if (format.length() < 5) throw new IllegalArgumentException(msg);
    this.format = format;
    this.color = color;
}

我們會得到一個(gè)編譯時(shí)錯(cuò)誤:

Error: constructor is not canonical, so its first statement must invoke another constructor
    public Photo(String format, boolean color, String msg) {
           ^
1 error

顯然,如果我們添加另一個(gè)非規(guī)范構(gòu)造函數(shù)來調(diào)用,遲早會調(diào)用(顯式或隱式)規(guī)范構(gòu)造函數(shù)。在我們的例子中,如果我們?nèi)缓笾苯诱{(diào)用規(guī)范構(gòu)造函數(shù),我們還必須刪除設(shè)置實(shí)例變量的指令,因?yàn)檫@些將在非規(guī)范構(gòu)造函數(shù)的第一行中被調(diào)用后由規(guī)范構(gòu)造函數(shù)設(shè)置構(gòu)造函數(shù)。事實(shí)上,下面的構(gòu)造函數(shù):

public Photo(String format, boolean color, String msg) {
    this(format, color);
    if (format.length() < 5) throw new IllegalArgumentException(msg);
    this.format = format;
    this.color = color;
}

會導(dǎo)致以下編譯錯(cuò)誤:

error: variable format might already have been assigned
        this.format = format;
            ^
error: variable color might already have been assigned
        this.color = color;
            ^
2 errors

這表明這兩個(gè)變量此時(shí)已經(jīng)被初始化。這表明規(guī)范構(gòu)造函數(shù)始終負(fù)責(zé)設(shè)置記錄的實(shí)例變量。所以我們只需要刪除不必要的行:

public Photo(String format, boolean color, String msg) {

    this(format, color);

    if (format.length() < 5) throw new IllegalArgumentException(msg);

}

在這一點(diǎn)上,我們將能夠Photo使用規(guī)范構(gòu)造函數(shù)和非規(guī)范構(gòu)造函數(shù)從記錄創(chuàng)建對象。例如:

var photo1 = new Photo("Photo 1" , true); // canonical constructor
System.out.println(photo1);
var photo2 = new Photo("Photo 2" , false, "Error!"); // non-canonical constructor
System.out.println(photo2);
var photo3 = new Photo("Photo" , true, "Error!"); // non-canonical constructor
System.out.println(photo3);

前面的代碼將打印輸出:

Photo[format=Photo 1, color=true]
Photo[format=Photo 2, color=false]
Exception in thread "main" java.lang.IllegalArgumentException: Error!
	at Photo.<init>(Photo.java:8)
	at TestRecordConstructors.main(TestRecordConstructors.java:7)

何時(shí)使用記錄

何時(shí)使用記錄而不是類應(yīng)該已經(jīng)很清楚了。如上所述,記錄旨在表示不可變的數(shù)據(jù)容器。記錄不能總是用來代替類,尤其是當(dāng)這些類主要定義業(yè)務(wù)方法時(shí)。

然而,軟件的本質(zhì)是進(jìn)化。因此,即使我們創(chuàng)建一個(gè)記錄來表示一個(gè)不可變數(shù)據(jù)的容器,也不一定有一天將其轉(zhuǎn)換為一個(gè)類是不合適的。應(yīng)該引導(dǎo)我們更喜歡以類的形式重寫記錄的一個(gè)線索是,當(dāng)我們添加了太多方法或擴(kuò)展了太多接口時(shí)。在這種情況下,值得詢問記錄是否需要轉(zhuǎn)換為類。

由于其不可變的性質(zhì),記錄非常適合密封接口。此外,它通常不代表聚合大量實(shí)例變量的概念。

記錄的概念似乎非常適合稱為 DTO(數(shù)據(jù)傳輸對象的首字母縮寫詞)的設(shè)計(jì)模式的實(shí)現(xiàn)。

結(jié)論

這些記錄代表了 Java 語言向前邁出的重要一步。隨著時(shí)間的推移,這無疑是程序員最欣賞的新奇事物之一。事實(shí)上,他們將不再被迫添加Object通過 IDE繼承的常用訪問方法和方法實(shí)現(xiàn)。 

無聊且通常心不在焉地執(zhí)行的操作,這也可能導(dǎo)致引入錯(cuò)誤。特別是,記錄使我們能夠?qū)W⒂跀?shù)據(jù)的設(shè)計(jì),而無需深入了解實(shí)現(xiàn)細(xì)節(jié),我們始終可以對其進(jìn)行自定義。此外,記錄的不可變特性將指導(dǎo)我們編寫更簡單、更高效的程序。


0 人點(diǎn)贊