Recordのアクセサはなぜ getName() ではなく name() なのか
目次を開く
Java の Record はなぜ getName() ではなく name() なのか。
クラスと揃えれば移行が楽では?
Java の Record を使っていたとき、ふと疑問に思いましたので調べてみました。
class Person {
private String name;
private int age;
// (略)コンストラクタ
public String getName() {
return name;
}
// ・・・
}
Person person = new Person("太郎", 25);
System.out.println(person.getName()); // 太郎
record Person(String name, int age) {}
Person person = new Person("太郎", 25);
System.out.println(person.name()); // 太郎
// ↑ なんで getName() じゃないの?
もちろんたまたまではなく、調べてみると、これは Java 設計者の意図的な選択であり、いくつかの重要な思想が背景にありました。
Record とクラスはそもそもコンセプトが違う
まず前提として、Record は「ボイラープレートを減らすためのクラス」(=従来のクラスの書き方を単に簡略化したもの)ではありません。
JEP 395(Java 16 で Record を正式導入した JEP)には、設計目標がこう書かれています。
While it is superficially tempting to treat records as primarily being about boilerplate reduction, we instead choose a more semantic goal: modeling data as data.
(訳)ボイラープレート削減が主目的ではなく、「データをデータとして表現する」 という意味論的なゴールを選んだのだ。
その定義として、Record は JEP 395 の冒頭でこう位置づけられています。
transparent carriers for immutable data
(訳)「イミュータブルなデータの透明な入れ物」
ここで transparent(透明) という言葉が重要で、Record はその内部状態を隠蔽しません。フィールド自体は private final ですが、コンポーネントごとにアクセサメソッドが自動生成され、状態の記述がそのまま API になるのです。
一方で通常のクラス(JavaBeans)は、フィールドを private で隠してアクセサ経由で外部に公開するというカプセル化を基本とします。・・・勘がよければそろそろわかってきましたね。
通常クラスのアクセサである getName() の get という動詞には、「内部に隠されたものを取り出す操作」 が表れているのです。
Record にはそもそも隠すものがない。だからアクセサは「操作」ではなく、「コンポーネントそのもの」の射影として、フィールド名だけで表現されている、ということです。
・・・あ~、まあ言われてみれば、納得。
Brian Goetz 本人の発言
get をつけなかった判断について、Java の Record 設計を主導した Brian Goetz は 2020 年の OpenJDK メーリングリスト(amber-dev, 2020-08-05)でこう説明しています。
Taking a bad library naming convention and burning it into the language forever would have been the worse choice.
(訳)悪いライブラリの命名規則を永遠に言語に焼き付けるより、そうしないほうがよかった。
さらに設計の優先順位についても明言しています。
We made the deliberate choice to design the feature for new code than for catering to the quirks of existing code.
(訳)既存コードの奇癖に合わせるより、新しいコードのために設計することを意図的に選んだ。
また、同じメールには getX と isX 問題への言及もあります。JavaBeans の命名規則では boolean は isX()、それ以外は getX() という慣習があるが、これは標準ではなく慣習であるため、エコシステム全体で一貫して適用されていない。Record に get を採用しても「完全に互換」にはなれない、と指摘しています。
さらに遡ると、2019 年以前の設計ドキュメントにも同様の意思が見られます(inspired by actual events, 2018)。
These will not be named getXxx; we are not burning the ill-advised JavaBean naming conventions into the language, no matter how much people think it already is.
(訳)ill-advised(良くない考えの)JavaBean 命名規則を言語に焼き付けることはしない。
移行コストについて
ちょっと待ってください。んなこといっても既存の JavaBeans 的クラスと Record 間の移行は大変じゃないか!
そこはどうしようもないの?
実際、Goetz はメーリングリストでこのトレードオフを認めています。
new code will pay less tax than old code, but its not the case you can't migrate to records and get some benefit.
(訳)新しいコードのほうが恩恵を受けやすいが、既存コードでも移行して利益を得ることはできる。
ただし「フレームワーク依存のコード」(JPA、Jackson 等)を移行する場合は、フレームワーク側の対応が必要になる。これについて Goetz は、フレームワークは Record に対応するはずだ(実際 Spring 等は Record に対応していますね?)とし、移行コストは一時的なものと位置づけています。
まとめ
| 通常クラス(JavaBeans) | Record | |
|---|---|---|
| 設計思想 | カプセル化・状態の隠蔽 | 透明なデータキャリア |
| フィールド | private がキホン |
自動的に private final |
| アクセサ命名 | getName() |
name() |
| ミュータビリティ | 可変 | イミュータブル |
| 用途 | 振る舞いを持つオブジェクト | 値の集合体(DTO等) |
name() という命名は、「Record はデータを隠さない」という設計思想の表れであり、getX という命名規則を言語仕様に取り込まない意図的な決断だったんですね。既存コードとの互換性よりも 「新しいコードのために正しい設計をする」 ことを優先した結果ともいえるでしょうか。
Record は JavaBeans の代替ではなく、別の概念であることは肝に銘じておくことにします。
移行の際に摩擦が生じるとすれば、それは Record が既存の慣習と意図的に一線を画しているためであり、その摩擦こそが「Record は JavaBeans ではない」ということを強く表していますね。
参考
- JEP 395: Records(Java 16、正式導入)
- Brian Goetz — amber-dev mailing list, 2020-08-05(アクセサ命名の議論)
- JLS 8.10 Record Classes