本ページはプロモーションが含まれています

Java入門

Javaのrecordでコードを1/5に!使い方からクラスとの違いまで解説

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

「Javaのクラス定義、もっとシンプルにならないかな…」

長年Javaで開発をしていると、データを保持するためだけのクラス、いわゆるDTO(Data Transfer Object)を作るたびに、同じようなコードを何度も書いていることに気づきます。フィールドを宣言し、コンストラクタを作り、getterを一つひとつ用意して、おまけにequals()hashCode()toString()までオーバーライドする。この作業に、私は少しうんざりしていました。

あるプロジェクトでJava14から導入されたrecordを試したところ、これまで数十行にわたって書いていたDTOが、たった1行で定義できたのです。コード量が劇的に減り、クラスの目的が「データを保持すること」だと一目でわかるようになりました。この経験は、私のJavaコーディングを大きく変えるきっかけになりました。

この記事は、過去の私と同じように感じている「Javaのrecordについて知りたい方」に向けて書いています。

  • クラス定義の冗長さに悩んでいる
  • recordの具体的な使い方が知りたい
  • classとrecordをどう使い分ければ良いか分からない

こんな悩みを抱えているなら、ぜひ読み進めてください。この記事を読み終える頃には、Javaのrecordを完全に理解し、あなたのプロジェクトで自信を持って活用できるようになるでしょう。

Javaのrecordとは?【簡単に言うとデータ専用クラス】

Javaのrecordとは、一言でいうと「不変(イミュータブル)なデータを保持するため専用の、特別なクラス」です。

これまでデータを扱うためだけに作っていたクラスの、面倒な記述を大幅に削減してくれます。Java14でプレビュー機能として導入され、Java16で正式にリリースされました。recordを使えば、データの定義に集中でき、より本質的なロジックの記述に時間を使えるようになります。

recordが登場した背景(Java14で導入された理由)

recordが登場する前のJavaでは、データを保持するクラスを作るのに多くの「お決まりのコード(c)」が必要でした。

例えば、x座標とy座標を持つ「点」を表すクラスを作るだけでも、

  • フィールド(x, y
  • コンストラクタ
  • 各フィールドのgetterメソッド(getX(), getY()
  • equals()メソッド(値が同じか比較するため)
  • hashCode()メソッド(ハッシュ値の計算)
  • toString()メソッド(デバッグ用の文字列出力)

これらすべてを自分で実装する必要があったのです。これらは決まりきった作業でありながら、バグの温床にもなりやすく、開発者の負担になっていました。recordは、こうした課題を解決するために生まれました。

recordが解決する「クラス定義の冗長さ」とは

言葉で説明するよりも、実際のコードを見てもらうのが一番分かりやすいでしょう。先ほどの「点」を、従来のclassで表現してみます。

import java.util.Objects;

public final class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[" +
                "x=" + x +
                ", y=" + y +
                ']';
    }
}

たった2つのデータを保持したいだけなのに、40行近くのコードが必要になります。これがrecordが解決しようとした「クラス定義の冗長さ」です。

recordの基本構文と定義方法

では、同じPointクラスをrecordで定義するとどうなるのでしょうか。驚くほどシンプルになります。

public record Point(int x, int y) {}

たったこれだけです。

classキーワードをrecordに変え、クラス名の後に()を付けて、保持したいデータ(フィールド)をカンマ区切りで宣言します。これだけで、先ほどの40行近いコードと全く同じ機能を持つクラスが定義できてしまいます。

具体的には、この1行のrecord定義によって、以下の要素がコンパイラによって自動的に生成されます。

record

  • private finalなフィールド(xy
  • 全フィールドを引数に持つコンストラクタ
  • 各フィールドのアクセサメソッド(x()y()
  • equals()hashCode()toString()メソッド

驚くほど簡潔だと思いませんか。これがrecordの大きな魅力です。

recordの使い方【サンプルコード付き】

recordの基本的な魅力が分かったところで、次は具体的な使い方を見ていきましょう。インスタンスの生成やメソッドの追加など、実践的な使い方をサンプルコードと共に解説します。

最もシンプルなrecordの例

先ほどのPointレコードを実際に使ってみましょう。インスタンスの生成方法は、通常のクラスと全く同じです。

// Pointレコードの定義
public record Point(int x, int y) {}

// インスタンスの生成
var p1 = new Point(10, 20);
var p2 = new Point(10, 20);
var p3 = new Point(30, 40);

// フィールドへのアクセス
System.out.println("p1のx座標: " + p1.x()); // 出力: p1のx座標: 10
System.out.println("p1のy座標: " + p1.y()); // 出力: p1のy座標: 20

// 自動生成されたtoString()の確認
System.out.println(p1); // 出力: Point[x=10, y=20]

// 自動生成されたequals()の確認
System.out.println("p1とp2は同じか: " + p1.equals(p2)); // 出力: p1とp2は同じか: true
System.out.println("p1とp3は同じか: " + p1.equals(p3)); // 出力: p1とp3は同じか: false

このように、インスタンス化はnewを使い、フィールドへのアクセスはフィールド名()という形式のメソッドで行います。getX()ではなくx()となる点に注意してください。

コンストラクタ・メソッドを定義する方法

recordは自動でコンストラクタを生成してくれますが、独自のコンストラクタやメソッドを追加することも可能です。

あわせて読む

独自のコンストラクタ(コンパクトコンストラクタ)

コンストラクタで引数のバリデーションを行いたい場合があります。recordには、そのための特別な「コンパクトコンストラクタ」という構文が用意されています。

public record User(String name, int age) {
    // コンパクトコンストラクタ
    public User {
        if (age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります");
        }
    }
}

// 使い方
var user1 = new User("Taro", 25); // OK
// var user2 = new User("Jiro", -1); // ここでIllegalArgumentExceptionが発生

public User { ... }のように、引数リストの()を省略して書くのが特徴です。この中でバリデーションロジックを記述します。フィールドへの代入(this.age = age;など)は、コンパイラが自動で行ってくれるため不要です。

独自のメソッド

もちろん、recordにも通常のクラスと同じように、独自のメソッドを追加できます。

public record Circle(double radius) {
    // 円の面積を計算するメソッド
    public double area() {
        return radius * radius * Math.PI;
    }
}

// 使い方
var circle = new Circle(5.0);
System.out.println("円の面積: " + circle.area()); // 出力: 円の面積: 78.5398...

このように、データを保持するだけでなく、そのデータを使った簡単な計算処理などを持たせることも可能です。

recordとイミュータブル(不変)な設計の関係

recordを理解する上で非常に重要なのが「イミュータブル(不変)」という概念です。

recordで宣言されたフィールドは、すべてprivate finalとして扱われます。これは、一度インスタンスを作成したら、その中身(フィールドの値)を後から変更できないことを意味します。setterメソッドも自動生成されません。

なぜ不変であることが重要なのでしょうか。

不変なオブジェクトは、一度作れば状態が変わらないため、プログラムの動作を予測しやすくなります。特に、複数のスレッドから同時にアクセスされるような環境(マルチスレッドプログラミング)では、データが意図せず書き換えられる心配がなくなり、非常に安全にデータを扱うことができます。

recordは、この安全で堅牢な「不変オブジェクト」を、非常に簡単に作成するための仕組みなのです。

recordと通常のクラスの違い

recordが便利なことは分かりましたが、具体的にclassと何が違うのでしょうか。コード量以外の違いを詳しく見ていきましょう。

classとのコード量の比較

これはすでにお見せしましたが、改めて比較してみます。

【従来のclass(約40行)】

public final class PointClass {
    // フィールド、コンストラクタ、getter、equals、hashCode、toString...
    // (先ほどの長いコード)
}

【record(1行)】

public record PointRecord(int x, int y) {}

コード量は圧倒的にrecordが少なく、クラスの目的が「xyというデータを保持すること」であることが一目瞭然です。可読性と保守性が劇的に向上します。

getter/setterの違い

recordclassでは、フィールドへのアクセス方法が異なります。

  • class (getter): 通常、getという接頭辞がつきます。(例: point.getX()
  • record (アクセサ): getはつかず、フィールド名と同じ名前のメソッドになります。(例: point.x()

また、前述の通りrecordは不変であるため、値を変更するためのsetterメソッド(setX()など)は生成されません。 これがrecordの大きな特徴の一つです。

equalsやhashCode、toStringの自動生成について

classでは自分で実装する必要があったこれらのメソッドを、recordは自動で、かつ適切に実装してくれます。

  • equals(): 全てのフィールドの値が等しい場合にtrueを返します。
  • hashCode(): 全てのフィールドの値を使ってハッシュ値を計算します。
  • toString(): クラス名と全フィールドの値を"クラス名[フィールド名=値, ...]"という形式の分かりやすい文字列で返します。

これらのメソッドを自分で実装すると、フィールドを追加した際に修正を忘れるなどのミスが起こりがちです。recordを使えば、そうした人為的なミスを防ぐことができ、コードの安全性が高まります。

recordを使うメリット・デメリット

どんな技術にも良い面と悪い面があります。recordを効果的に使うために、メリットとデメリットの両方をきちんと理解しておきましょう。

メリット① コードが短くなる

最大のメリットは、やはりコードの記述量を大幅に削減できる点です。

DTOやVO(Value Object)など、データを保持するだけのクラスを簡潔に書けるため、開発効率が向上します。コードが短くなることは、可読性の向上にも直結し、結果としてメンテナンスしやすいプログラムに繋がるでしょう。

メリット② 不変データを安全に扱える

recordは、設計上イミュータブル(不変)です。

これにより、意図しないデータの書き換えを防ぎ、プログラムの堅牢性を高めます。特に、複数の処理が並行して動くような複雑なシステムにおいて、この不変性は非常に大きなメリットとなります。安心してデータを使い回せるため、バグの少ないクリーンなコードを書きやすくなるのです。

デメリット① 継承ができない

recordは、他のクラスをextends(継承)することができません。

これは、recordが内部的にjava.lang.Recordという特別なクラスを継承しているためです。Javaではクラスの多重継承が認められていないため、他のクラスを継承できないという制約があります。ただし、implementsを使ってインターフェースを実装することは可能です。

あわせて読む

デメリット② 柔軟な拡張には向かない

recordのフィールドはすべてfinalであり、インスタンス変数(finalではないフィールド)を持つことができません。

そのため、「オブジェクト作成後に状態が変化する」ような、可変(ミュータブル)なクラスとして設計したい場合にはrecordは不向きです。あくまで「変わらないデータのかたまり」を定義するための機能だと割り切る必要があります。

recordの実践例【DTOやAPIレスポンスで便利】

recordがどのような場面で特に役立つのか、具体的な実践例を見ていきましょう。

recordを使ったDTO(データ転送オブジェクト)の例

DTOは、異なるレイヤー間(例: サービス層とコントローラー層)でデータをやり取りするために使われるオブジェクトです。DTOの役割は純粋にデータを運ぶことなので、recordの特性と非常に相性が良いです。

あわせて読む

例えば、ユーザー情報を運ぶUserDtoを考えてみましょう。

【classの場合】

// フィールド、コンストラクタ、getter、equals...などが必要
public class UserDto {
    private final long id;
    private final String name;
    private final String email;
    // ...
}

【recordの場合】

public record UserDto(long id, String name, String email) {}

recordを使えば、このように1行で定義が完了します。データベースから取得したデータを詰め替えて、APIのレスポンスとして返すような場面で大活躍します。

Spring Bootでrecordを使うケース

WebアプリケーションフレームワークであるSpringBootでも、recordは非常に便利です。特に、JSON形式でデータをやり取りするREST APIの開発で真価を発揮します。

Spring Bootは、recordをAPIのリクエストボディやレスポンスボディとして自動的に解釈してくれます。

@RestController
public class UserController {

    // ユーザー情報を表現するrecord
    public record UserResponse(long id, String name) {}

    @GetMapping("/users/{id}")
    public UserResponse getUser(@PathVariable long id) {
        // 本来はデータベースなどからユーザー情報を取得する
        // ここではダミーデータを返す
        return new UserResponse(id, "Taro Yamada");
    }
}

このコードでは、UserResponseというrecordを定義し、それをコントローラーメソッドの戻り値にしています。Spring Bootは、このrecordオブジェクトを自動的に以下のようなJSONに変換してクライアントに返してくれます。

{
  "id": 1,
  "name": "Taro Yamada"
}

recordのおかげで、JSONの構造とJavaのコードが1対1で対応し、非常に見通しが良くなります。

recordを使うときの注意点

recordは便利ですが、いくつか注意点もあります。

  1. JPAエンティティには使えない:データベースのテーブルとマッピングするJPAのエンティティとしてrecordを使うことは、現時点では推奨されません。JPAの仕様では、引数なしのコンストラクタやsetterメソッドが要求されることが多く、recordの設計と相性が悪いためです。エンティティは従来のclassで定義し、それをDTOであるrecordに変換して扱うのが良い方法です。
  2. 可変(ミュータブル)なオブジェクトには使わない:オブジェクトの状態が後から変わることを前提とする設計には、recordを使用してはいけません。例えば、Builderパターンで段階的にオブジェクトを構築していくようなケースや、設定情報を保持しつつ動的に変更するようなオブジェクトには不向きです。
  3. 何でもrecordにしようとしない:recordは銀の弾丸ではありません。データ保持という明確な目的を持つ場合にのみ使用し、複雑なビジネスロジックや状態管理が必要な場合は、これまで通りclassを使いましょう。

recordを使うべきか?まとめと判断基準

最後に、recordclassをどのように使い分ければよいのか、判断基準をまとめます。

classとrecordの使い分けポイント

この2つは競合するものではなく、目的によって使い分ける「適材適所」の関係です。

  • record を使うべき時:
    • 目的: データの集合を「不変なコンテナ」としてシンプルに表現したい。
    • 具体例: DTO、VO、APIのレスポンス/リクエスト、設定情報など。
    • キーワード: 不変性、データ、簡潔さ
  • class を使うべき時:
    • 目的: 状態を持ち、その状態が変化する。または、他のクラスとの継承関係や、複雑なビジネスロジックをカプセル化したい。
    • 具体例: サービス、リポジトリ、ドメインオブジェクト(状態変化を伴うもの)、JPAエンティティなど。
    • キーワード: 可変性、振る舞い、拡張性

recordが向いているケース/向かないケース

向いているケース向かないケース
概要主にデータを保持することが目的の場合状態の変化や複雑なロジックが主目的の場合
具体例・DTOやVO ・APIのリクエスト/レスポンス ・複数の値を返すメソッドの戻り値・JPAエンティティ ・継承が必要な設計 ・Builderパターンなど可変性が求められる場面

今後のJava開発でrecordが主流になるのか

recordがすべてのclassを置き換えることはありません。しかし、データを扱うという特定の領域においては、間違いなくrecordが今後のスタンダードになっていくでしょう。

冗長なコードを減らし、コードの意図を明確にするrecordは、現代のJava開発における強力な武器です。

これまで何気なくclassで書いていたデータ保持用のクラスを、一度「これはrecordで書けないか?」と考えてみる癖をつけるだけで、あなたのJavaコードはよりクリーンで、より安全なものに進化するはずです。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門