傳統上Java有一種叫做POJO(Plain Old Java Object)的物件,是指定義一種專門為了封裝資料的Class,裏面只有Private Field(私有成員變數),以及對應的Getter與Setter方法的Java Class產生的物件,POJO只是一種概念沒有從語言上強制規範。從Java 16開始新增了一種新的Class稱為Record,透過強制規範與自動完成程式碼,可以稍微用來取代以前的POJO與Lombok,但無法完全取代。

本文先提供POJOLombokRecord的使用範例,再來比較他們的差別。

各種使用範例

我們假設有一個User Class,用來儲存使用者資料,裡面有Name與Address兩個Field(成員變數)

傳統POJO寫法

public class User {
    private String name;
    private String address;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
}

可以看到寫起來非常繁瑣,雖然使用Eclipse之類的IDE自動補完那一堆Getter與Setter,但是當你新增Field的時候還是要重新補完一次,甚至刪除、修改Field的話可能還要手動去調整那一堆Getter與Setter,不是很方便,也是傳統Java讓人感覺很麻煩又不好用的點。

使用Lombok

@lombok.Data
public class User {
    private String name;
    private String address;
}

可以看到改用Lombok後變得非常簡便,可以自動補完Getter、Setter、Equals、Hashcode、ToString等方法,修改了Field會隨時自動修改底下方法,在Gradle或Maven內加上依賴就可以使用了。

Visual Studio Code上也支援Lombok,Ctrl+I(Windows)Command+I(macOS)的快捷鍵也能找到這些Lombok自動補完的方法,用起來非常方便。但在Eclipse使用比較麻煩,要去Eclipse目錄安裝一些外掛,比安裝其他Eclipse外掛還麻煩。不安裝的話Eclipse會一直報錯說你使用了不存在的Method,而且我安裝成功後偶爾會失效。

我使用時習慣添加更多Lombok Annotation,可以有很多方便功能,例如以下範例。

@lombok.Data
@lombok.Builder
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public class User {
    private String name;
    private String address;
}

使用Builder的話就必須加上AllArgsConstructor,代表要自動加入一個包含所有參數的Constructor(建構子)。但依照Java語言本身的規則,設定一個Constructor之後原本的無參數Constructor就不會自動出現,因此需要無參數Constructor的話還要加上NoArgsConstructor Annotation。雖然看起來比較繁瑣,但是每次都把四行複製貼上就好,Annotation已經是全名也不用再import,不會太麻煩。

加上這四行就是為了那個好用的Builder,可以看到以下範例。

var u1 = User.builder()
    .name("klab.tw")
    .build();
var u2 = User.builder()
    .name("klab.tw")
    .address("Taiwan")
    .build();

System.out.println(u1.equals(u2));
// false
u1.setAddress("Taiwan");
System.out.println(u1.equals(u2));
// true

可以看到透過Lombok自動補上的靜態方法builder()就能任意選擇想要的Field來建構需要的物件,當你的Class有很多個Field的時候這非常好用。

補充一下在Gradle中添加Lombok依賴的方式。

plugins {
	id 'java'
}
repositories {
	mavenCentral()
}
dependencies {
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

使用Record

public record User(String name, String address) {   
}

使用Record看起來變得更簡潔了,而且也是自動有Equals、Hashcode、ToString等方法。以下提供一個範例使用範例。

var u1 = new User("klab.tw", "Taiwan");
var u2 = new User("klab.tw", "Taiwan");
var u3 = new User("klab.tw", "Asia");

System.out.println(u1.toString());
// Users[name=klab.tw, address=Taiwan]
System.out.println(u1.name());
// klab.tw
System.out.println(u1.address());
// Taiwan
System.out.println(u1.equals(u2));
// true
System.out.println(u1.equals(u3));
// false

Record有點像一個語法糖,因此裡面可以跟一般的Java Class一樣加上其他Method。但區別在Record裡面是不能新增Non-static Fields,也就是只能增加靜態的成員變數(Static Fields),編譯器會告知「User declared non-static fields n are not permitted in a record」。而且也不能在任何地方修改原本的Non-static Fields,他們全部都是Final的,編譯器會告知「The final field Users.name cannot be assigned」。以下提供一個範例來看如何為User Record增加新功能。

public record User(String name, String address) {
    private static int sayHiCount = 0;
    public String sayHi() {
        sayHiCount++;
        return "Hi! I am " + name + ", I say " + sayHiCount + " times.";
    }
}

var user = new User("klab.tw", "Taiwan");
System.out.println(user.sayHi());
// "Hi! I am klab.tw, I say 1 times."
System.out.println(user.sayHi());
// "Hi! I am klab.tw, I say 2 times."

比較

POJO與Lombok

POJO好處就是它是原生的Java程式碼,沒有安裝問題、不依賴其他JAR套件。缺點就是麻煩,有IDE幫忙還是麻煩。

Lombok好處就是可以省下太多功夫了,除了上面提到的自動補完Getter、Setter、Equals、Hashcode、ToString之外,還有Builder功能。添加依賴的部分在Gradle與Maven的幫忙下也不太麻煩,唯一問題可能是在Eclipse上使用起來有點麻煩,這也是我改用VS Code的其中一個原因。

Lombok與Record

從上面的Record範例可以看出Record很方便,從Java 16開始內建此功能,是原生功能不用安裝、依賴其他套件可以直接使用。但它不能完全取代Lombok,最主要是因為Record產生的是不可變物件(Immutable Object),就跟Java的String一樣建立後就不會再改變。除了String之外,我印象中從Java 8添加一系列Stream之後才開始有越來越多不可變物件,像是透過List.of()Map.of()方法產生出來的資料集合就不可變更,對它使用add、put之類的方法會拋出java.lang.UnsupportedOperationException。不可變物件有很多好處,但這也讓Record無法完全取代POJO與Lombok。

第二個區別是建立Record物件實例的時候必須輸入所有的參數(允許Null),跟其他Java Class Constructor(建構子)一樣不能變更參數順序,也不能少輸入參數,有這個需求用Lombok的Builder會很方便。

第三個區別是Record不能繼承,也不能被繼承。第二與第三點的限制,我想Java的想法應該是希望開發者盡量縮小每個Record的資料量,然後透過組合的方式在Record裏面又放Record。林信良老師的這篇組合優於繼承?也有提到這些概念。讓我想到在React開發的時候,官方也是推薦優先考慮HoC組合來代替繼承。

另外還一個小差別是POJO與Lombok讀取資料的方法都是getXxx()的格式,例如讀取Name是getName(),但Record的用法是name(),這點習慣不太一樣。