在本文中,我們將看到 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)單、更高效的程序。