![]()
【Java入門 実用編】カスタム Comparator の利用 | コレクションクラスの活用
2026.02.06
前回の記事では、Stream API を利用したソートの中で、Comparator に準備された自然順序や、ラムダ式を用いた自由度の高いソート方法についてご紹介しました。当記事では、オブジェクトの順序を決定する条件を事前に準備し、ソートなどの順序比較に利用できるカスタム Comparator の作成と、これを利用した List のソート方法についてご説明します。

目次
カスタム Comparator とは
カスタム Comparator は、引数で比較対象のクラスを定義し、そのクラスのオブジェクト同士の順序を一定のルールに基づいて返す、Comparator インターフェイスの実装クラスです。
カスタム Comparator は任意のクラス同士のインスタンスを比較する際の「順位付け」を実装したクラスとして準備します。前回の記事で紹介した、Stream API とラムダ式を利用したソートするケースでは、同じソートを別のメソッドなどで行いたい場合に、同じ内容のラムダ式を書く必要がありますが、カスタム Comparator として準備することで、この「順位付け」のルールを再利用することが可能になります。
カスタム Comparator の利用
カスタム Comparator のルール
カスタム Comparator を作成するには、java.util.Comparator インターフェイスを実装し、compare(T o1, T o2) メソッドをオーバーライドします。
総称型 T の部分は、比較するクラス(2つの引数で共通のクラス)を指定して実装します。例えば、文字列(String)を比較したい場合は、以下のようにオーバーライドします。
int compare(String o1, String o2) {
// 比較の内容を実装する
if(o1 == null) return 1;
if(o2 == null) return -1;
return o1.compareTo(o2);
}
このメソッドは、2つのオブジェクト(o1 と o2)を受け取り、比較結果を 「整数(int)」 で返すという厳密なルールがあります。ソートなどで利用する場合は、以下のような動作となることに注意してください。
・戻り値がプラス(正の数): o1 は o2 より大きい(ソート時に o1 を後ろに並べる)
・戻り値がマイナス(負の数): o1 は o2 より小さい(ソート時に o1 を前に並べる)
・戻り値が 0: o1 と o2 は等しい(ソート時は順序はそのまま)
カスタム Comparator の作成と利用
カスタム Comparator は任意のクラスの順位付けを実装することができるため、例えば JavaBean のような複数の値を持つクラスのインスタンス同士の比較を行うことも可能です。
ここでは実際に比較するクラスと、その比較の実装と利用方法について説明します。
ソート対象のクラスの準備
比較対象として独自のクラスを用意します。ここでは、ID、名前、年齢の3つのフィールドを持つ Member(会員)クラスを作成します。
Member.java
public class Member {
private int id;
private String name;
private int age;
// コンストラクタ
public Member(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// ゲッターメソッド
public int getId() { return id; }
public String getName() { return name; }
public int getAge() { return age; }
// toString メソッドのオーバーライド(確認用)
@Override
public String toString() {
return String.format("ID:%d / %-6s (%d歳)", id, name, age);
}
}カスタム Comparator の準備
比較対象の Member クラスをどのように比較するかを、カスタム Comparator クラスを準備して定義します。
上記の Member クラスの場合、実際に利用する場合は「単に年齢順に並べる」だけではなく、「年齢順に並べ、もし同い年ならIDが若い順に並べる」といった複数条件(サブソート) が求められるケースがほとんどです。
また、表示する画面群によって、同じクラスでも「年齢が高い順、同じ場合は名前順(辞書順)」「名前順(辞書順)、名前が同じ場合は年齢の若い順」といった複数の要件がある場合なども考えられます。
このような場合には、以下のような複数のカスタム Comparator を準備しておき、画面によって(または画面の操作によって)必要なカスタム Comparator を利用することが考えられます。
パターン1:MemberAgeComparator.java(年齢の降順→名前辞書順)
import java.util.Comparator;
public class MemberAgeComparator implements Comparator<Member> {
@Override
public int compare(Member m1, Member m2) {
// 1. 年齢が高い順(降順)
// 降順なので後者(m2)の年齢から前者(m1)の年齢を引いた値の正負で判定
int ageDiff = m2.getAge() - m1.getAge();
if (ageDiff != 0) {
return ageDiff;
}
// 2. 名前順(辞書順・昇順)
// Stringの compareTo で辞書順比較
int nameDiff = m1.getName().compareTo(m2.getName());
if (nameDiff != 0) {
return nameDiff;
}
// 3. 全く一緒の場合はIDの昇順(IDの大小で判定)
return m1.getId() - m2.getId();
}
}
パターン2:MemberNameComparator.java(名前辞書順→年齢の昇順)
import java.util.Comparator;
public class MemberNameComparator implements Comparator<Member> {
@Override
public int compare(Member m1, Member m2) {
// 1. 名前順(辞書順・昇順)
int nameDiff = m1.getName().compareTo(m2.getName());
if (nameDiff != 0) {
return nameDiff;
}
// 2. 年齢が若い順(昇順)
// 昇順なので前者(m1)の年齢から後者(m2)の年齢を引いた値の正負で判定
int ageDiff = m1.getAge() - m2.getAge();
if (ageDiff != 0) {
return ageDiff;
}
// 3. 全く一緒の場合はIDの昇順(IDの大小で判定)
return m1.getId() - m2.getId();
}
}
並びを一意にする必要がある場合、要件で求められた順序付けに加え、要件の条件では全く同じだった場合に一意の順序となる値(上記の例ではID)での順序付けを付け加える必要があることに注意してください。
カスタム Comparator を利用したソート
準備した部品(カスタム Comparator)を利用して、実際にリストをソートしてみましょう。比較結果が分かりやすいよう、年齢や名前が重複するデータを用意します。
ここでは Stream API の sorted() メソッドを利用して「非破壊的(元のリストを書き換えない)」にソートを行い、元のリストのデータ順を変更せずに、並び替えて詰め直された新しい List を作成していることに注意してください。(これにより2つの Comparator でのソート実行結果が確認できます)
SortSwitchSample.java(ソート実行確認のMainクラス)
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SortSwitchSample {
public static void main(String[] args) {
// 1. ソート対象データの準備
List<Member> originalList = new ArrayList<>();
originalList.add(new Member(1, "Tanaka", 45));
originalList.add(new Member(2, "Sato", 20));
originalList.add(new Member(3, "Suzuki", 45)); // Tanakaと同い年
originalList.add(new Member(4, "Sato", 30)); // ID:2のSatoと同姓
originalList.add(new Member(5, "Sato", 20)); // ID:2のSatoと同姓同年齢
System.out.println("=== 元のリスト ===");
originalList.forEach(System.out::println);
// --------------------------------------------------
// パターン1:年齢優先のソート
// --------------------------------------------------
List<Member> ageSortedList = originalList.stream()
.sorted(new MemberAgeComparator())
.collect(Collectors.toList());
System.out.println("\n=== 年齢優先ソート(年齢降順 -> 名前昇順 -> ID昇順) ===");
ageSortedList.forEach(System.out::println);
// --------------------------------------------------
// パターン2:名前優先のソート
// --------------------------------------------------
List<Member> nameSortedList = originalList.stream()
.sorted(new MemberNameComparator())
.collect(Collectors.toList());
System.out.println("\n=== 名前優先ソート(名前昇順 -> 年齢昇順 -> ID昇順) ===");
nameSortedList.forEach(System.out::println);
}
}
実行結果:
=== 元のリスト ===
ID:1 / Tanaka (45歳)
ID:2 / Sato (20歳)
ID:3 / Suzuki (45歳)
ID:4 / Sato (30歳)
ID:5 / Sato (20歳)
=== 年齢優先ソート(年齢降順 -> 名前昇順 -> ID昇順) ===
ID:3 / Suzuki (45歳)
ID:1 / Tanaka (45歳)
ID:4 / Sato (30歳)
ID:2 / Sato (20歳)
ID:5 / Sato (20歳)
=== 名前優先ソート(名前昇順 -> 年齢昇順 -> ID昇順) ===
ID:2 / Sato (20歳)
ID:5 / Sato (20歳)
ID:4 / Sato (30歳)
ID:3 / Suzuki (45歳)
ID:1 / Tanaka (45歳)カスタム Comparator の使い分け
前回の記事で紹介したように、Java 8以降は「ラムダ式」を使ってその場でソート条件を記述することも可能ですが、 Comparator を作成するケースとの使い分けはどのように考えるべきでしょうか。必ずしも決まりがあるわけではありませんが、大まかには以下の基準で考えて良いと思います。
- ラムダ式が適しているケース
- その場所(1つのメソッド内)でしか使わない、1度きりの「使い捨て」のソートの場合
(または、そのメソッド自体が共通メソッドとして準備されるケース) - 比較条件が「単一のフィールドのみ」など、非常にシンプルで短い場合
- その場所(1つのメソッド内)でしか使わない、1度きりの「使い捨て」のソートの場合
- カスタム Comparator(クラス化)が適しているケース
- 複数の画面やAPIで、同じソート順序を使い回したい(再利用したい)場合
- ソートのルールが複雑な場合
(メイン処理コードの可読性が著しく低下しするケース) - 単体テストなどで、ソートのロジックそのものが正しく機能するかどうかを個別にテストしたい場合。
※ただし、業務で作成する場合はコーディング規約やプロジェクトの指針に従って利用してください
実際のシステム開発では、「特定条件での表示順」といったルールはシステム全体で共通化されるべき業務ルール(ドメインロジック)の一部であることがほとんどです。 これらを独立したカスタム Comparator クラスとして実装・管理することで、変更に強く、可読性の高いコードベースを構築することができます。


