LoginSignup
7
8

More than 3 years have passed since last update.

[Java16]新機能Recordの詳細

Posted at

この記事について

2020/3/16にリリースされたJava16の言語仕様に関わる変更について,自分の理解を深めるのを兼ねてまとめてみます.プレビュー中の機能は含みません.

出典

新機能: レコード

レコードに関する主な変更点は以下の通りです.

  • 不変データを保持するための特別なクラス「レコード」を追加する
    • アクセサやObjectから継承したメソッドを自動で実装する
    • 必要に応じてコンストラクタを独自に実装して検証・正規化を行える
    • フィールド値が変化するオブジェクトに使うものではない
    • 「ボイラープレート問題」の完全な解消を目指すものではない
    • JavaBeansの命名規則には従わない
    • 「アノテーション駆動」ではなく,昔ながらのJavaらしいやり方で実現する
  • レコードを定義する新しい構文が追加される
  • 新しい標準APIクラス java.lang.Recordが追加される
  • リフレクションAPIやclassファイル表現に,レコードに関連して追加される要素がある
  • recordだけでなくenuminterfaceをメソッド内の局所クラスとして宣言できるようになる
  • 内部クラスがstaticメンバを持てるようになる

制限付き識別子record

recordという文字列は予約語には追加されませんが,varyieldと同様に「制限付き識別子」となり,型識別子(クラス名など)では使用できません.

レコードの定義

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 なら,標準コンストラクタはpublicprotectedのどちらか.
  • 型変数を持たない
  • 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になるenuminterfaceは,メソッド内で局所クラスのように宣言することは出来ませんでした.

局所レコードの追加と同様に,暗黙にstaticであるenuminterfaceもメソッド内で局所的に宣言できるようになります.

ただし,局所クラスに明示的に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.ClassRecordComponent[] getRecordComponents()boolean isRecord() が追加されます.

他の新要素との関わり

  • sealedクラス(JEP 360)と組み合わせることで,いわゆる「代数的データ型7」が表現できる
  • 将来的に,パターンマッチングのデコンストラクションの対象になることが想定されている

両者を組み合わせてswitch式で使うことも想定されています.

おわりに

気がついた要素をあれもこれもと書いていたらかなり長くなってしまいました… 何かお気づきの点があれば,ぜひお知らせください.


  1. clone、finalize、getClass、hashCode、notify、notifyAll、toString, wait 

  2. equals,hashCode,toStringjava.lang.Recordabstractメソッドとしてオーバーライドされています 

  3. sealed classes の導入後はsealedにもなれない予定 

  4. この規定により,レコードのインスタンスが生成されるときは必ず標準コンストラクタが呼び出されます 

  5. 当然ながら,コンパクトコンストラクタを複数書くことも出来ません 

  6. メソッドのオーバーライドでthrowsの対象を追加できないのと同じ 

  7. 筆者が馴染みのあるものでは,Rustのenumなど 

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8