Java Gold SE17|recordのコンストラクタ問題を解説

【図解】Java Gold SE17のrecordクラス「コンパクトコンストラクタ」解説アイキャッチ画像。上部にタイトル「【Java Gold SE17】recordのコンストラクタ問題を解説【compact constructor】」と「8年目現役エンジニアの本音で解説」。中央に対比図があり、左側は標準recordコンストラクタ(全フィールド記述、冗長)のコードとフロー、右側はコンパクトコンストラクタ(簡潔、省略可能、バリデーションに集中)のコードとフローを対比。下部には、動物キャラクターがいる「record理解への最短ルート」ロードマップ山と、金メダル(Java Gold SE17)を持った人間のエンジニアのイラストが手書き風ベクトルアートで描かれている。

Java Gold SE17のrecord問題で、次のコードを見て迷ったことはありませんか?

record Person(String name, int age) {
    Person {
        if (age < 0) {
            throw new IllegalArgumentException();
        }
    }
}

「コンストラクタなのに引数がない?」

this.age = age;を書かなくていいの?」

「recordのフィールドには、いつ値が代入される?」

Java Silverまでのコンストラクタ知識だけで考えると、少し違和感のあるコードです。

Java Gold SE17では、recordの基本構文を知っているだけでは不十分。

特に注意したいのが、コンパクトコンストラクタと正規コンストラクタの違いです。

この記事ではJava Gold SE17対策として、recordのコンストラクタをコード問題形式で解説します。

実行結果だけでなく、コンパイルエラーになるコードを見抜くポイントまで確認していきましょう。

Java Gold SE17の学習順に迷っている方は「Java Gold SE17対応の勉強法ロードマップ」も参考にしてください。

目次

recordがJava Gold SE17で狙われやすい理由

recordは、少ないコードでデータを保持するクラスを定義できるJavaの仕組みです。

通常のクラスでは、フィールドやコンストラクタ、アクセサメソッドなどを自分で記述する必要があります。

一方、recordでは次のように宣言できます。

record Person(String name, int age) {}

非常にシンプルです。

しかしJava Goldのコード問題では、単純に「recordとは何か?」だけを覚えていても対応できません。

例えば、次のようなポイントが狙われます。

  • コンストラクタは正しく宣言されているか
  • recordコンポーネントへ直接代入できるか
  • 引数を書かないコンストラクタは何か
  • フィールドへ値が代入されるタイミング
  • コンパイルエラーになる箇所はどこか

recordはコードが短いからこそ、細かい仕様を理解しているか確認しやすい機能です。

Java Gold SE17では「この構文を見たことがある」ではなく、なぜコンパイルできるのか、なぜコンパイルエラーになるのかを判断できるようにしておきましょう。

recordの基本構文

まずはrecordの基本を確認します。

record Person(String name, int age) {}

このコードでは、nameageをrecordコンポーネントとして宣言しています。

String name
int age

recordでは、各コンポーネントに対応するフィールドが暗黙的に作成されます。

ここで重要なのが、コンポーネントに対応するフィールドはprivate finalであるという点です。

イメージとしては次のようなフィールドを持っています。

private final String name;
private final int age;

そのため、recordのインスタンスを生成した後に値を変更することはできません。

Person person = new Person("Taro", 20);

例えば次のような変更はできません。

person.age = 30;

そもそもageprivateであり、さらにfinalです。

Java Goldでは、このfinalフィールドの初期化ルールとコンストラクタの書き方を組み合わせた問題が狙われます。

ここからコード問題を見ていきましょう。

【問題】recordのコンパクトコンストラクタ

次のコードを確認してください。

record Person(String name, int age) {

    Person {
        if (age < 0) {
            throw new IllegalArgumentException();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        var person = new Person("Taro", 20);
        System.out.println(person.age());
    }
}

このコードをコンパイルして実行した結果として、正しいものを選んでください。

A. 20と表示される

B. 0と表示される

C. IllegalArgumentExceptionがスローされる

D. Personコンストラクタでフィールドに値を代入していないためコンパイルエラー

少し考えてみてください。

この問題のポイントは次の部分です。

Person {
    if (age < 0) {
        throw new IllegalArgumentException();
    }
}

通常のJavaコンストラクタとは書き方が違います。

正解は「A. 20と表示される」

正解はA. 20と表示されるです。

実行結果は次の通り。

20

このPersonコンストラクタは、**コンパクトコンストラクタ(compact constructor)**です。

Person {
    if (age < 0) {
        throw new IllegalArgumentException();
    }
}

通常のコンストラクタであれば、次のように引数を記述します。

Person(String name, int age) {
}

しかしコンパクトコンストラクタでは、recordヘッダーに宣言されたコンポーネントの仮引数リストを再度記述しません。

つまり、

record Person(String name, int age)

と宣言されていれば、コンパクトコンストラクタ内でnameageを利用できます。

今回のコードでは、

if (age < 0) {
    throw new IllegalArgumentException();
}

と書くことで、record生成時に渡されたageをチェックしています。

今回のage20

そのためage < 0falseになり、例外はスローされません。

そしてコンパクトコンストラクタの本体が正常に終了すると、コンポーネントに対応するフィールドが初期化されます。

そのため、

person.age()

の結果は20です。

コンパクトコンストラクタでは引数チェックを書ける

コンパクトコンストラクタの代表的な用途が、値のチェックや正規化です。

例えば年齢に負の値を許可したくない場合。

record Person(String name, int age) {

    Person {
        if (age < 0) {
            throw new IllegalArgumentException(
                "age must be 0 or greater"
            );
        }
    }
}

次のコードを実行します。

new Person("Taro", -1);

age < 0trueになるため、IllegalArgumentExceptionがスローされます。

recordはデータを保持するために利用されることが多いため、不正な値を持つインスタンスを生成させたくない場面があります。

そこでコンパクトコンストラクタを利用して、生成時に値をチェックできます。

Java Goldでは、

コンパクトコンストラクタではrecordコンポーネントとして受け取る値をチェックできる

と覚えておきましょう。

ただし、ここで非常に重要な注意点があります。

【ひっかけ問題】フィールドへ直接代入するとどうなる?

次のコードを確認してください。

record Person(String name, int age) {

    Person {
        if (age < 0) {
            throw new IllegalArgumentException();
        }

        this.age = age;
    }
}

このコードはどうなるでしょうか?

A. 正常にコンパイルできる

B. ageが二重代入される

C. 実行時に例外が発生する

D. コンパイルエラー

正解は、D. コンパイルエラーです。

コンパクトコンストラクタの本体では、recordコンポーネントに対応するフィールドへ代入できません。

つまり、

this.age = age;

はコンパイルエラーです。

ここはJava Gold SE17でかなり迷いやすいポイント。

通常のクラスでは、

this.age = age;

というコードを何度も書いてきたはずです。

その感覚でrecordのコンパクトコンストラクタを読むと、

「フィールドへ代入しているから正しい」

と判断してしまうかもしれません。

しかしコンパクトコンストラクタでは逆です。

自分でコンポーネントフィールドへ代入しようとするとコンパイルエラーになります。

試験では、この違和感を狙った選択肢に注意してください。

recordのフィールドは暗黙的にprivate final

なぜrecordのフィールドへ自由に代入できないのでしょうか?

recordコンポーネントに対応するフィールドは、private finalです。

イメージとしては次の通り。

private final String name;
private final int age;

finalフィールドは初期化後に別の値を代入できません。

recordは、値の集合を表現するためのデータキャリアとして設計されています。

そのため、

person.age = 30;

のように生成後のフィールドを書き換えることはできません。

また、コンパクトコンストラクタではコンストラクタ本体の処理後に、各コンポーネントに対応するフィールドが初期化されます。

そのためコンパクトコンストラクタ内で、

this.age = age;

と直接代入することは禁止されています。

Java Goldのコード問題では、コンパクトコンストラクタ内にthis.を使ったフィールド代入があれば注意してください。

正規コンストラクタとの違い

では、正規コンストラクタではどうでしょうか?

recordでは次のようなコンストラクタを記述できます。

record Person(String name, int age) {

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

これはrecordの**正規コンストラクタ(canonical constructor)**です。

コンパクトコンストラクタとの違いを比較してみましょう。

項目コンパクトコンストラクタ正規コンストラクタ
引数の記述省略する記述する
書き方Person {}Person(String name, int age) {}
値チェック可能可能
コンポーネントフィールドへの代入書けない明示的に初期化する
主な用途簡潔な検証・正規化初期化処理を明示する

コンパクトコンストラクタでは、

Person {
}

と記述します。

正規コンストラクタでは、

Person(String name, int age) {
}

とrecordヘッダーに対応する仮引数を記述します。

正規コンストラクタを明示的に宣言した場合は、各コンポーネントフィールドを確実に初期化する必要があります。

this.name = name;
this.age = age;

ここが大きな違いです。

コンパクトコンストラクタならコンポーネントフィールドへ直接代入しない。

明示的な正規コンストラクタならコンポーネントフィールドを初期化する。

Java Gold対策では、この違いをセットで覚えてください。

【追加問題】このrecordはコンパイルできる?

次のコードを確認してください。

record Person(String name, int age) {

    Person(String name, int age) {
        if (age < 0) {
            throw new IllegalArgumentException();
        }
    }
}

このコードはコンパイルできるでしょうか?

正解は、コンパイルエラーです。

今回宣言しているのはコンパクトコンストラクタではありません。

Person(String name, int age)

と仮引数を明示しているため、正規コンストラクタです。

しかしコンストラクタ内では、

this.name = name;
this.age = age;

というフィールドの初期化を行っていません。

コンポーネントに対応するフィールドはfinalです。

正規コンストラクタでは、各フィールドが確実に初期化される必要があります。

コンパクトコンストラクタと同じ感覚で、

Person(String name, int age) {
    if (age < 0) {
        throw new IllegalArgumentException();
    }
}

と書くとコンパイルエラーになるため注意しましょう。

Java Gold本番での見抜き方

recordのコンストラクタ問題が出たら、最初にコンストラクタの宣言部分を確認してください。

Person {
}

ならコンパクトコンストラクタ。

Person(String name, int age) {
}

なら正規コンストラクタです。

この違いを見抜いた後に、コードを上から確認します。

Java Gold本番では次の順番がおすすめです。

  1. recordヘッダーのコンポーネントを確認する
  2. コンストラクタに仮引数が書かれているか確認する
  3. コンパクトか正規コンストラクタか判定する
  4. コンポーネントフィールドへの直接代入を確認する
  5. 例外条件を確認する
  6. 最後に出力結果を追う

特に、

this.age = age;

を見つけたら、そのコードだけを見て正しいと判断してはいけません。

まずコンストラクタの種類を確認してください。

通常のクラスやrecordの正規コンストラクタでは自然なコードでも、コンパクトコンストラクタではコンパイルエラーになります。

最初にコンストラクタの種類を判定する。

これだけでもrecord問題の選択肢をかなり絞れます。

Java Goldのコード読解問題に慣れたい方は「Java Gold SE17対応の無料問題集」で実際に問題を解いてみてください。

実務ではrecordの値チェックに使いやすい

実務目線でも、コンパクトコンストラクタは値チェックと相性が良いです。

例えば、APIで扱うデータをrecordで表現するとします。

record UserRequest(String name, int age) {

    UserRequest {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException(
                "name is required"
            );
        }

        if (age < 0) {
            throw new IllegalArgumentException(
                "age must be 0 or greater"
            );
        }
    }
}

このrecordでは、不正な値を持つUserRequestを生成できません。

new UserRequest("", 20);

名前が空なので例外が発生します。

new UserRequest("Taro", -1);

年齢が負なので例外が発生します。

recordの生成時点で、データが満たすべきルールをチェックできます。

もちろん実際のWebアプリケーションでは、Bean Validationなど別の仕組みで入力値を検証するケースもあります。

それでも、record自身が常に守るべき単純な条件をコンストラクタでチェックするという考え方は理解しておくと役立ちます。

試験知識として暗記するだけではなく、

「不正な状態のrecordを作らせないために使える」

と考えると、コンパクトコンストラクタの役割を理解しやすいでしょう。

recordの正式な仕様やコンパクトコンストラクタについては、Oracle公式のJava SE 17「Record Classes」も参考にしてください。

まとめ:record問題はコンストラクタの種類と代入ルールを見抜く

Java Gold SE17のrecord問題では、基本構文だけ覚えてもコード問題で迷います。

今回のポイントをまとめます。

  • recordコンポーネントに対応するフィールドはprivate final
  • コンパクトコンストラクタでは仮引数リストを書かない
  • コンパクトコンストラクタ内で値チェックを書ける
  • コンパクトコンストラクタではコンポーネントフィールドへ直接代入できない
  • 正規コンストラクタではrecordヘッダーに対応する仮引数を明示する
  • 明示的な正規コンストラクタではコンポーネントフィールドを確実に初期化する
  • Person {}を見たらコンパクトコンストラクタ
  • Person(String name, int age) {}なら正規コンストラクタ
  • this.age = ageがあれば、まずコンストラクタの種類を確認する

特にJava Silver取得直後は、

this.age = age;

というコードを見ると「正しいコンストラクタの書き方」と判断しがちです。

しかしrecordのコンパクトコンストラクタでは、その常識がひっかけになります。

Java Gold本番では、まずコンストラクタの種類を確認してください。

コンパクトコンストラクタなのか、正規コンストラクタなのか。

そこを見抜いてからフィールド代入と例外条件を追えば、recordのコンストラクタ問題はかなり解きやすくなります。

record以外のJava Gold頻出問題も確認したい方は、「OptionalのorElseとorElseGetの違いを問題で解説」や「Streamのdistinct・sorted・limitの実行順序を問題で解説」もあわせて参考にしてください。

さらにコード問題を解きたい方は「Java Gold SE17対応の無料問題集」で腕試ししてみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

tibiyaのアバター tibiya ITエンジニア

文系卒エンジニア(8年目)
Java / JavaScript / C# を中心に業務システム開発を経験。
Java資格・IT就活・文系エンジニアの実体験を発信しています。

コメント

コメントする

目次