この記事について
2020/3/16にリリースされたJava16の言語仕様に関わる変更について,自分の理解を深めるのを兼ねてまとめてみます.プレビュー中の機能は含みません.
- record (この記事)
- instanceof パターンマッチング
- 値ベースのクラスに対する警告
出典
新機能: レコード
レコードに関する主な変更点は以下の通りです.
- 不変データを保持するための特別なクラス「レコード」を追加する
- アクセサや
Object
から継承したメソッドを自動で実装する - 必要に応じてコンストラクタを独自に実装して検証・正規化を行える
- フィールド値が変化するオブジェクトに使うものではない
- 「ボイラープレート問題」の完全な解消を目指すものではない
- JavaBeansの命名規則には従わない
- 「アノテーション駆動」ではなく,昔ながらのJavaらしいやり方で実現する
- アクセサや
- レコードを定義する新しい構文が追加される
- 新しい標準APIクラス
java.lang.Record
が追加される - リフレクションAPIやclassファイル表現に,レコードに関連して追加される要素がある
-
record
だけでなくenum
やinterface
をメソッド内の局所クラスとして宣言できるようになる - 内部クラスが
static
メンバを持てるようになる
制限付き識別子record
record
という文字列は予約語には追加されませんが,var
やyield
と同様に「制限付き識別子」となり,型識別子(クラス名など)では使用できません.
レコードの定義
record
は,enum
と同様に特別なクラスです.
最も単純な定義は
record Point(int x, int y) {}
のようになります.これは,次のようなクラスと機能上はほぼ同等です.
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return other.x == x && other.y = y;
}
public int hashCode() {
return Objects.hash(x, y);
}
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
用語について
record Point(int x, int y) {}
レコードはrecord
,名前(Point
),型引数(オプション),ヘッダ((int x, int y)
),ボディ({}
)からなります.ヘッダはレコードの「成分(component)」を並べたものです.
レコードの成分について
レコード成分は0個でもよく
record EmptyRecord(){}
のようにします.
レコード成分の名前には,Object
クラスのメソッド名と一致するもの1は使えません.
レコード成分は可変長引数にすることも出来ます.対応するフィールドやアクセサの型は配列になります.
自動生成メンバ
標準では,次のようなメンバがコンパイラにより自動的に生成されます.
- 各成分と同名同型の
private final
なフィールド - 各成分と同名同型の
public
なアクセサ(getX()
ではなくx()
) - 標準コンストラクタ(後述)
-
equals()
及びhashCode()
の実装2- 同じ型でかつすべての成分が equal であるときのみ等しいとみなされる
-
toString()
の実装2- レコード自体の名前と,全成分の名前および文字列表現を含む文字列を返す
レコードと通常のクラスの相違点
レコードには以下のような制限があります.
-
extends
句を持てない.レコードは暗黙的に必ずjava.lang.Record
クラスを継承する - 暗黙に
final
であり,abstract
にはなれない3 -
static
でないフィールドを明示的に宣言できない - インスタンス初期化子(
{...}
)を含めることはできない - 自動的に実装されるメンバを明示的に書く場合は,型が一致していなければならない
- ただしアノテーションは自由に追加できる
-
native
メソッドを宣言することはできない - コンストラクタの実装に制約がある(後述)
- シリアライズにおいて特別扱いされる(後述)
これらの制約により,その「状態」はヘッダのみで完全に決まり,インスタンス化した後に変化することはないというレコードの性質を実現します.
また,レコードは後述するコンパクトなコンストラクタを持つことができます.
レコードと通常クラスの共通点
- トップレベルでも,ネストでも宣言できる
- ただし,ネストされたレコードは暗黙に
static
になる
- ただし,ネストされたレコードは暗黙に
- 総称型が使える
- インターフェースを実装できる
- 以下の要素を含むことが出来る
- 通常のコンストラクタ
-
native
でないメソッド(static
も含む) -
static
初期化子(static {...}
) -
static
フィールド -
class
,interface
レコードのコンストラクタ
通常のクラスとは異なり,レコードではコンストラクタを明示的に書かない場合もデフォルトコンストラクタ(public
で引数のない空のコンストラクタ)は生成されません.代わりに,「標準コンストラクタ」(canonical constructor)が生成されます.
標準コンストラクタは,
- 引数のシグネチャはレコードのヘッダと同じ
- アクセスレベルは所属
record
と同じ
というものです.自動生成される標準コンストラクタは,各引数を,対応するprivate
フィールドにそのまま代入します.つまり
record Sample(int i, String str){}
と宣言するのは,
record Sample(int i, String str){
Sample(int i, String str){
this.i = i;
this.str = str;
}
}
と宣言するのと同じです.
標準コンストラクタは自分で書くこともできます.この場合,以下の条件があります
- 全ての引数はレコードの成分と同じ型,同じ名前,同じ順序で宣言
- 対応するレコード成分が可変長引数ならば,引数も可変長引数でなければならない.レコード成分と標準コンストラクタの引数で可変長引数と配列を入れ替えることは出来ない.
- コンストラクタ末尾までに,全ての
private
フィールドに値が確実に代入されなければならない - コンストラクタのアクセスレベルはレコード本体と同じかより広いものでなければならない.例えば
protected record
なら,標準コンストラクタはpublic
かprotected
のどちらか. - 型変数を持たない
-
throw
句を持たない - 明示的なコンストラクタ呼び出し(
this(...);
やsuper(...);
)を含むことはできない
レコードは標準コンストラクタ以外のコンストラクタを持つことも出来ます.ただし,その場合はコンストラクタ内の最初に必ず標準コンストラクタをthis(...);
を使って呼び出さなければなりません.4
コンパクト標準コンストラクタ(compact cacnonical constructor)
レコード成分はヘッダに書いてあるので,標準コンストラクタでは省略することができます.この場合,暗黙に各成分と同名同型の引数が定義され,コンストラクタ末尾で対応するフィールドに自動的に代入されます.例えば,
record Sample(int num){
Sample{
// num に対して何かする
}
}
と書けば,
record Sample(int num){
Sample(int num){
// num に対して何かする
this.num = num;
}
}
と同じになります.
コンパクトコンストラクタ内部でインスタンスフィールドへの代入を行うとコンパイルエラーになります.値を変更するときは同名の引数の方に代入します(this
を付けなければOK).
コンパクトコンストラクタと通常の標準コンストラクタを両方書くと,同じコンストラクタを2回定義したことになりコンパイルエラーです.5
コンパクトコンストラクタの利用例
引数の正当性の検証
record Range(int lo, int hi) {
Range {
if (lo > hi) { // このlo,hiは暗黙に定義されたコンストラクタ引数(フィールドではない)
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}
}
あるいは,引数の調整・正規化
record Rational(int num, int denom) {
Rational {
int gcd = gcd(num, denom);
num /= gcd;
denom /= gcd;
// ここで自動的に this.num = num; this.denom = denom; が行われる
}
}
が簡単に書けます.
メソッドの独自実装
成分のアクセサやequals()
等のメソッドは,自分で実装することが出来て,その場合コンパイラの自動生成よりも優先されます.しかし,返り値型やアクセスレベルは元のメソッドに準じなければなりません.また,アクセサの実装は型引数やthrows
句を持てません6.
アクセサの独自実装は,標準のものと同じ規則に従って振る舞うべきです.例えば,record R(T1 c1, ..., Tn cn){}
というレコードがあり,R
型のnull
でないインスタンスr1
があるとき,
R r2 = new R(r1.c1(), ..., r1.cn());
で生成したインスタンスr2
は必ずr1.equals(r2)
を満たさなければなりません.
コンパイラ生成のアクセサを独自実装で上書きするという意味で,ユーザーが実装するアクセサに@Override
をつけることが出来ます.
equals
,hashCode
,toString
の自動実装はフィールドを参照するので,アクセサでだけ値を変更してもこれらのメソッドには反映されません.
アノテーション
通常クラスと同様に,レコード全体やメンバにアノテーションをつけることが出来ます.また,ヘッダ内の各成分にもアノテーションを付与することが出来ます.
成分に付与されたアノテーションは,自動生成される要素にも(@Target
指定の範囲で)可能な限り伝播します.対象となるのは,
- フィールドに付与可能なら,対応する
private final
フィールド - メソッドに付与可能なら,対応するアクセサ
- 仮引数に付与可能なら,コンパクトコンストラクタ,あるいは自動生成される標準コンストラクタの対応する引数
- 型に付与可能なら,
- 対応するフィールドの型
- 対応するアクセサの返り値型
- 標準コンストラクタの対応する仮引数の型
- レコード成分自体の型(リフレクションでアクセスできる)
明示的に実装したアクセサやコンパクトでない標準コンストラクタには,アノテーションは伝播しません.明示的に指定したアノテーションは適用されます.
リフレクションで得られるレコード成分の型にアノテーションが含まれるためには,@Target(RECORD_COMPONENT)
が指定されていなければなりません.
シリアライズ
レコードはjava.lang.Serializable
を実装することでシリアライズと復元が可能ですが,その扱いは通常のクラスとは異なります.
- 各成分のシリアライズの方法は完全に各成分に委譲される
- 復元時は(デフォルトコンストラクタではなく)標準コンストラクタが使用される
- レコードの循環参照は保存されない
-
writeObject
,readObject
,readObjectNoData
,writeExternal
,readExternal
によるカスタマイズは不可(これらのメソッドがあっても無視される) -
writeReplace()
とreadResolve()
は利用可能 -
serialVersionUID
は明示しない限り0L
である.レコードに関してはserialVersionUID
が一致していなくてもよい
局所レコード
通常クラスと同様に,メソッドの内部でもレコードを宣言できます(local record,局所レコード).これは例えばStream
操作に便利です.
List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
// Local record
record MerchantSales(Merchant merchant, double sales) {}
return merchants.stream()
.map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
.sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
.map(MerchantSales::merchant)
.collect(toList());
}
局所レコードは暗黙的にstatic
であり,含まれているメソッド内の引数を内部で利用することはできません.
static
な局所クラス
従来は,ネスト時に暗黙的にstatic
になるenum
やinterface
は,メソッド内で局所クラスのように宣言することは出来ませんでした.
局所レコードの追加と同様に,暗黙にstatic
であるenum
やinterface
もメソッド内で局所的に宣言できるようになります.
ただし,局所クラスに明示的にstatic
を付けるのはJava16でもコンパイルエラーになります.
内部クラスのstatic
メンバ
従来は,('static'でない)内部クラスは(定数を除き)static
メンバを持つことは出来ませんでした.
内部クラスがrecord
を持てるようにするためこの制限が緩和され,(レコードに限らず)static
メンバを持てるようになりました.
互換性について
java.lang.Record
クラス
暗黙にインポートされるjava.lang
に新しいクラスが追加されるので,既存のRecord
というクラスをワイルドカードでインポートしていた場合コンパイルできなくなります.使用したい方のRecord
を
import java.lang.Record;
のように明示的にインポートすることで対処できます.
classファイルの変更
クラスが record
であることは,新しいattribute Record
で表現されます.この属性にはレコード成分の情報が含まれます.
リフレクションAPIの変更
java.lang.Class
に RecordComponent[] getRecordComponents()
と boolean isRecord()
が追加されます.
他の新要素との関わり
両者を組み合わせてswitch
式で使うことも想定されています.
おわりに
気がついた要素をあれもこれもと書いていたらかなり長くなってしまいました… 何かお気づきの点があれば,ぜひお知らせください.
-
clone、finalize、getClass、hashCode、notify、notifyAll、toString, wait ↩
-
equals
,hashCode
,toString
はjava.lang.Record
でabstract
メソッドとしてオーバーライドされています ↩ ↩2 -
sealed classes の導入後は
sealed
にもなれない予定 ↩ -
この規定により,レコードのインスタンスが生成されるときは必ず標準コンストラクタが呼び出されます ↩
-
当然ながら,コンパクトコンストラクタを複数書くことも出来ません ↩
-
メソッドのオーバーライドで
throws
の対象を追加できないのと同じ ↩ -
筆者が馴染みのあるものでは,Rustの
enum
など ↩