🍍

9.1 レコードクラス~Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

9.1 レコードクラス

チャプターの概要

このチャプターでは、プレーンでイミュータブルなクラスを簡潔に記述するためにJava 16で登場した、レコードクラスについて学びます。
レコードクラスは、今後多くのアプリケーションでの導入が見込まれる極めて有用な機能です。

9.1.1 レコードクラスの特徴

レコードクラスとは

レコードクラスとは、値を中心にした、プレーンでイミュータブルなクラスを簡潔に記述するための機能です。レコードクラスを利用すると、実開発ではたびたび登場する長くて冗長なコードを、ほんの数行で記述可能になるため、生産性が大きく向上します。この機能は、Java 16でサポートされた比較的新しいものですが、今後はさらに普及が進むものと考えられます。Java 16以降の環境では、レコードクラスの特徴を理解した上で、積極的に活用することを推奨します。

レコードクラスの宣言

ここでは、レコードクラスを作成する方法を説明します。
レコードクラスは、recordキーワードによって以下のように宣言します。

【構文】レコードクラスの宣言
record レコードクラス名(1 引数名1,2 引数名2,3 引数名3, ....) {
}

このようにrecordキーワードに続けて、レコードクラス名を記述します。またその後ろには( )を記述し、( )の中は、データ型と属性名の組み合わせを,で区切って指定します。

なおレコードクラスに付与可能な修飾子には、以下の二種類です。

  • public … すべてのクラスからアクセス可能であることを表す
  • strictfp … 浮動小数点の演算が実行環境に依存しないことを表す(本コースでは対象外)

レコードクラスの具体例

それではここで、「人物」を表すPersonレコードクラスを題材に、レコードクラスの作成方法を具体的に見ていきましょう。Personレコードクラスには、name(名前)、age(年齢)、gender(性別)という3つの属性を持つものとします。以下のコードを見てください。

pro.kensait.java.advanced.lsn_9_1_1.Person
public record Person(String name, int age, String gender) {
}

たったこれだけです。このコードは宣言された3つの要素、すなわちString型のname、int型のage、String型のgenderというフィールドを持つ、イミュータブルなクラスになります。
これは実質的には以下のようなクラスを作成した場合と、全く同じものがコンパイルされます。

public final class Person {
    // フィールド
    private final String name;
    private final int age;
    private final String gender;
    // コンストラクタ
    public Person(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    // アクセサメソッド(参照のみ)
    public String name() {
        return name;
    }
    public int age() {
        return age;
    }
    public String gender() {
        return gender;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", gender=" + gender + "]";
    }
    @Override
    public boolean equals(Object obj) {
        // 3つのフィールド(name、age、gender)から等価性を判定するロジック
        ........
    }
    @Override
    public int hashCode() {
        // 3つのフィールド(name、age、gender)からハッシュ値を算出するロジック
        ........
    }
}

このようなPersonクラスは、実際のアプリケーション開発ではしばしば登場します。PersonクラスとPersonレコードクラスを比較すると、レコードクラスによって如何にコード量を削減できるかが、よくお分かりいただけると思います。
さてPersonレコードクラスは、以下のような特徴を暗黙的に持ちます。

  • privateでfinalな、3つのフィールドを持つ(String型のname、int型のage、String型のgender)。
  • コンストラクタを持つ。必然的に引数は(String型のname、int型のage、String型のgender)の3つ。
  • 参照のみのアクセサメソッドを持つ。名前はそれぞれ、name()メソッド、age()メソッド、gender()メソッド。
    ※このようにレコードクラスの参照用アクセサメソッドの名前は、属性名と同一です。先頭にgetが付くわけではないので、注意してください。
  • 3つのフィールド(name、age、gender)を文字列かするための、toString()メソッドを持つ。
  • 3つのフィールド(name、age、gender)から等価性を判定するためのequals()メソッドを持つ。
  • 3つのフィールド(name、age、gender)からハッシュ値を算出するhashCode()メソッドを持つ。
  • このクラスを継承することはできない。

次に、このPersonレコードクラスを利用する側のコードを示します。

snippet (pro.kensait.java.advanced.lsn_9_1_1.Main)
Person alice = new Person("Alice", 25, "female");
System.out.println(alice.age()); // 25

このようにPersonレコードクラスはクラスと全く同じように扱うことができ、レコードクラスであることを意識する必要はありません。

9.1.2 レコードクラスの拡張

レコードクラスの拡張

レコードクラスは、イミュータブルである特徴から、後からインスタンスフィールドを追加することはできません。
インスタンスフィールド以外であれば、以下のような拡張が可能です。

  • 暗黙的なコンストラクタを拡張する。
  • 暗黙的なメソッドをオーバーライドする。
  • 独自のメソッドを追加する。
  • 何らかのインタフェースをimplementsする。
  • スタティックなメンバーを追加する。

コンストラクタの拡張

まずは暗黙的なコンストラクタの拡張です。レコードクラスに、コンパクトコンストラクタと呼ばれる特殊なコンストラクタを記述すると、暗黙的なコンストラクタが呼び出される前に、引数に対する処理を追加することができます。具体的には以下のコードを見てください。

snippet (pro.kensait.java.advanced.lsn_9_1_2.Person)
public record Person(String name, int age, String gender) {
    public Person { //【1】
        if (name == null) throw new IllegalArgumentException("name is null");
        if (age < 0) throw new IllegalArgumentException("age is under 0");
    }
}

コンパクトコンストラクタは、通常のコンストラクタとは異なり、引数を宣言する必要がありません【1】。このコンストラクタでは、nameフィールドやageフィールドに対する不変条件の検証を行っています。

メソッドのオーバーライドと追加

レコードクラスによって提供される暗黙的なメソッドは、オーバーライドすることができます。また独自のメソッドを追加することも可能です。具体的には以下のコードを見てください。

snippet (pro.kensait.java.advanced.lsn_9_1_2.Person)
public record Person(String name, int age, String gender) {
    @Override
    public String toString() { //【2】
        return "[ name => " + name + ", age => " + age + "]";
    }
    public void sayHello() { //【3】
        String message = "こんにちは!私は" + name + "、" + age + "歳です。";
        System.out.println(message);
    }
}

暗黙的に提供されるtoString()メソッドでは要件を満たさない場合は、このようにオーバーライドします【2】。equals()メソッド、hashCode()メソッドについても同様です。
また独自のメソッドを宣言し、その中から暗黙的に宣言されたフィールドにアクセスすることも可能です【3】。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. レコードクラスの特徴やメリットについて。
  2. レコードクラスの作成方法について。
  3. レコードクラスのコンストラクタやメソッドを拡張する方法について。

Discussion