[Java16]新機能Recordの詳細

# この記事について

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

+ record (この記事)
+ [instanceof パターンマッチング](https://qiita.com/ReiTsukikazu/items/be973af7070c92bf87d5)
  + [型指定の制約について](https://qiita.com/ReiTsukikazu/items/bfac1d3bbac60915b73e)
+ [値ベースのクラスに対する警告](https://qiita.com/ReiTsukikazu/items/ddcfc8f279017e9ba186)

## 出典

+ [JEP 395: Records](https://openjdk.java.net/jeps/395)
+ [Java SE 16 言語仕様](https://docs.oracle.com/javase/specs/jls/se16/html/index.html)

# 新機能: レコード

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

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

## 制限付き識別子`record`

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

## レコードの定義

`record`は，`enum`と同様に特別なクラスです．
最も単純な定義は

```java
record Point(int x, int y) {}
```
のようになります．これは，次のようなクラスと機能上はほぼ同等です．

```java
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);
    }
}
```

### 用語について

```java
record Point(int x, int y) {}
```

レコードは`record`，名前(`Point`)，型引数(オプション)，ヘッダ(`(int x, int y)`)，ボディ(`{}`)からなります．ヘッダはレコードの「成分(component)」を並べたものです．

### レコードの成分について

レコード成分は0個でもよく

```java
record EmptyRecord(){}
```

のようにします．

レコード成分の名前には，`Object`クラスのメソッド名と一致するもの[^ObjectMethod]は使えません．

[^ObjectMethod]: clone、finalize、getClass、hashCode、notify、notifyAll、toString, wait

レコード成分は可変長引数にすることも出来ます．対応するフィールドやアクセサの型は配列になります．

### 自動生成メンバ

標準では，次のようなメンバがコンパイラにより自動的に生成されます．

+ 各成分と同名同型の`private final`なフィールド
+ 各成分と同名同型の`public`なアクセサ(`getX()`ではなく`x()`)
+ 標準コンストラクタ(後述)
+ `equals()`及び`hashCode()`の実装[^recordAbstractMethod]
    + 同じ型でかつすべての成分が equal であるときのみ等しいとみなされる
+ `toString()`の実装[^recordAbstractMethod]
    + レコード自体の名前と，全成分の名前および文字列表現を含む文字列を返す


[^recordAbstractMethod]: `equals`,`hashCode`,`toString` は `java.lang.Record`で`abstract`メソッドとしてオーバーライドされています

### レコードと通常のクラスの相違点

レコードには以下のような制限があります．

+ `extends`句を持てない．レコードは暗黙的に必ず`java.lang.Record`クラスを継承する
+ 暗黙に`final`であり，`abstract`にはなれない[^cannotSealed]
+ `static`でないフィールドを明示的に宣言できない
+ インスタンス初期化子(`{...}`)を含めることはできない
+ 自動的に実装されるメンバを明示的に書く場合は，型が一致していなければならない
    + ただしアノテーションは自由に追加できる
+ `native`メソッドを宣言することはできない
+ コンストラクタの実装に制約がある(後述)
+ シリアライズにおいて特別扱いされる(後述)

これらの制約により，その「状態」はヘッダのみで完全に決まり，インスタンス化した後に変化することはないというレコードの性質を実現します．

[^cannotSealed]: sealed classes の導入後は`sealed`にもなれない予定

また，レコードは後述するコンパクトなコンストラクタを持つことができます．

### レコードと通常クラスの共通点

+ トップレベルでも，ネストでも宣言できる
    + ただし，ネストされたレコードは暗黙に`static`になる
+ 総称型が使える
+ インターフェースを実装できる
+ 以下の要素を含むことが出来る
    + 通常のコンストラクタ
    + `native`でないメソッド(`static`も含む)
    + `static`初期化子(`static {...}`)
    + `static`フィールド
    + `class`, `interface`


## レコードのコンストラクタ

通常のクラスとは異なり，レコードではコンストラクタを明示的に書かない場合もデフォルトコンストラクタ(`public`で引数のない空のコンストラクタ)は生成されません．代わりに，「標準コンストラクタ」(canonical constructor)が生成されます．

標準コンストラクタは，

+ 引数のシグネチャはレコードのヘッダと同じ
+ アクセスレベルは所属`record`と同じ

というものです．自動生成される標準コンストラクタは，各引数を，対応する`private`フィールドにそのまま代入します．つまり

```java
record Sample(int i, String str){}
```

と宣言するのは，

```java
record Sample(int i, String str){
    Sample(int i, String str){
        this.i = i;
        this.str = str;
    }
}
```

と宣言するのと同じです．

標準コンストラクタは自分で書くこともできます．この場合，以下の条件があります

+ 全ての引数はレコードの成分と同じ型，<b>同じ名前</b>，同じ順序で宣言
    + 対応するレコード成分が可変長引数ならば，引数も可変長引数でなければならない．レコード成分と標準コンストラクタの引数で可変長引数と配列を入れ替えることは出来ない．
+ コンストラクタ末尾までに，全ての`private`フィールドに値が確実に代入されなければならない
+ コンストラクタのアクセスレベルはレコード本体と同じかより広いものでなければならない．例えば`protected record` なら，標準コンストラクタは`public`か`protected`のどちらか．
+ 型変数を持たない
+ `throw`句を持たない
+ 明示的なコンストラクタ呼び出し(`this(...);`や `super(...);`)を含むことはできない

レコードは標準コンストラクタ以外のコンストラクタを持つことも出来ます．ただし，その場合はコンストラクタ内の最初に必ず標準コンストラクタを`this(...);`を使って呼び出さなければなりません．[^alwaysCanonicalConstructorCalled]

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


### コンパクト標準コンストラクタ(compact cacnonical constructor)

レコード成分はヘッダに書いてあるので，標準コンストラクタでは省略することができます．この場合，暗黙に各成分と同名同型の引数が定義され，コンストラクタ末尾で対応するフィールドに自動的に代入されます．例えば，

```java
record Sample(int num){
    Sample{
        // num に対して何かする
    }
}
```

と書けば，

```java
record Sample(int num){
    Sample(int num){
        // num に対して何かする
        this.num = num;
    }
}
```
と同じになります．

コンパクトコンストラクタ内部でインスタンスフィールドへの代入を行うとコンパイルエラーになります．値を変更するときは同名の引数の方に代入します(`this`を付けなければOK)．

コンパクトコンストラクタと通常の標準コンストラクタを両方書くと，同じコンストラクタを2回定義したことになりコンパイルエラーです．[^multipleCompactConstructor]

[^multipleCompactConstructor]: 当然ながら，コンパクトコンストラクタを複数書くことも出来ません


#### コンパクトコンストラクタの利用例

引数の正当性の検証

```java
record Range(int lo, int hi) {
    Range {
        if (lo > hi) { // このlo,hiは暗黙に定義されたコンストラクタ引数(フィールドではない)
            throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
        }
    }
}
```

あるいは，引数の調整・正規化

```java
record Rational(int num, int denom) {
    Rational {
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
        // ここで自動的に this.num = num; this.denom = denom; が行われる
    }
}
```

が簡単に書けます．


## メソッドの独自実装

成分のアクセサや`equals()`等のメソッドは，自分で実装することが出来て，その場合コンパイラの自動生成よりも優先されます．しかし，返り値型やアクセスレベルは元のメソッドに準じなければなりません．また，アクセサの実装は型引数や`throws`句を持てません[^sameAsOverrideThrows]．

[^sameAsOverrideThrows]: メソッドのオーバーライドで`throws`の対象を追加できないのと同じ

アクセサの独自実装は，標準のものと同じ規則に従って振る舞うべきです．例えば，`record R(T1 c1, ..., Tn cn){}`というレコードがあり，`R`型の`null`でないインスタンス`r1`があるとき，

```java
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`操作に便利です．

```java
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`を

```java
import java.lang.Record;
```

のように明示的にインポートすることで対処できます．

## classファイルの変更

クラスが `record`であることは，新しいattribute `Record` で表現されます．この属性にはレコード成分の情報が含まれます．

## リフレクションAPIの変更

`java.lang.Class` に `RecordComponent[] getRecordComponents()` と `boolean isRecord()` が追加されます．

## 他の新要素との関わり

+ `sealed`クラス([JEP 360](https://openjdk.java.net/jeps/360))と組み合わせることで，いわゆる「代数的データ型[^algebData]」が表現できる
+ 将来的に，パターンマッチングのデコンストラクションの対象になることが想定されている

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

[^algebData]: 筆者が馴染みのあるものでは，Rustの`enum`など

# おわりに

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