☕️

Javaのレコードについてメモ:コンストラクター

2021/09/18に公開

レコードについて軽く調べてメモっておくシリーズ。
今回はコンストラクターについてです。

環境

$ java --version
java 17 2021-09-14 LTS
Java(TM) SE Runtime Environment (build 17+35-LTS-2724)
Java HotSpot(TM) 64-Bit Server VM (build 17+35-LTS-2724, mixed mode, sharing)

コンストラクターの概要

言語仕様の8.10.4. Record Constructor Declarationsにコンストラクターのことが書かれています。

ざっくりと次のような感じみたいです。

  • すべてのコンポーネントフィールドを初期化するためのパラメーターリストを受け取るコンストラクターをcanonical constructorと呼ぶっぽい
  • canonical constructorは暗黙的に宣言されるが、明示的に宣言してもよい
  • canonical constructorを明示的に宣言する場合、従来通りのコンストラクターとコンパクトコンストラクターの2通りがある
  • デフォルトコンストラクターは暗黙的に宣言されない
  • 非canonical constructorを宣言してもよいが、その先頭で明示的にcanonical constructorを呼び出す必要がある

コンパクトコンストラクターはレコードクラスの単純名のあとにブロックを置いた形式です。

public record Example(String text) {
    public Example { // コンパクトコンストラクター
        // ここで事前条件のチェックができる
        Objects.requireNonNull(text);
    }
}

コンパクトコンストラクターにはreturnを含められないようです。
Static InitializerInstance Initializerと同じですね。
ちなみに、レコードにはInstance Initializerが書けません。

シリアライズ

仕様に書かれていた次の一文が気になりました。

The serialization mechanism treats instances of a record class differently than ordinary serializable or externalizable objects. In particular, a record object is deserialized using the canonical constructor (§8.10.4).

通常、オブジェクトがデシリアライズされるときにコンストラクターは呼び出されないのですが、レコードはcanonical constructorが呼び出されるみたいです。

次のコードで確認してみました。

package example.record;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class RecordDeserializeDemo {

    record Rec(String text) implements Serializable {

        public Rec {
            System.out.println("Recのcanonical constructorが呼び出されました。text = " + text);
        }
    }

    // 比較のため通常のクラスも用意する
    static final class Obj implements Serializable {

        private final String text;

        public Obj(String text) {
            System.out.println("Objのコンストラクターが呼び出されました。text = " + text);
            this.text = text;
        }

        public String text() {
            return text;
        }
    }

    public static void main(String[] args) throws Exception {
        Rec rec1 = new Rec("demo");
        System.out.println("rec1.text = " + rec1.text());

        Obj obj1 = new Obj("demo");
        System.out.println("obj1.text = " + obj1.text());

        System.out.println("シリアライズします");
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(baos);
        out.writeObject(rec1);
        out.writeObject(obj1);
        out.flush();
        out.close();

        System.out.println("デシリアライズします");
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Rec rec2 = (Rec) in.readObject();
        System.out.println("rec2.text = " + rec2.text());

        Obj obj2 = (Obj) in.readObject();
        System.out.println("obj2.text = " + obj2.text());
    }
}

出力は次の通りでした。

Recのcanonical constructorが呼び出されました。text = demo
rec1.text = demo
Objのコンストラクターが呼び出されました。text = demo
obj1.text = demo
シリアライズします
デシリアライズします
Recのcanonical constructorが呼び出されました。text = demo
rec2.text = demo
obj2.text = demo

確かにデシリアライズ時にcanonical constructorが呼び出されていますね。

以上です。

Discussion