
[Java] 今から始めるレコードクラス
こんにちは!
カサレアルでJavaのコースを担当している櫻庭です。
レコードクラスはJava 16で導入された機能で、データを扱うことに特化した特殊なクラスです。
本エントリーでは、このレコードクラスについてまとめてみました。レコードクラスはとても便利なので、ぜひ活用してみてください。
目次
レコードクラス
レコードクラスはデータをまとめて扱うことに特化したクラスです。
保持するデータの定義だけで使用でき、記述量を大幅に減らすことが可能です。
データをまとめる機能としてC言語などで使用できる構造体があります。レコードクラスも構造体と同様に扱えますが、レコードクラスのオブジェクトはイミュータブルであることが構造体との大きな違いです。
また、通常のクラスのようにコンストラクタやメソッドを定義できます。
レコードクラスの定義
レコードクラスが保持するデータはレコードコンポーネントと呼ばれます。レコードコンポーネントは一般のクラスのインスタンスフィールドに相当します。
レコードクラスの定義はclassではなく、recordを使用します。そして、レコードクラス名の後にコンストラクタの引数のようにレコードコンポーネントを定義します。
修飾子 record レコードクラス名(
型 レコードコンポーネント名,
型 レコードコンポーネント名,
......
) {}この定義だけでコンストラクタやアクセッサーメソッドを書かずとも、使うことができます。
たとえば、数直線上の範囲を表すRangeレコードクラスを考えてみます。範囲には下限と上限があり、それぞれdouble型のmin、maxとします。
この場合、Rangeレコードクラスは以下のように定義できます。
public record Range(double min, double max) {}定義の最後の波括弧{}を忘れずに。
Rangeレコードクラスのレコードコンポーネントはプリミティブ型ですが、参照型も使用できます。たとえば、IDと名前を持つ顧客をレコードクラスで定義してみましょう。
public record Customer(int id, String name) {}レコードクラスは継承について、以下のような制限があります。
できないこと
- 他のクラスを継承してレコードクラスを定義できない
- レコードクラスを継承してサブクラス/サブレコードクラスを定義できない
できること
- インタフェースを実装してレコードクラスを定義できる
つまり、継承はできませんが、インタフェースの実装だけは可能ということです。実際にインタフェースを実装して、メソッドを追加することは後で解説します。
レコードクラスの使い方
レコードクラスの定義は、通常のクラスと異なりますが、使い方は通常のクラスと同様です。
これは、レコードクラスをjavacでコンパイルする時に、以下の要素を自動生成するためです。
- コンストラクタ
- アクセッサーメソッド
- equals()メソッド
- hashCode()メソッド
- toString()メソッド
では、レコードオブジェクトの生成から順に説明していきます。
レコードオブジェクトの生成
レコードクラスのインスタンシエーションは、通常のクラスと同様にnewを使用します。
したがって、先ほどのRangeレコードクラスを、下限を0、上限を10としてオブジェクトの生成を行うには次のようになります。
Range range = new Range(0.0, 10.0);引数の並びは、レコードクラスの定義でのレコードコンポーネントの並びと同じです。ここでは、minに0、maxに10が代入されます。
Customerレコードクラスを、IDを1、名前を”サクラバ ユウイチ”でオブジェクト生成するには、以下のようになります。
Customer customer = new Customer(1, "サクラバ ユウイチ");レコードクラスの定義は通常のクラスとは異なるものの、オブジェクト生成は通常のクラスと同じであることが分かります。
レコードコンポーネントへのアクセス
レコードコンポーネントの値を取得するアクセッサーメソッドも自動作成されます。
従来、Javaではフィールドの取得に慣例として、getXX()形式のgetterメソッドが使われてきました。これに対し、レコードクラスではレコードコンポーネント名がそのままメソッド名になります。
たとえば、Rangeレコードクラスでは、min()メソッドとmax()メソッドになります。
Range range = new Range(10.0, 20.0);
IO.println(range.min()); // 10.0が出力
IO.println(range.max()); // 20.0が出力JShellを使って結果を試してみましょう。
jshell> record Range(double min, double max) {}
| 次を作成しました: レコード Range
jshell> var range = new Range(10.0, 20.0)
range ==> Range[min=10.0, max=20.0]
jshell> IO.println(range.min())
10.0
jshell> IO.println(range.max())
20.0同様に、Customerレコードクラスであればid()メソッドとname()メソッドです。
jshell> record Customer(int id, String name) {}
| 次を作成しました: レコード Customer
jshell> var customer = new Customer(1, "サクラバユウイチ")
customer ==> Customer[id=1, name=サクラバユウイチ]
jshell> IO.println(customer.id())
1
jshell> IO.println(customer.name())
サクラバユウイチなお、レコードクラスはイミュータブルであるため、レコードコンポーネントの値を変更することはできません。このため、setterメソッドに相当するメソッドは作成されません。
equals(), hashCode()
通常のクラスのデフォルトのequals()メソッド(つまり、Objectクラスのequals()メソッド)は、オブジェクトが同じかどうかをチェックします。このため、オブジェクトのフィールドの値で比較を行うには、equals()メソッドをオーバーライドする必要があります。
また、equals()メソッドをオーバーライドする時には、一緒にhashCode()メソッドもオーバーライドしなくてはなりません(Effective Java 第3版 3章を参照してみてください)。
これに対し、レコードクラスではequals()メソッドがオーバーライドされ、レコードコンポーネントの値で比較するようになっています。また、hashCode()メソッドもオーバーライドされて、レコードコンポーネントの値を使用して算出されています。
たとえば、同じレコードコンポーネントの値で2つのオブジェクトを作成し、==を使った比較と、equals()メソッドを使った比較を試してみましょう。
jshell> var range1 = new Range(10.0, 20.0)
range1 ==> Range[min=10.0, max=20.0]
jshell> var range2 = new Range(10.0, 20.0)
range2 ==> Range[min=10.0, max=20.0]
jshell> IO.println(range1 == range2)
false
jshell> IO.println(range1.equals(range2))
true
jshell> IO.println(range1.hashCode() == range2.hashCode())
truetoString()
ObjectクラスのtoString()メソッドは、”クラス名@ハッシュ値”という形式の文字列を返します。
これに対し、レコードクラスではレコードクラス名とレコードコンポーネントとその値が列挙されます。JShellでレコードオブジェクトを生成した時に表示される文字列がtoString()メソッドの出力です。
jshell> var range = new Range(0.0, 10.0)
range ==> Range[min=0.0, max=10.0]
jshell> IO.println(range.toString())
Range[min=0.0, max=10.0]なお、IOクラスのprintln()メソッドやSystem.out.println()メソッドでオブジェクトを出力するときは、内部的にオブジェクトのtoString()メソッド呼び出されるため上記のようにtoString()メソッドを記述する必要はありませんが、ここではあえて記述を行っています。
パターンマッチング
レコードクラスをパターンマッチングで使用することも可能です。しかも、Java 21からは、レコードコンポーネントの値を直接変数に代入できるようになりました。
たとえば、三角形、長方形、正方形を表すレコードクラスを定義し、それぞれの面積を求める処理をswitch式で記述してみます。
まずは、レコードクラスの定義から見てみましょう。
// 多角形を表すインタフェース
interface Polygon {}
// 三角形
// 底辺: base, 高さ: height
record Triangle(double base, double height) implements Polygon {}
// 長方形
// 幅: width, 高さ: height
record Rectangle(double width, double height) implements Polygon {}
// 正方形
// 辺の長さ: length
record Square(double length) implements Polygon {}ここでは、三角形、長方形、正方形を表すレコードクラスをメソッドを定義していないPolygonインタフェースを実装したレコードクラスとして定義しています。
次に、面積を求めるメソッドを以下に示します。
// 面積を求める
double calcArea(Polygon p) {
double area = switch (p) {
case Triangle(var b, var h) -> b * h / 2.0;
case Rectangle(var w, var h) -> w * h;
case Square(var l) -> l * l;
default -> throw new IllegalArgumentException();
};
return area;
}レコードクラスの定義と同様に変数を記述しておくと、直接その変数にレコードコンポーネントの値を代入します。
このことによって、caseの中でアクセッサーメソッドをコールする必要がなくなり、記述が簡潔になります。
ここでは、varを使ってローカル変数を定義していますが、型名を省略せずに記述することももちろん可能です。
レコードクラスの使い道
レコードクラスは、データを扱うところであればどこでも使うことができます。ただし、レコードオブジェクトがイミュータブルであることは意識しておく必要があります。
どこでも使えるとは言われても、ではどこで使えばよいのでしょうか。ここが使えるというおすすめをいくつか紹介します。
- 値オブジェクト
- DTO
- テンポラリーのタプル
値オブジェクト
データ駆動設計における、ドメインで扱われる値をモデル化したのが値オブジェクト(Value Object)です。
値オブジェクトはイミュータブルであることが重要です。この意味からも値オブジェクトをレコードクラスで表すことは理にかなっています。
たとえば、金額、通貨、予定日、期限、メールアドレス、電話番号など業務によってさまざまな値オブジェクトが考えられます。レコードクラスはこのような小さなデータを表すのにうってつけです。
また、値オブジェクトには制約がつきものです。たとえば金額であれば負の値はとらないなどです。
このような制約も、後で説明するコンストラクタのオーバーライドにより、容易に表すことが可能です。
また、シールドインタフェースと組み合わせることで、関数型プログラミングで使用される代数的データ型(Algebraic Data Type, ADT)を記述できます。
ADTは直積型と直和型の2種類の型表現によってデータを表していく手法です。名前は難しそうですが、レコードクラスが直積型、シールドクラス/インタフェースが直和型に相当すると考えれば大丈夫です。
ADTにはいくつかの利点がありますが、その中の1つに網羅性を高めることがあります。
たとえば、先ほどのPolygonインタフェースをシールドインタフェースで定義すれば、面積を求めるswitch式でdefaultを書く必要がなくなります。また、javacコンパイラがcaseの記述漏れをチェックできるようになり、caseの記述漏れによるバグを減らすことができます。
DTO
DTOはData Transfer Object (データ転送オブジェクト)のことで、レイヤー間やモジュール間でデータをやり取りする時に使用するオブジェクトです。
レイヤー間などでデータをやり取りする場合、データが1種類ということはほとんどありません。複数のデータをまとめてやり取りすることが多いはずです。
このやり取りに使用するデータの容れものとしてレコードクラスを使用するということです。
すでにSpring FrameworkではDTOとしてレコードクラスをサポートしています。他のフレームワークでも、レコードクラスをサポートしているものが増えてきています。
テンポラリーなタプル
タプルというのは、複数のデータをまとめて扱うための機能です。Pythonなどではタプルの型定義などせずに使用することができます。
しかし、Javaで型を定義せずに使うということはできないので、簡易的にレコードクラスを使うということです。
たとえば、Stream APIのパイプラインの途中でインデックスを保持しておきたいというような時に使用できます。
final List contents = ...;
// インデックスとリストの要素を保持するためのレコードクラス
record IValue(int i, T value) {}
IntStream.range(0, contents.size())
.mapToObj(i -> new IValue(i, contents.get(i)))
... // パイプラインを続ける
ここでのレコードクラスはメソッド内で定義したローカルなレコードクラスなので、このメソッドの外側では使えません。しかし、一時的に使用するのであれば、これで十分です。
他にもレコードクラスの使い道はあると思うので、いろいろと模索してみてください。
レコードクラスの拡張
ここまでは、レコードクラスをレコードコンポーネントを定義するだけの状態で使用してきました。しかし、レコードクラスはコンストラクタやメソッドを追加することも可能です。
ただし、インスタンスフィールドを定義することはできません。レコードオブジェクトが保持できるのはインスタンスフィールドに相当するレコードコンポーネントだけです。
カノニカルコンストラクタ
カノニカルコンストラクタ(Canonical Constructor)は、レコードコンポーネントを初期化するために自動作成されるコンストラクタです。日本語で標準コンストラクタと表記されることもあります。
たとえば、Rangeレコードクラスの場合、カノニカルコンストラクタは以下のコンストラクタと同等のコンストラクタです。
public Range(double min, double max) {
this.min = min;
this.max = max;
}このカノニカルコンストラクタを自分で定義することも可能です。
カノニカルコンストラクタは通常のコンストラクタの記述法ではなく、レコードクラス名の後に波括弧で囲った内部に処理を記述します。
修飾子 record レコードクラス名(レコードコンポーネント列) {
修飾子 レコードクラス名 {
// ここに初期化処理を記述
}
}レコードコンポーネントへの値代入は、自動で行ってくれます。
レコードクラスを値オブジェクトとして使用する場合、値に制約があることも多くあります。たとえば、範囲であれば下限が上限より大きい値にならない、金額であれば負の値をとらないようにするなどです。
このような制約はカノニカルコンストラクタでチェックできます。
ここではRangeレコードクラスで、下限が上限より大きい場合にIllegalArgumentException例外をスローするようにしてみます。
public record Range(double min, double max) {
// カノニカルコンストラクタ
public Range {
// 下限が上限以上の場合、例外をスローする
if (min > max) {
throw new IllegalArgumentException();
}
}
}金額で負の値を指定された場合、初期値として0を使用するのであれば以下のようになります。
public record Money(int amount) {
// カノニカルコンストラクタ
public Money {
// 値が負の場合、0で初期化する
if (amount < 0) {
amount = 0;
}
}
}カノニカルコンストラクタは定義が通常のコンストラクタとは異なりますが、もう1点大きな違いがあります。
- カノニカルコンストラクタではthows節を記述できない
つまり、カノニカルコンストラクタで検査例外をスローすることはできません。
このため、Rangeレコードクラスの例では非検査例外であるIllegalArgumentException例外を使用しています。
他のコンストラクタ
カノニカルコンストラクタ以外のコンストラクタを定義することも可能です。
たとえば、金額を表すMoneyレコードクラスを引数なしで生成したら、金額を0にすることにしてみましょう。
public record Money(int amount) {
// デフォルトコンストラクタ
public Money() {
// カノニカルコンストラクタの呼び出し
// 金額を0に設定する
this(0);
}
// カノニカルコンストラクタ
public Money {
// 値が負の場合、0で初期化する
if (amount < 0) {
amount = 0;
}
}
}カノニカルコンストラクタ以外のコンストラクタの注意点として以下の点があります。
- レコードコンポーネントの初期化はカノニカルコンストラクタで行う
つまり、コンストラクタを追加したとしても、そのコンストラクタ内でカノニカルコンストラクタを呼び出す必要があります。
レコードコンポーネントに代入する値を算出する処理まではコンストラクタで行いますが、レコードコンポーネントに代入する処理はカノニカルコンストラクタで行うということです。
上記のMoneyレコードクラスではデフォルトコンストラクタの内部でthis(0);でカノニカルコンストラクタを呼び出しています。
メソッド
レコードクラスでもメソッドを追加することができます。
コンストラクタとは異なり、表記の違いもありませんし、制約もありません。ただし、レコードクラスはイミュータブルなので、レコードコンポーネントの値を変更することはできません。
ここでは、Moneyレコードクラスの加算、減算を行うメソッドを追加してみましょう。以下にメソッドの部分だけ示しました。
public Money add(Money m) {
return new Money(amount + m.amount);
}
public Money sub(Money m) {
return new Money(amount - m.amount);
}加算や減算を行ったとしても、自身のレコードコンポーネントの値を更新するのではなく、新しいレコードオブジェクトを作成します。
イミュータブルなクラスでは、このように処理の結果を新たなオブジェクトで返すようにします。
たとえば、Date and Time APIの一連のクラスはすべてイミュータブルなので、plus()メソッドなどのメソッドは新たにオブジェクトを生成して返します。
Date and Time APIでも使用されていますが、保持している値を一部だけ更新して新たなオブジェクトを生成する場合、withで始まるメソッドを使用することが慣例になっています。
そこで、範囲を表すRangeレコードクラスの下限を変更したオブジェクトを生成するwithMin()メソッドと、上限を変更したオブジェクトを生成するwithMax()メソッドを作成してみましょう。
public record Range(double min, double max) {
public Range {
// 下限が上限以上の場合、例外をスローする
if (min > max) {
throw new IllegalArgumentException();
}
}
// 下限の更新
public Range withMin(double min) {
return new Range(min, this.max);
}
// 上限の更新
public Range withMax(double max) {
return new Range(this.min, max);
}
}
まとめ
本エントリーではレコードクラスについてまとめてみました。
レコードクラスは以下のような特徴のあるクラスです。
- データを保持するのに特化したクラス
- 簡潔な定義
- イミュータブル
- インタフェースの実装が可能
- アクセッサーメソッド、equal()メソッドなどを自動生成
- カノニカルコンストラクタの簡略記法
また、レコードクラスの使い道についても紹介しました。
とても便利なクラスなので、ぜひご活用ください。
カサレアルでは、Javaを学ぶ方に向けて「Javaプログラミング入門」や「Javaプログラミング基礎」などのコースを開催しています。
Javaに関するコースの詳細や開催日程に関しては以下のリンクをご覧ください。
バックエンド開発を学ぶ研修一覧
https://www.casareal.co.jp/ls/service/openseminar/search/backend