JavaでRecord型をマスターする12のステップ

JavaのRecord型をイラスト付きで解説する記事のサムネイルJava
この記事は約19分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

この記事を読めばJavaのRecord型の使い方や応用技法を身につけることができるようになります。

特にプログラミングが初めての方でもわかりやすいように、Record型の基本から、応用例、注意点、カスタマイズ方法までを手順を踏んで詳しく説明します。

コードサンプルも豊富に用意していますので、理解が深まること間違いなしです。

●JavaのRecord型とは

JavaのRecord型とは、不変性を保ったデータクラスを簡単に作るための新しい言語機能です。

この機能はJava 16から追加され、その後もバージョンアップを重ねているため、最新のJavaを使えばこの素晴らしい機能を手軽に試すことができます。

○Record型の概要とメリット

Record型は、不変のデータを格納するための軽量なクラスを手軽に生成できる機能です。

この機能があると、たとえば”名前”と”年齢”を持つPersonクラスを作る際に、各フィールドに対するゲッターメソッドや、equalshashCodetoStringなどのメソッドを自動で生成してくれます。

メリットとしては、

  1. コード量の削減:複数のメソッドを手動で追加する手間が省ける。
  2. 不変性:フィールドがfinalであり、一度設定されたら変更が不可能。
  3. クリアなコード:構造がシンプルになるので、コードが読みやすくなる。

といった点が挙げられます。

○JavaでのRecord型の登場背景

Javaでは、長らくデータクラスを手動で定義する必要がありました。

しかし、KotlinやScalaなどの近年のプログラミング言語では、このようなデータクラスを簡単に生成できる機能があり、それが高く評価されています。

Javaもこれに続き、より効率的なプログラミングを可能にするためにRecord型が導入されました。

●Record型の基本的な使い方

JavaのRecord型を使うことで、データクラスの作成が一気に簡単になります。

それでは、具体的なコードを見ながら基本的な使い方について説明していきましょう。

○サンプルコード1:Record型の基本的な宣言と使用

Record型の基本的な宣言と使用方法を見ていきます。

Record型を使って「Person」という名前と「年齢」を持つクラスを作ってみましょう。

public record Person(String name, int age) {
}

このコードは、PersonというRecord型を作っています。

この例ではnameageという2つのフィールドを持つPersonクラスが短いコードで生成されます。

このコードを実行すると、特に出力はありませんが、背後で次のようなメソッドが自動的に生成されます。

  • name()age()というアクセサメソッド(ゲッターメソッド)
  • equals()hashCode()toString()メソッド

○サンプルコード2:Record型でのアクセサメソッドの使用

上で生成したPersonクラスのアクセサメソッドを使ってみます。

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

このコードは、Personクラスのインスタンスを生成して、そのnameageを出力するプログラムです。

コードを実行すると、次のような出力がされます。

John
25

つまり、name()メソッドとage()メソッドでPersonオブジェクトから名前と年齢を取得できたわけです。

○サンプルコード3:Record型とコンストラクタ

Record型ではコンストラクタも自動的に生成されます。

しかし、独自の処理を加えたい場合は、コンストラクタをカスタマイズすることもできます。

public record Person(String name, int age) {
  public Person {
    if (age < 0) {
      throw new IllegalArgumentException("年齢は0以上でなければなりません");
    }
  }
}

このコードでは年齢が0未満の場合に例外をスローするようにコンストラクタをカスタマイズしています。

このようにして、Record型でも独自のロジックを組み込むことが可能です。

もしこのコードで年齢に負の値を設定しようとすると、IllegalArgumentExceptionが発生するという動きになります。

●Record型の応用例

Record型はその基本的な用途だけでなく、さまざまな応用例が考えられます。

その一部を具体的なサンプルコードとともに解説していきます。

○サンプルコード4:Record型を使ったリストの操作

Record型を使ってオブジェクトのリストを扱ってみましょう。

この例ではPersonクラスのオブジェクトをリストで管理する場合を考えます。

import java.util.List;

public class Main {
  public static void main(String[] args) {
    List<Person> personList = List.of(
      new Person("John", 25),
      new Person("Emily", 30),
      new Person("Tom", 35)
    );

    personList.forEach(person -> System.out.println(person.name() + ": " + person.age()));
  }
}

このコードはPersonオブジェクトを格納するListを作成し、その後でリスト内の各Personnameageを出力しています。

実行すると、各Personオブジェクトのnameageが表示されます。

○サンプルコード5:Record型とストリームの組み合わせ

JavaのストリームAPIと組み合わせることで、Record型のデータ操作が一段と便利になります。

import java.util.stream.Collectors;
import java.util.List;

public class Main {
  public static void main(String[] args) {
    List<Person> personList = List.of(
      new Person("John", 25),
      new Person("Emily", 30),
      new Person("Tom", 35)
    );

    List<String> names = personList.stream()
      .map(Person::name)
      .collect(Collectors.toList());

    System.out.println(names);
  }
}

このコードは、Personオブジェクトのリストから、その名前だけを抽出して新しいリストを生成しています。

ストリームAPIのmapメソッドを使用して、Personオブジェクトのnameだけを抽出し、新たなList<String>を生成しています。

このコードを実行すると、新しい名前のリストが表示されます。

○サンプルコード6:Record型を使った簡易的なデータベースの実装

次に進む前に、データの格納と取得をより効率的に行うための簡易的なデータベースの例を見てみましょう。

この例では、HashMapとRecord型を組み合わせています。

import java.util.HashMap;

public class Main {
  // Personの情報を保持するRecord型
  public record Person(String name, int age) {}

  public static void main(String[] args) {
    // HashMapを使用して簡易的なデータベースを作成
    HashMap<String, Person> database = new HashMap<>();

    // データの追加
    database.put("1", new Person("John", 25));
    database.put("2", new Person("Emily", 30));

    // データの取得と表示
    Person retrievedPerson = database.get("1");
    if (retrievedPerson != null) {
      System.out.println("Name: " + retrievedPerson.name() + ", Age: " + retrievedPerson.age());
    }
  }
}

このコードでは、Personという名前のRecord型を定義しています。

それをキーと値のペアとしてHashMapに格納しています。

HashMapは、効率的なデータの取得と更新が可能なデータ構造です。

このデータベースにデータを追加(putメソッド)した後、特定のキーに対応するデータを取得(getメソッド)しています。

このコードを実行すると、指定したキー(この場合は”1″)に対応するPersonオブジェクトのnameageがコンソールに表示されます。

この例では”Name: John, Age: 25″と表示されます。

○サンプルコード7:Record型とラムダ式の連携

Record型はJavaの他の特性とも非常によく連携します。

特に、ラムダ式との組み合わせは非常に多くの場面で有用です。

Record型を用いてラムダ式でデータを操作する例を紹介します。

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class Main {
  // Personの情報を保持するRecord型
  public record Person(String name, int age) {}

  public static void main(String[] args) {
    List<Person> people = new ArrayList<>();
    people.add(new Person("John", 25));
    people.add(new Person("Emily", 30));
    people.add(new Person("Tom", 40));

    // 年齢が30以上の人をフィルタリング
    Predicate<Person> isOlderThan30 = person -> person.age() >= 30;
    people.stream().filter(isOlderThan30).forEach(person -> System.out.println(person.name()));
  }
}

このコードでは、Person型のオブジェクトのリストを作成し、その中から年齢が30以上の人物だけをフィルタリングしています。

この処理にはラムダ式が用いられており、filterメソッドにPredicateインターフェースを実装したisOlderThan30を渡しています。

このコードを実行すると、年齢が30以上のPersonオブジェクトのnameがコンソールに表示されます。

この例では”Emily”と”Tom”が表示されることとなります。

○サンプルコード8:Record型を使用したデザインパターンの実例

デザインパターンはソフトウェア設計においてよく用いられる手法の一つですが、JavaのRecord型を使用することで、いくつかのデザインパターンをよりシンプルに実装できます。

今回は、代表的な「シングルトンパターン」をRecord型を使って実装してみましょう。

public record SingletonRecord() {
  public static final SingletonRecord INSTANCE = new SingletonRecord();

  private SingletonRecord() {
    // private constructor to prevent instantiation
  }
}

このコードでは、SingletonRecordという名前のRecord型を作成しています。

この型にはINSTANCEという静的フィールドがあり、その初期値としてnew SingletonRecord()が設定されています。

コンストラクタはprivateに設定されており、外部から新たにインスタンスを生成することを防いでいます。

この設計により、シングルトンパターンを非常に簡潔に実装することができます。

具体的には、SingletonRecord.INSTANCEという形でインスタンスにアクセスでき、そのインスタンスはプログラムの実行中に一つしか存在しないことが保証されます。

○サンプルコード9:Record型とOptionalの組み合わせ

JavaのOptionalクラスは、値が存在するかもしれない、または存在しないかもしれない場合に便利なクラスです。

Record型とOptionalを組み合わせることで、より堅牢なコードが書けます。

import java.util.Optional;

public class Main {
  public record Employee(String name, Optional<String> email) {}

  public static void main(String[] args) {
    Employee employee1 = new Employee("John", Optional.of("john@email.com"));
    Employee employee2 = new Employee("Emily", Optional.empty());

    System.out.println("Name: " + employee1.name() + ", Email: " + employee1.email().orElse("No email"));
    System.out.println("Name: " + employee2.name() + ", Email: " + employee2.email().orElse("No email"));
  }
}

このコードでは、EmployeeというRecord型を作成しています。

emailフィールドはOptional<String>型としています。Optional.empty()を使うことで、emailがない場合も安全に扱うことができます。

コードを実行すると、まずはemployee1nameemailが出力され、その後でemployee2nameと”No email”(emailが存在しないため)が出力されます。

●注意点と対処法

JavaのRecord型は簡潔なコード記述が可能ですが、その便利さゆえに誤解やトラップがあることも確かです。

○Record型の使用上の制約

Record型は非常に有用ですが、全てのケースで使えるわけではありません。

次のような制約があります。

  1. フィールドはfinalであり、後から値を変更することはできません。
  2. 継承をサポートしていません。
  3. インスタンスフィールドの追加はできません。

このような制約があるため、クラス設計の段階でこれらの制約に合致するかどうかを確認する必要があります。

○サンプルコード10:Record型の罠とその対処法

Record型でよく見られる罠の一つは、Record内で可変オブジェクトを持つ場面です。

import java.util.ArrayList;
import java.util.List;

public record DangerousRecord(List<String> items) {
}

public class Main {
  public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    DangerousRecord record = new DangerousRecord(list);

    // 外部からリストにアクセスできてしまう
    list.add("New Item");
    System.out.println(record.items());
  }
}

このコードでは、DangerousRecordというRecord型で、可変なArrayListをフィールドに持っています。

この設計は問題があります。

なぜなら、listが外部で変更された場合、DangerousRecordの状態も変わってしまいます。

コードを実行すると、「New Item」というアイテムが追加されてしまいます。

この問題を解決するには、不変なコレクションを使用するか、コンストラクタ内でディープコピーを行う方法があります。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public record SafeRecord(List<String> items) {
  public SafeRecord {
    items = Collections.unmodifiableList(new ArrayList<>(items));
  }
}

この修正版では、コンストラクタ内で受け取ったリストをCollections.unmodifiableListで不変にし、新しいリストを生成しています。

このようにすることで、Record型が持つリストが外部から変更されることはありません。

○サンプルコード11:Record型と他のJava特性との組み合わせ時の注意点

Record型はJavaの他の特性と組み合わせることができますが、その際に注意が必要です。

例えば、JavaのStreamと組み合わせる場合、次のようにNullPointerExceptionが発生する可能性があります。

import java.util.Optional;
import java.util.stream.Stream;

public record PersonWithOptional(Optional<String> name) {
}

public class Main {
  public static void main(String[] args) {
    Stream.of(new PersonWithOptional(Optional.empty()))
      .forEach(person -> System.out.println(person.name().orElse("Unknown")));
  }
}

このコードを実行すると、「Unknown」と出力されることが期待されますが、Optional.empty()がnullとして扱われる場合、NullPointerExceptionが発生する可能性があります。

この問題を防ぐためには、コンストラクタ内でOptionalのnullチェックを行います。

import java.util.Optional;

public record PersonWithOptional(Optional<String> name) {
  public PersonWithOptional {
    name = Optional.ofNullable(name.orElse(null));
  }
}

この修正によって、Optionalフィールドがnullであった場合でも安全に処理を行うことができます。

●カスタマイズ方法

JavaのRecord型は使いやすい反面、デフォルトの挙動だけでは限界があります。しかし、一部のカスタマイズは可能です。

ここでは、Record型のメソッドのオーバーライドとその具体例について詳しく解説します。

○Record型のメソッドのオーバーライド

Record型にはデフォルトでいくつかのメソッドが備わっていますが、これらのメソッドはオーバーライドすることもできます。

特にtoStringequalshashCodeなどのメソッドはオーバーライドして独自の挙動を追加する場合があります。

例えば、toStringメソッドをオーバーライドして独自の文字列表現を生成することができます。

public record CustomToStringPerson(String name, int age) {
  @Override
  public String toString() {
    return String.format("人物情報:名前=%s, 年齢=%d", name, age);
  }
}

上記のコードはCustomToStringPersonというRecord型を定義し、toStringメソッドをオーバーライドしています。

この例では、デフォルトのtoStringの代わりに、"人物情報:名前=xxx, 年齢=xx"という形式の文字列を返します。

○サンプルコード12:カスタマイズしたRecord型の実例

オーバーライドを利用したRecord型のより高度な例を見てみましょう。

この例では、PersonというRecord型が持つnameフィールドが全て大文字であることを保証するようなコードを書いてみます。

public record PersonWithCapitalName(String name) {
  // コンパクトコンストラクタでnameフィールドを大文字に変換
  public PersonWithCapitalName {
    name = name.toUpperCase();
  }

  // toStringもオーバーライドして独自の表現を返す
  @Override
  public String toString() {
    return "大文字の名前: " + name;
  }
}

public class Main {
  public static void main(String[] args) {
    PersonWithCapitalName person = new PersonWithCapitalName("john");
    System.out.println(person);  // 実行結果の説明も後で行います
  }
}

このコードでは、PersonWithCapitalNameというRecord型に、コンパクトコンストラクタとtoStringメソッドをオーバーライドしています。

特にコンパクトコンストラクタでnameフィールドを全て大文字にしています。

このようにして、Record型でも一定のロジックを導入することが可能です。

このコードを実行すると、コンソールに"大文字の名前: JOHN"と出力されます。

これはPersonWithCapitalNameオブジェクトのtoStringメソッドがオーバーライドされているためです。

また、nameフィールドもコンストラクタで大文字に変換されているため、JOHNと大文字で表示されます。

まとめ

JavaのRecord型を学ぶ過程でさまざまな側面を解説しました。

Record型の基本的な使い方から応用例、そしてその限界とカスタマイズ方法までを網羅的に説明してきました。

各節で表したサンプルコードを通じて、具体的なコードの実装とその実行結果も紹介しました。期待されます。それゆえに、Javaとその機能について常に最新の情報を追いかける重要性も忘れずに。

Record型を使いこなせるようになると、よりシンプルで読みやすいコードが書けるようになります。

その結果として、コードの品質が向上し、保守性も高まるでしょう。

Javaでの開発作業が、より楽しく、より生産的なものになることを心より願っています。