はじめに
DZoneで JDK 14 Rampdown: Build 27 を読むと
One might say that JDK 14 Early Access Build 27 is the "records build."
と書いており、とうとう試せるようになった Records をJShellで試してみました。
なお、 JDK14 のリリース時や、その後の Records の正式リリース時に内容は変わっているかもしれませんので、ご容赦ください。
JEP 359: Records
JEP 359: Records1 はJavaの文法改善を目的としたProject Amberの1つ 2 です。
半年ごとにリリースされるようになった Java ですが、当初のもくろみどおり、いろいろモダンな言語も参考に改善が進んでいます。
そんな中で Records は、TypeScriptの Constructor Shorthand や、Kotrinの Primary Constructor のコンストラクタ引数がフィールド変数の定義を兼ねる簡単な記法2のように、 レコード名と引数(レコードコンポーネント)の定義で、データの入れ物となるデータクラスを定義する仕組みです3。
これが使えるようになると、戻り値、DTO(Data Transfer Objects)、Stream APIの操作、etc...などで「ただの"データの入れ物"を作るときにも、クラス(や適切なフィールド・メソッド)を作らないといけない」という現状に、「データの入れ物として明示的にデータクラスを作れば良い」という選択肢が加わります。
Recordsの使い方
基本的な使い方
Recordsの記法は、Qiitaでも "Amberで検討されているJava構文の変更 " や "Kotlinと今後のJavaはどっちがいい?" で解説されていますが、
record Person(String name) {};
と書くと、
final class Person {
// フィールド変数
public final String name;
// 引数つきコンストラクタ
public Person(String name) {
this.name = name;
}
// フィールド変数にあわせて Getter, hashCode, equals, toString が自動実装される
// getterはfluentな(フィールド変数名と同じ)メソッド名。
public String name() {
return this.name;
}
public int hashCode() { /* 中略、等価性を保つように実装 */ }
public boolean equals() { /* 中略、等価性を保つように実装 */ }
public String toString() { /* 中略、クラス名・フィールド名・値を返す */ }
// 以上
}
という、イミュータブルなクラスと同等のデータクラスが自動的に定義されたことになります。便利。
ただし、これは単なるクラスの速記法や、lombokの様なボイラープレートを解決する記法ではなくて4、「データの入れ物」という新しい仕組み(enumの様な制限のあるクラスの仕組み)を作ることが目的で5、冗長性の解消はあくまで結果であるとJEPにも書いてあります。
実際に使うときには、このあたりを意識した方がよさそうです。
インターフェースの宣言
classと同様に、 implements を使います。
例えば、Serializable インターフェースを定義したいときは、
record Person(String name) implements Serializable {};
と書けば
final class Person implements Serializable {
// 略
}
とシリアライズ可能なデータクラス定義されたことになります。
継承
スーパークラスになることも、サブクラスになることもできません。先述のとおり、あくまで従来のクラスの速記法ではないというスタンスです。
独自のコンストラクタやメソッドを増やす
自動的に定義されるコンストラクタやメソッド以外を増やしたい場合は、ボディ({}
の中)に記載します。
record Person(String name){
Person() {
this("Jhon Doe");
}
public int getNameLength() {
return name.length();
}
}
上記の例であれば、recordで自動定義される内容に加えて、引数なしコンストラクタ、getNameLengthメソッドが用意されます。
増やせるものは、コンストラクタ、クラスメソッド、クラスフィールド、クラスイニシャライザ、インスタンスメソッド、インスタンスイニシャライザです。データクラスの形を変えることがないよう、インスタンスフィールドは追加できません6。
JShellで使ってみる
JShellで使ってみます。試したPCには、執筆時にSDKMAN!でインストールできる JDK 14 Build 28 (14.ea.28-open
) が入っています。
java -version
openjdk version "14-ea" 2020-03-17
OpenJDK Runtime Environment (build 14-ea+28-1366)
OpenJDK 64-Bit Server VM (build 14-ea+28-1366, mixed mode, sharing)
以下、実際に再現してみたい方は jshell>
とか ...>
は除いてコピペしてみてください。
1. jshellの起動
プレビュー版の機能を有効にして、JShellを起動します。
jshell --enable-preview
2. record でデータクラスを定義してインスタンス化する
String name
をレコードコンポーネント(つまり、インスタンスフィールド)に持つ Person
データクラスを定義してみます。
jshell> record Person(String name) {} ;
特に表示無く、データクラスが完成するので、インスタンス化してみます。
jshell> var someone = new Person("Yamada");
someone ==> Person[name=Yamada]
someone変数でインスタンスが参照できるようになりました。
データを取り出したり、メソッドを使ってみましょう。
jshell> System.out.println(someone.name());
Yamada
jshell> System.out.println(someone.toString());
Person[name=Yamada]
等価性も確認してみましょう。
var other = new Person("Yamada");
...> someone.equals(other);
other ==> Person[name=Yamada]
$7 ==> true
jshell> other = new Person("Ichikawa");
...> someone.equals(other);
other ==> Person[name=Ichikawa]
$9 ==> false
インスタンスが異なっても、フィールドの値の等価性が判定できていますね。
3. インターフェースを設定してみる
Personと、SerializableなPersonを作って、インターフェース型を判定してみましょう。
someone instanceof Serializable;
| エラー:
| 不適合な型: Personをjava.io.Serializableに変換できません:
| someone instanceof Serializable;
| ^-----^
jshell> record SerializablePerson(String name) implements Serializable {} ;
...> var nextOne = new SerializablePerson("Sato");
...> nextOne instanceof Serializable;
nextOne ==> SerializablePerson[name=Sato]
$16 ==> true
implements Serializable
している SerializablePerson
のインスタンスは、 instanceof
の結果が true
になっており、インターフェースの設定が効いていることがわかります。
4. 独自のコンストラクタやメソッドを増やす
Personのボディでデフォルトコンストラクタを増やしたり、レコードコンポーネントを使うメソッドを追加してみましょう。
jshell> record Person(String name) {
...> Person() {
...> this("Jhon Doe");
...> }
...> public int getNameLength() {
...> return name.length();
...> }
...> }
Personがデフォルトコンストラクタでインスタンス化されると、Jhon Doe
で初期化されるようにしています。また、lengthを返すインスタンスメソッドも追加しています。
インスタンス化して使ってみましょう。
jshell> var someone = new Person();
...> someone.name();
...> someone.getNameLength();
someone ==> Person[name=Jhon Doe]
$3 ==> "Jhon Doe"
$4 ==> 8
デフォルトコンストラクタの設定や、増やしたメソッドが動作していますね。
5. その他
Class.isRecord()
record から作られたデータクラスかどうかは、 isRecord で調べられます。
jshell> Person.class.isRecord();
$8 ==> true
jshell> String.class.isRecord();
$9 ==> false
Class.getRecordComponents()
record から作られたデータクラスが持つレコードコンポーネントを、 RecordComponent の配列で返します。
jshell> record Range(int lo, int hi) {};
jshell> Range.class.getRecordComponents();
$12 ==> RecordComponent[2] { int lo, int hi }
6. 利用例
利用例として、JEPの背景文章にあった、Streamの中間操作の例を試してみました。
record Person(String name) {} ;
record PersonX(Person p, int hash) {
PersonX(Person p) {
this(p, p.name().toUpperCase().hashCode());
}
}
//本来はどこかのデータソースから取り出してくる想定と思います
var list = List.of(new Person("Yamada"),
new Person("Ichikawa"),
new Person("Sato"),
new Person("Tanaka"));
list.stream()
.map(PersonX::new)
.sorted(Comparator.comparingInt(PersonX::hash))
.peek(System.out::println)
.map(PersonX::p)
.collect(Collectors.toList());
JShellの結果はこちら。
list ==> [Person[name=Yamada], Person[name=Ichikawa], Pers ... ato], Person[name=Tanaka]]
PersonX[person=Person[name=Tanaka], hash=-1827701194]
PersonX[person=Person[name=Yamada], hash=-1684585447]
PersonX[person=Person[name=Ichikawa], hash=-159644485]
PersonX[person=Person[name=Sato], hash=2537801]
$16 ==> [Person[name=Tanaka], Person[name=Yamada], Person[name=Ichikawa], Person[name=Sato]]
若干、実行結果がJShellで省略されていますが、Yamada, Ichikawa, Sato, Tanakaを持つ(データ)クラスのリストを、PersonXに変換、hash順にソートする仕組みです7。
Person、PersonX は record で作られたデータクラスですが、通常のクラスとは異なるもののクラスの枠組みは外れていないので、利用時も通常のクラスと混ぜて使ったり、これまでの文法を大きく変えずに利用できます 。
旧来であれば、Person, PersonXはそれぞれクラス定義をして、ミュータブル/イミュータブルにあわせてメソッドを用意したり... という手はずが必要でしたが、「データの入れ物」として扱えるのなら、
record Person(String name) {} ;
record PersonX(Person p, int hash) {
PersonX(Person p) {
this(p, p.name().toUpperCase().hashCode());
}
}
だけで記述が済みます。
また、クラスなのか「データの入れ物」なのかの区別が必要な時も、Class#isRecord で判断できます。
このように Records を使うことで、「(単なる)データの入れ物」と従来のクラスの区別の仕方をJavaに取り入れて、かつ記述が簡潔に済む ようになりそうです。
おわりに
JShellを使って、JDK14からプレビューリリースされる Records を、JShellで体験してみました。
私も正直、最初は「ボイラープレート対策!素敵!」と思ってたんですが、JEPの文章を読んでみたり実際に使ってみると、文中にあった「enumの時と同じように現れた、新しい "データの入れ物" という仕組み」という側面の方がしっくりくるようになりました。
もちろん、Kotlinの data class をさっさと混在して使ってしまう、という手もありますが、JEP 359は、Javaが標準でも次第に使いやすくなっていく象徴の様にも思えます。
JDK14、そしてその後の正式リリースが待ち通しいです。
-
英語が苦手な方は kagamihogeの日記 - JEP 359: Records (Preview)をテキトーに訳した で概要をつかみましょう。 ↩
-
Amberの具体的な内容は きしだなおきさんの Amberで検討されているJava構文の変更 を読むとわかりやすいです。 ↩ ↩2
-
JEPの背景文章にはKotlinのdata class、Scalaの case class、C#のrecord classがデータクラスの例にあげられていました。 ↩
-
JEP 359: Recordsより、
"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. (If the semantics are right, the boilerplate will take care of itself.)" ↩ -
JEP 359: Recordsより、
Records are a new kind of type declaration in the Java language. Like an enum, a record is a restricted form of class. ↩ -
JEP 359: Recordsより、
"The record's body may declare static methods, static fields, static initializers, constructors, instance methods, instance initializers, and nested types." ↩ -
元のコードにあったTypoを修正し、わかりやすい用にlimitを外したり、peekを付けたりしています。 ↩