不可改變的資料載體-Record

潘家羲 Sparrow Pan

  • 恆逸教育訓練中心-資深講師
  • 技術分類:程式設計

 

 

物件導向的語言是由物件的協同作業來達成系統的功能,而物件(Object)的建立有賴於類別(Class)的定義。基於不同的目的,我們常撰寫不同的類別來完成展示邏輯、控制邏輯、商業邏輯以及資料存取邏輯等不同的程式邏輯。而在實務的應用上,當我們需要一個不可改變的資料載體(例如DTO(Data Tranfer Object)),往往得大費周章地特別為這個載體定義一個專屬的類別,既然是不可改變,這個扮演載體的類別我們希望它不能被繼承,欄位必須是唯讀的,再加上建構子、getter方法、equals()、hashCode(),、toSring()等方法,一個很制式、很固定的樣板代碼(Boilerplate code)大概會如下列範例程式所示:

package com.acme.geographic;
 
import java.util.Objects;
 
public final class Point {
private final double x;
private final double y;
private final double z;
public Point(double x, double y, double z) {
super();
this.x = x;
this.y = y;
this.z = z;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
return Double.doubleToLongBits(x) == Double.doubleToLongBits(other.x)
&& Double.doubleToLongBits(y) == Double.doubleToLongBits(other.y)
&& Double.doubleToLongBits(z) == Double.doubleToLongBits(other.z);
}
@Override
public String toString() {
return "Point [x=" + x + ", y=" + y + ", z=" + z + "]";
}	
}

撰寫這個類別對開發人員來說並不會太困難,因為只要宣告好類別跟類別的成員,剩下的事情IDE開發工具都可以幫我們搞定。诶...既然剩下的程式碼可以請IDE幫忙產生那些制式的程式碼,那是不是可以乾脆把IDE要做的事情轉移給編譯器呢? 於是Java在SE16版本推出全新的語法: record,上述的程式範例如果改成record的寫法會變成:

package com.acme.geographic.dto;
 
public record Point(double x,double y,double z) {
 
}

哇!這個簡化未免太驚人了吧!接下來我們透過Record的Client Code來觀察一下:

package com.acme.main;
 
import com.acme.geographic.dto.Point;
 
public class Test {
 
public static void main(String[] args) {
// TODO Auto-generated method stub
Point p1 = new Point(2, 4, 6);
System.out.println(p1.x());//2.0
System.out.println(p1.y());//4.0
System.out.println(p1.z());//6.0
Point p2 = new Point(1, 3, 5);
Point p3 = new Point(2, 4, 6);
System.out.println(p1);//Point[x=2.0, y=4.0, z=6.0]
System.out.println(p2);//Point[x=1.0, y=3.0, z=5.0]
System.out.println(p3);//Point[x=2.0, y=4.0, z=6.0]
System.out.println(p1.equals(p2));//false
System.out.println(p1.equals(p3));//true
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
System.out.println(p3.hashCode());
}
 
}

我們可以發現,定義了record型別後,Java的編譯器會自動產生:

  1. 全參數的建構子
  2. 所有欄位的getter方法(沒有get開頭)
  3. equals()方法
  4. hashCode()方法
  5. toString()方法

概念上很多人常會拿Record來跟Lombok做比較:

package com.acme.geographic.to;
 
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

@Getter
@EqualsAndHashCode
@ToString
@AllArgsConstructor
public final class Point {
private double x;
private double y;
private double z;
}

其實Lombok是透過@Getter、@EqualsAndHashCode、@AllArgsConstructor、@ToString這些Annotaion的宣告,來提示編譯器自動產生相關的程式碼,所以定位上Lombok仍可用來實現任意目的類別,而Record型別的設計是有限制的,包括:

  1. 不可改變,所以沒有setter
  2. getter方法不包含get前綴
  3. 可實作interface
  4. 可定義其他欄位跟方法
  5. 不能繼承
  6. 不能被繼承
  7. 可定義Compact Constructor來實現欄位的驗證

Record除了大量減少Boilerplate Code外,在角色定位上,Record較適合用在一些特殊任務上,例如DTO(Data Tranfer Object)、事件數據(Event Payload)、資料庫結果對應、方法回傳多值等應用。

若您對於學習Java有更多的興趣,歡迎參考恆逸教育訓練中心Oracle Java課程可以了解更多的技巧喔!



您可在下列課程中了解更多技巧喔!