從Java 16以後,記錄類別成為正式特性,若在網路上搜尋它的相關文件,通常會先看到自行實作equals、hashCode、toString的範例,然後強調寫這些程式碼有多麻煩,而且容易出錯,同時,還連帶說明:雖然整合開發環境可以自動產生這些方法,不過,要是修改了類別成員、卻忘了重新生成方法,會引來什麼樣的臭蟲之類,接著,當中又示範了記錄類別的語法,歌頌「多簡潔!一點也不囉嗦!」

免除語法囉嗦?

單純從編譯器會自動生成equals等方法來看,「不囉嗦」這點是真的!然而,是哪方面因為「不囉嗦」而獲得益處呢?如果只是為了可以自動生成這些方法,那麼,用Lombok就好了啊?有人會說:那不是標準,但別忘了,Java本身也有JSR269(可參考先前專欄〈從Lombok到JSR269〉)。

能自動生成相關方法,純粹就只是針對語法上來示範記錄類別,畢竟Java一直都以囉嗦聞名,能少寫一些程式碼這件事,很容易說服大家這是個重大改進的特性,程式碼示範上也很簡單,只不過若因為這樣就埋單此一特性,之後當試著將記錄類別用於POJO、JavaBean等場合時,會發現記錄類別限制很多,一點也不好用!

例如,記錄類別的狀態是不可變動(immutable)(也就不適用於各種POJO)、不能在類別本體去定義非static的值域(field)、生成的取值方法會與規範建構式(canonical constructor)的引數同名(因此它不是個JavaBean)、不能被繼承等限制,需要equals等方法的場合很多,然而,這些場合不見得需要這些限制。

如果真正要瞭解記錄類別,我們不應該從一開始就從equals等方法可以自動生成來看待它。例如,面對以下的記錄類別定義,不應該強調它的語法有多簡潔:

public record Point(float x, float y) {}

記錄類別的語義

對於方才定義的Point記錄類別,在語義上是在告訴其他人,這裡定義了點,用來記錄點的資料,資料是以x與y的順序構成,某個Point實例的x、y值,就是某點唯一的狀態,除此之外沒有別的意涵了。

如果資料必須轉換為其他的資料呢?例如,將目前的資料轉換為JSON格式的字串?資料與資料之間的轉換,確實是一個需求,因此,可以在記錄類別的本體中定義方法,例如toJSON、fromJSON:

public String toJSON() {
return "{\"x\":%f,\"y\":%f}".formatted(x(), y());
}
public static Point fromJSON(String json) {
// 剖析JSON字串得到x,y
return new Point(x, y);
}

這類需求的方法定義加到記錄類別中,在語義上仍然明確地表示了:這些方法是與資料相關的處理;那麼,記錄類別可以當成JavaBean來使用嗎?JavaBean其實有多種角色,如果JavaBean只是作為資料載體,確實可以在記錄類別中定義取值函式(Getter),而在無參數建構式中,必須以預設值呼叫規範建構式,不過,無法提供設值函式(Setter),也就是說,最多當個純取值的JavaBean。

記錄類別在語義上,就是作為不可變動的資料載體,畢竟JEP 395一開頭的摘要,就明確地寫到「記錄類別就是不可變資料的透明載體(transparent carriers )」,透明指的是,無論是在名稱、結構、狀態上,記錄類別都明確地曝露、表現出來,沒有任何隱藏。

封裝的邊界

物件導向經常談到封裝,然而,封裝的對象或意圖其實是多元的,也許是想隱藏狀態、不曝露實作、遮蔽資料的結構、管理物件複雜的生命週期、隔離物件間的相依關係等,大部分情況下,封裝都意謂著某種程度的隱蔽性,藏起什麼東西之類的。

然而,作為一個資料載體時,只是單純地將一組資料聚合在一起,這些資料在名稱、結構,以及聚合後的整體名稱(就數學上,資料的組成構成了一個集合,例如點的集合),都是透明的,物件在外觀表現上會曝露一切,白話來說,物件本身提供的API,會與物件想表現的資料耦合在一起。

這使得資料載體的封裝,與其他的封裝意圖大相徑庭,因為就其他封裝的意圖來說,往往會希望物件本身提供的API,能夠隱藏物件本身(內部)的資料等東西;資料載體本身的意圖就是曝露資料的一切,然而,就Java來說,無論是哪種封裝,都必須透過class來定義,從而才造就了簡單的需求,也需要囉嗦的定義過程。

現代許多軟體之間,有諸多資料交換的需求,在Java定義資料載體時的囉嗦,催生了記錄類別。因為資料載體在外觀表現上,就是要曝露一切,也就不能有private的值域;記錄類別必須是不可變動,因為,可變動代表有隱藏的狀態;記錄類別不可繼承,因為繼承也代表著有狀態隱藏的可能性,想想看,若能繼承Point定義Point3D,將Point3D實例傳給接受Point的方法,從方法實作的觀點來看,如何能確定它接受到的物件只有x與y呢?

在API上表現一切,就是為什麼記錄類別會需要設下相關限制的原因,而這些限制,使得equals、hashCode的產生,可以單純化(可變動、可繼承會令其複雜化的幾個原因,可參考〈物件相等性〉),這只是語義上的要求,連帶使得語法上得以獲得簡化罷了。

記錄類別並不單純是語法糖的原因,就在於其語義,也就是外觀表現上要曝露一切,不可變動等相關的限制,就是要避免隱藏狀態,由於一切都揭露了,面對一個記錄類別的實例,在拆解其狀態時,就會出現類似的流程,因而,在未來版本的Java中,會有針對記錄類別的解構模式比對來支援這個流程,也就是JEP 405提案的內容。

優先思考語義

在其他的語言中,有著類似記錄類別的元素,像是Scala的案例類別(case class)、Kotlin的資料類別(data class)等,就算是動態定型語言,例如Python,也有@dataclass裝飾器,雖然Java也從這些語言中吸收了相關經驗,不過,這些元素在各自語言中,有各自不同的語義,這也是為何Java使用record作為關鍵字作為區別,以免與其他語言中類似元素間產生混淆。

Java作為一門古老的語言,本身已經有許多語法,要加入新語法會是件慎重、需要諸多考量的任務,僅僅是為了少寫些程式的語法糖,往往不足以構成納入新語法的理由,這也是Java常被批評為保守的理由之一。

因此面對記錄類別,或者是模式匹配、彌封類別(sealed class)等新特性,最好的方式是從語義上理解它們,必要時,可以深入閱讀相對應的JEP,因為JEP往往記載了新特性提案的前因後果。

若能從語義上瞭解新特性存在的意義,語法上若是有簡化等相關優點,往往只是附帶罷了,因為理解了語義,善用相關語法,才能讓意圖更明確,也才能將相關的限制,作為避免逾越語義的防火牆,將這類特性發揮在最適合的場合。

專欄作者

熱門新聞

Advertisement