App下載

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

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

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

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

句法

記錄的語(yǔ)法是最小的:

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

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

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

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

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

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

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

System.out.println(painting);

將產(chǎn)生輸出:

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

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

記錄、枚舉和類(lèi)

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

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

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

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

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

實(shí)際上,JDK javap 工具允許我們Painting.class使用以下命令通過(guò)自省讀取生成的類(lèi)的結(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();
}
請(qǐng)注意,訪問(wèn)器方法標(biāo)識(shí)符不遵循我們迄今為止使用的通常約定。而不是被調(diào)用getTitle,  getAuthor并且getPrice它們被簡(jiǎn)單地稱(chēng)為title,author和price,但功能保持不變。

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

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

如果記錄不存在

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

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 + "]" ;
    }
}

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

繼承與多態(tài)

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

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

在記錄中,我們還可以覆蓋訪問(wèn)器方法和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ò)展其他類(lèi)或記錄。但是,記錄可以實(shí)現(xiàn)接口。

與枚舉一樣,記錄也是隱式的final,因此abstract不能使用修飾符。所以,當(dāng)我們?cè)谝粋€(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í)例共享,并且不能訪問(wèn)特定對(duì)象的實(shí)例成員。

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

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

然而,在記錄中,自動(dòng)添加編譯器的構(gòu)造函數(shù)將記錄頭中定義的變量定義為參數(shù)。此構(gòu)造函數(shù)稱(chēng)為規(guī)范構(gòu)造函數(shù)。在它的特性中,它是唯一允許設(shè)置記錄的實(shí)例變量的構(gòu)造函數(shù)(我們很快就會(huì)看到)。也就是說(shuō),我們定義構(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í)例變量的值之前添加一致性檢查,這會(huì)很有用。例如,考慮以下抽象照片概念的記錄,我們向其顯式添加規(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í)例變量是必須的,否則編譯器會(huì)報(bào)錯(cuò)。例如,如果我們不初始化格式變量,我們將收到以下錯(cuò)誤:

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

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

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

確實(shí)可以創(chuàng)建一個(gè)緊湊的規(guī)范構(gòu)造函數(shù)。它的特點(diǎn)是不聲明參數(shù)列表。這并不意味著它將有一個(gè)空的參數(shù)列表,而是圓括號(hào)不會(huì)出現(xiàn)在構(gòu)造函數(shù)的標(biāo)識(shí)符旁邊。因此,讓我們重寫(xiě)一個(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)方法。請(qǐng)注意,甚至不需要初始化自動(dòng)初始化的實(shí)例變量。更準(zhǔn)確地說(shuō),如果我們嘗試在緊湊的規(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í)上,與類(lèi)中默認(rèn)構(gòu)造函數(shù)的情況不同,添加具有不同參數(shù)列表的構(gòu)造函數(shù)無(wú)論如何都不會(huì)阻止編譯器添加規(guī)范構(gòu)造函數(shù)。此外,非規(guī)范構(gòu)造函數(shù)必須調(diào)用另一個(gè)構(gòu)造函數(shù)作為其第一條語(yǔ)句。事實(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;
}

我們會(huì)得到一個(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ù)來(lái)調(diào)用,遲早會(huì)調(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;
}

會(huì)導(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í)例變量。所以我們只需要?jiǎng)h除不必要的行:

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)建對(duì)象。例如:

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

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

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

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

結(jié)論

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

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


0 人點(diǎn)贊