在本文中,我們將看到 Oracle with Java 16 如何正式引入除類、接口、枚舉和注釋之外的第五種 Java 類型:記錄類型。記錄是使用非常綜合的語法定義的特定類。它們旨在實現(xiàn)表示數(shù)據(jù)的類。
特別是,記錄旨在表示不可變的數(shù)據(jù)容器。記錄語法可幫助開發(fā)人員專注于設計數(shù)據(jù),而不會迷失在實現(xiàn)細節(jié)中。
句法
記錄的語法是最小的:
?[modifiers] record identifier (header) {[members]}
?
術(shù)語?header
?是指由逗號分隔的變量聲明列表,它將代表記錄的實例變量。一條記錄隱式定義了一個構(gòu)造函數(shù),該構(gòu)造函數(shù)將標頭作為參數(shù)列表,定義標頭中聲明的所有字段的訪問器方法,并提供?toString
?,?equals
?和?hashCode
?方法的默認實現(xiàn)。
讓我們馬上看一個例子。因此,假設我們要編寫一個拍賣畫作銷售應用程序。這些將被理解為不可變對象。事實上,一旦它們被出售,它們就無法改變。例如,一幅畫在被定義后就不能改變它的標題。然后我們可以創(chuàng)建?Painting
?記錄:
public record Painting(String title, String author, int price) { }
我們可以實例化這條記錄,就好像它是一個類,它有一個用頭參數(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ù)據(jù)容器。與枚舉一樣,記錄也通過提供比類更少冗長的語法和簡單、清晰的規(guī)則來簡化開發(fā)人員的工作。
這些記錄僅在 Java 14 中作為功能預覽引入,并在 Java 16 中正式發(fā)布。與往常一樣,Java 通過將將記錄轉(zhuǎn)換為類的任務委托給編譯器以保持與舊程序的向后兼容性來減輕這一新功能的影響。具體來說,當枚舉被編譯器轉(zhuǎn)換成擴展抽象java.lang.Enum類的類時,記錄被編譯器轉(zhuǎn)換成擴展抽象java.lang.Record類的類。
對于Enum類,編譯器將不允許開發(fā)人員創(chuàng)建直接擴展Record類的類。事實上,它也是一個特殊的類,專門為支持記錄的概念而創(chuàng)建。
當我們編譯Painting.java文件時,我們會得到Painting.class文件。在這個文件中,編譯器將插入一個Painting類(記錄轉(zhuǎn)換的結(jié)果):
- 被聲明final;
- 定義一個將標頭作為參數(shù)列表的構(gòu)造函數(shù)。
- 定義標頭中聲明的所有字段的訪問器方法。
- 覆蓋Object方法:toString,equals和hashCode。
實際上,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();
}
請注意,訪問器方法標識符不遵循我們迄今為止使用的通常約定。而不是被調(diào)用getTitle, getAuthor并且getPrice它們被簡單地稱為title,author和price,但功能保持不變。
因此,我們可以使用以下語法對記錄的各個字段進行讀取訪問:
String title = painting.title();
String author = painting.author();
如果記錄不存在
如果我們創(chuàng)建了一個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 仍然允許我們對此類進行半自動開發(fā)。
繼承與多態(tài)
記錄旨在表示攜帶不可變數(shù)據(jù)的對象。因此,記錄繼承是不可實現(xiàn)的。特別是,記錄不能擴展,因為記錄是自動聲明的final。此外,記錄不能擴展類(顯然不能擴展記錄),因為它已經(jīng)擴展了Record類。
這是一個看似有限的選擇,但它符合使用記錄的理念。記錄必須是不可變的,并且繼承與不變性不兼容。但是,通過隱式擴展Record類,記錄繼承了該類的方法。實際上,Record該類僅覆蓋了從Object該類繼承的 3 個方法:toString、equals和hashCode,并沒有定義新方法。
在記錄中,我們還可以覆蓋訪問器方法和Object編譯器在編譯時生成的三個方法。事實上,如果需要,在我們的代碼中顯式聲明它們以自定義和優(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)知道記錄和枚舉一樣,不能擴展,也不能擴展其他類或記錄。但是,記錄可以實現(xiàn)接口。
與枚舉一樣,記錄也是隱式的final,因此abstract不能使用修飾符。所以,當我們在一個記錄中實現(xiàn)一個接口時,我們必須實現(xiàn)所有繼承的方法。
自定義記錄
不可能在記錄中聲明實例變量和實例初始值設定項。這是為了不違反記錄的作用,記錄應該代表不可變數(shù)據(jù)的容器。
相反,你可以聲明靜態(tài)方法、變量和初始值設定項。事實上,這些是靜態(tài)的,由記錄的所有實例共享,并且不能訪問特定對象的實例成員。
但是自定義記錄最有趣的部分是能夠創(chuàng)建構(gòu)造函數(shù)。
我們知道,在一個類中如果不添加構(gòu)造函數(shù),編譯器會添加一個無參數(shù)的構(gòu)造函數(shù),稱為默認構(gòu)造函數(shù)。當我們在類中顯式添加構(gòu)造函數(shù)時,無論其參數(shù)數(shù)量是多少,編譯器都將不再添加默認構(gòu)造函數(shù)。
然而,在記錄中,自動添加編譯器的構(gòu)造函數(shù)將記錄頭中定義的變量定義為參數(shù)。此構(gòu)造函數(shù)稱為規(guī)范構(gòu)造函數(shù)。在它的特性中,它是唯一允許設置記錄的實例變量的構(gòu)造函數(shù)(我們很快就會看到)。也就是說,我們定義構(gòu)造函數(shù)的選項如下:
- 顯式地重新定義規(guī)范構(gòu)造函數(shù),最好使用其緊湊形式。
- 定義一個調(diào)用規(guī)范構(gòu)造函數(shù)的非規(guī)范構(gòu)造函數(shù)。
規(guī)范構(gòu)造函數(shù)
我們可以顯式聲明一個規(guī)范的構(gòu)造函數(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;
}
}
注意初始化實例變量是必須的,否則編譯器會報錯。例如,如果我們不初始化格式變量,我們將收到以下錯誤:
error: variable format might not have been initialized
}
^
1 error
在這種情況下,我們顯式地創(chuàng)建了一個規(guī)范構(gòu)造函數(shù),它必須定義在記錄頭中定義的相同參數(shù)列表。但是,我們可以通過使用其緊湊形式來更輕松地創(chuàng)建顯式規(guī)范構(gòu)造函數(shù)。
緊湊規(guī)范構(gòu)造函數(shù)
確實可以創(chuàng)建一個緊湊的規(guī)范構(gòu)造函數(shù)。它的特點是不聲明參數(shù)列表。這并不意味著它將有一個空的參數(shù)列表,而是圓括號不會出現(xiàn)在構(gòu)造函數(shù)的標識符旁邊。因此,讓我們重寫一個與前面示例等效的構(gòu)造函數(shù):
public Photo {
if (format.length() < 5) throw new IllegalArgumentException(
"Format description too short");
}
緊湊規(guī)范構(gòu)造函數(shù)的使用應被視為在記錄中顯式定義構(gòu)造函數(shù)的標準方法。請注意,甚至不需要初始化自動初始化的實例變量。更準確地說,如果我們嘗試在緊湊的規(guī)范構(gòu)造函數(shù)中初始化實例變量,我們將得到一個編譯時錯誤。
非規(guī)范構(gòu)造函數(shù)
也可以定義一個參數(shù)列表不同于規(guī)范構(gòu)造函數(shù)的構(gòu)造函數(shù),即非規(guī)范構(gòu)造函數(shù)。在這種情況下,我們正在執(zhí)行構(gòu)造函數(shù)重載。事實上,與類中默認構(gòu)造函數(shù)的情況不同,添加具有不同參數(shù)列表的構(gòu)造函數(shù)無論如何都不會阻止編譯器添加規(guī)范構(gòu)造函數(shù)。此外,非規(guī)范構(gòu)造函數(shù)必須調(diào)用另一個構(gòu)造函數(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;
}
我們會得到一個編譯時錯誤:
Error: constructor is not canonical, so its first statement must invoke another constructor
public Photo(String format, boolean color, String msg) {
^
1 error
顯然,如果我們添加另一個非規(guī)范構(gòu)造函數(shù)來調(diào)用,遲早會調(diào)用(顯式或隱式)規(guī)范構(gòu)造函數(shù)。在我們的例子中,如果我們?nèi)缓笾苯诱{(diào)用規(guī)范構(gòu)造函數(shù),我們還必須刪除設置實例變量的指令,因為這些將在非規(guī)范構(gòu)造函數(shù)的第一行中被調(diào)用后由規(guī)范構(gòu)造函數(shù)設置構(gòu)造函數(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;
}
會導致以下編譯錯誤:
error: variable format might already have been assigned
this.format = format;
^
error: variable color might already have been assigned
this.color = color;
^
2 errors
這表明這兩個變量此時已經(jīng)被初始化。這表明規(guī)范構(gòu)造函數(shù)始終負責設置記錄的實例變量。所以我們只需要刪除不必要的行:
public Photo(String format, boolean color, String msg) {
this(format, color);
if (format.length() < 5) throw new IllegalArgumentException(msg);
}
在這一點上,我們將能夠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)
何時使用記錄
何時使用記錄而不是類應該已經(jīng)很清楚了。如上所述,記錄旨在表示不可變的數(shù)據(jù)容器。記錄不能總是用來代替類,尤其是當這些類主要定義業(yè)務方法時。
然而,軟件的本質(zhì)是進化。因此,即使我們創(chuàng)建一個記錄來表示一個不可變數(shù)據(jù)的容器,也不一定有一天將其轉(zhuǎn)換為一個類是不合適的。應該引導我們更喜歡以類的形式重寫記錄的一個線索是,當我們添加了太多方法或擴展了太多接口時。在這種情況下,值得詢問記錄是否需要轉(zhuǎn)換為類。
由于其不可變的性質(zhì),記錄非常適合密封接口。此外,它通常不代表聚合大量實例變量的概念。
記錄的概念似乎非常適合稱為 DTO(數(shù)據(jù)傳輸對象的首字母縮寫詞)的設計模式的實現(xiàn)。
結(jié)論
這些記錄代表了 Java 語言向前邁出的重要一步。隨著時間的推移,這無疑是程序員最欣賞的新奇事物之一。事實上,他們將不再被迫添加Object通過 IDE繼承的常用訪問方法和方法實現(xiàn)。
無聊且通常心不在焉地執(zhí)行的操作,這也可能導致引入錯誤。特別是,記錄使我們能夠?qū)W⒂跀?shù)據(jù)的設計,而無需深入了解實現(xiàn)細節(jié),我們始終可以對其進行自定義。此外,記錄的不可變特性將指導我們編寫更簡單、更高效的程序。