Java 16 record 入門|record・Lombok・POJO の違いを比較
Java には昔から POJO(Plain Old Java Object)と呼ばれるパターンがあります。データ保持のためだけの class で、private なフィールドとそれに対応する getter / setter を持つものです。POJO は慣習であって言語レベルの仕組みではありません。Java 16 では新しい class として record が追加され、「データを入れる class」という概念を言語に組み込み、大量のボイラープレートを自動生成するようになりました。record は POJO や Lombok を部分的に置き換えられますが、完全には置き換えられません。
本記事ではまず POJO・Lombok・record の使用例を並べて示し、続いて違いを比較します。
使用例
User という class があり、ユーザー情報を保持するために name と address の 2 つのフィールドを持つとします。
従来の 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 を自動生成できるとはいえ、フィールドを追加するたびに生成し直さなくてはなりませんし、削除・リネームの際は自動生成済みのメソッドも手で調整する羽目になります。Java が「うるさい」と言われる定番の原因のひとつです。
Lombok を使う
@lombok.Data
public class User {
private String name;
private String address;
}
Lombok を使うと劇的に短くなります。@Data が getter・setter・equals()・hashCode()・toString() を自動生成し、フィールドを追加・削除してもこれらは自動で追従します。Gradle や Maven に依存関係を追加するだけで使えます。
Visual Studio Code は Lombok をよくサポートしており、Ctrl+I(Windows)または Cmd+I(macOS)のショートカットで自動生成されたメソッドがコード補完に出てきます。Eclipse 側はやや面倒で、Eclipse 自体のインストールディレクトリにプラグインをインストールする必要があり、普通の Eclipse プラグイン導入より手間がかかります。入れないと Eclipse は Lombok 生成メソッドを「存在しない」と誤検知してエラーを出し続けますし、私の場合インストールが成功しても時々効かなくなることがありました。
実務では、さらにいくつかの Lombok アノテーションを重ねて便利機能を追加しています。
@lombok.Data
@lombok.Builder
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public class User {
private String name;
private String address;
}
@Builder を使うには @AllArgsConstructor も必要で、全フィールドを受け取るコンストラクタを自動生成します。ただし Java のルールではコンストラクタを 1 つでも定義すると既定の無引数コンストラクタは自動生成されなくなるため、無引数コンストラクタも欲しい場合は @NoArgsConstructor も付けます。一見うるさいですが、4 行まるごとコピー&ペーストするだけで、アノテーションは完全修飾名なので import も不要。それほど手間ではありません。
この 4 行を付ける目的は、便利な Builder を手に入れるためです。以下が使用例です。
var u1 = User.builder()
.name("Alice")
.build();
var u2 = User.builder()
.name("Alice")
.address("Wonderland")
.build();
System.out.println(u1.equals(u2));
// false
u1.setAddress("Wonderland");
System.out.println(u1.equals(u2));
// true
Lombok が自動生成する静的メソッド builder() を使えば、必要なフィールドだけを選んでオブジェクトを構築できます。フィールドが多い class ではこれが本当に便利です。
参考までに、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("Alice", "Wonderland");
var u2 = new User("Alice", "Wonderland");
var u3 = new User("Alice", "Narnia");
System.out.println(u1.toString());
// User[name=Alice, address=Wonderland]
System.out.println(u1.name());
// Alice
System.out.println(u1.address());
// Wonderland
System.out.println(u1.equals(u2));
// true
System.out.println(u1.equals(u3));
// false
record はシンタックスシュガー的な位置付けで、通常の Java class と同様にメソッドを追加できます。違いは、record には非 static なフィールドを追加できず、追加できるのは static フィールドのみという点です。追加するとコンパイラが「User declared non-static fields n are not permitted in a record」と教えてくれます。さらに record コンポーネント(宣言時に書いたフィールド)はすべて final なので、どこでも再代入できません。試みると「The final field User.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("Alice", "Wonderland");
System.out.println(user.sayHi());
// "Hi! I am Alice, I say 1 times."
System.out.println(user.sayHi());
// "Hi! I am Alice, 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 は非常に便利で、Java 16 から標準機能となったため、追加のインストールや依存なしで使えます。ただし Lombok を完全には置き換えられません。最大の理由は record が生成するのはイミュータブルオブジェクトだからです。Java の String と同様、生成後に変更できません。String 以外にも、Java 8 で Stream API が入って以降、イミュータブルな型が標準ライブラリでも増えました。例えば List.of() や Map.of() が返すコレクションは変更不可で、add や put を呼ぶと java.lang.UnsupportedOperationException が飛びます。イミュータブルには多くの利点がありますが、record が POJO や Lombok を完全に置き換えられない主因でもあります。
2 つ目の違いは、record のインスタンスを生成するとき すべてのコンポーネント を渡す必要がある点(null は可)。他の Java コンストラクタと同じで、引数の順序を変えたり、一部を省略したりはできません。こうした柔軟性が欲しい場面では Lombok の Builder が活躍します。
3 つ目の違いは、record は他の class を継承できず、他の class から継承もされないこと。この 2 点目・3 点目の制限からは、Java の設計思想として「record のサイズを小さく保ち、record を別の record の中に入れる形で組み合わせよ」という意図が読み取れます。これは「継承よりコンポジション」という広く知られた原則に沿った考え方で、React でも高階コンポーネントやコンポーネントのコンポジションが継承より推奨されています。
もう 1 つ細かい違い。POJO と Lombok はデータ取得が getXxx() 形式(name は getName())なのに対し、record は name() のようにそのまま書きます。使い分けるときに少し違和感が出るポイントです。