はじめに
この記事を読めばJavaのRecord型の使い方や応用技法を身につけることができるようになります。
特にプログラミングが初めての方でもわかりやすいように、Record型の基本から、応用例、注意点、カスタマイズ方法までを手順を踏んで詳しく説明します。
コードサンプルも豊富に用意していますので、理解が深まること間違いなしです。
●JavaのRecord型とは
JavaのRecord型とは、不変性を保ったデータクラスを簡単に作るための新しい言語機能です。
この機能はJava 16から追加され、その後もバージョンアップを重ねているため、最新のJavaを使えばこの素晴らしい機能を手軽に試すことができます。
○Record型の概要とメリット
Record型は、不変のデータを格納するための軽量なクラスを手軽に生成できる機能です。
この機能があると、たとえば”名前”と”年齢”を持つPerson
クラスを作る際に、各フィールドに対するゲッターメソッドや、equals
、hashCode
、toString
などのメソッドを自動で生成してくれます。
メリットとしては、
- コード量の削減:複数のメソッドを手動で追加する手間が省ける。
- 不変性:フィールドがfinalであり、一度設定されたら変更が不可能。
- クリアなコード:構造がシンプルになるので、コードが読みやすくなる。
といった点が挙げられます。
○JavaでのRecord型の登場背景
Javaでは、長らくデータクラスを手動で定義する必要がありました。
しかし、KotlinやScalaなどの近年のプログラミング言語では、このようなデータクラスを簡単に生成できる機能があり、それが高く評価されています。
Javaもこれに続き、より効率的なプログラミングを可能にするためにRecord型が導入されました。
●Record型の基本的な使い方
JavaのRecord型を使うことで、データクラスの作成が一気に簡単になります。
それでは、具体的なコードを見ながら基本的な使い方について説明していきましょう。
○サンプルコード1:Record型の基本的な宣言と使用
Record型の基本的な宣言と使用方法を見ていきます。
Record型を使って「Person」という名前と「年齢」を持つクラスを作ってみましょう。
public record Person(String name, int age) {
}
このコードは、Person
というRecord型を作っています。
この例ではname
とage
という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
クラスのインスタンスを生成して、そのname
とage
を出力するプログラムです。
コードを実行すると、次のような出力がされます。
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
を作成し、その後でリスト内の各Person
のname
とage
を出力しています。
実行すると、各Person
オブジェクトのname
とage
が表示されます。
○サンプルコード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
オブジェクトのname
とage
がコンソールに表示されます。
この例では”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
がない場合も安全に扱うことができます。
コードを実行すると、まずはemployee1
のname
とemail
が出力され、その後でemployee2
のname
と”No email”(emailが存在しないため)が出力されます。
●注意点と対処法
JavaのRecord型は簡潔なコード記述が可能ですが、その便利さゆえに誤解やトラップがあることも確かです。
○Record型の使用上の制約
Record型は非常に有用ですが、全てのケースで使えるわけではありません。
次のような制約があります。
- フィールドはfinalであり、後から値を変更することはできません。
- 継承をサポートしていません。
- インスタンスフィールドの追加はできません。
このような制約があるため、クラス設計の段階でこれらの制約に合致するかどうかを確認する必要があります。
○サンプルコード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型にはデフォルトでいくつかのメソッドが備わっていますが、これらのメソッドはオーバーライドすることもできます。
特にtoString
やequals
、hashCode
などのメソッドはオーバーライドして独自の挙動を追加する場合があります。
例えば、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での開発作業が、より楽しく、より生産的なものになることを心より願っています。