カスタムComparatorは「業務ルールをそのまま並び順にする道具」
Comparator は「どっちが先か」を決めるための“比較ルール”です。
カスタムComparatorは、そのルールを自分で定義することを指します。
業務では「数字の昇順・降順」だけでは足りません。
「ステータスの優先度順」「等級の高い順」「コードの並び順(A1, A2, B1…)」など、
ビジネス固有の“並び方”がたくさん出てきます。
それを if 文だらけで書くのではなく、
「Comparator に閉じ込めて名前をつける」ことで、
読みやすく・再利用しやすくするのが、カスタムComparatorの本質です。
まずは「自分で compare を書いてみる」
Comparator 実装の最小形
Comparator は「2つの値を比べて、順番を決める」インターフェースです。
戻り値の意味はこうです。
負の値:左が先
0:同じとみなす
正の値:右が先
例えば、「文字列の長さが短い順」に並べたい Comparator を自前で書いてみます。
import java.util.Comparator;
public class LengthComparator implements Comparator<String> {
@Override
public int compare(String a, String b) {
int lenA = (a == null) ? 0 : a.length();
int lenB = (b == null) ? 0 : b.length();
return Integer.compare(lenA, lenB);
}
}
Java使う側はこうです。
import java.util.ArrayList;
import java.util.List;
public class CustomComparatorSample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("aaa", "b", "cc"));
list.sort(new LengthComparator());
System.out.println(list); // [b, cc, aaa]
}
}
Javaここで押さえてほしい重要ポイントは二つです。
一つ目は、「compare メソッドの中に“並び順のルール”がすべて書かれている」ことです。
長さをどう扱うか、null をどう扱うか、すべてここで決めます。
二つ目は、「Comparator に名前をつけることで、“何順なのか”が一目で分かる」ことです。LengthComparator というクラス名を見ただけで、「長さ順なんだな」と分かります。
ラムダ式で“その場のカスタムComparator”を書く
匿名クラスを書かずに、ルールだけを書く
毎回クラスを作るのは重いので、
Java 8 以降ならラムダ式で簡潔に書けます。
import java.util.ArrayList;
import java.util.List;
public class CustomComparatorLambda {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("aaa", "b", "cc"));
list.sort((a, b) -> {
int lenA = (a == null) ? 0 : a.length();
int lenB = (b == null) ? 0 : b.length();
return Integer.compare(lenA, lenB);
});
System.out.println(list); // [b, cc, aaa]
}
}
Javaここでのポイントは、
「(a, b) -> ... の中に“並び順のルール”だけを書ける」ことです。
ただし、ラムダ式は“その場限り”になりがちなので、
何度も使うルールはユーティリティクラスに切り出して、public static final Comparator<...> として定義しておく方が実務的です。
業務っぽい例:ステータスの優先度順
「NEW < IN_PROGRESS < DONE < CANCELLED」という独自順
よくあるのが、「文字列の辞書順ではなく、業務上の優先度順で並べたい」というケースです。
例えば、ステータスを次の順番で並べたいとします。
NEW → IN_PROGRESS → DONE → CANCELLED
文字列の自然順だと、アルファベット順になってしまい、意図と違います。
そこで、カスタムComparatorを作ります。
import java.util.Comparator;
import java.util.Map;
public final class StatusComparators {
private StatusComparators() {}
private static final Map<String, Integer> ORDER =
Map.of(
"NEW", 1,
"IN_PROGRESS", 2,
"DONE", 3,
"CANCELLED", 4
);
public static final Comparator<String> BY_BUSINESS_ORDER =
(a, b) -> {
int oa = ORDER.getOrDefault(a, Integer.MAX_VALUE);
int ob = ORDER.getOrDefault(b, Integer.MAX_VALUE);
return Integer.compare(oa, ob);
};
}
Java使う側はこうです。
import java.util.ArrayList;
import java.util.List;
public class StatusSortSample {
public static void main(String[] args) {
List<String> statuses = new ArrayList<>(
List.of("DONE", "NEW", "CANCELLED", "IN_PROGRESS")
);
statuses.sort(StatusComparators.BY_BUSINESS_ORDER);
System.out.println(statuses); // [NEW, IN_PROGRESS, DONE, CANCELLED]
}
}
Javaここで深掘りしたい重要ポイントは三つです。
一つ目は、「Map で“順位”を定義し、それを比較に使っている」ことです。
このパターンは「業務固有の順番」を表現するときの定番です。
二つ目は、「定義されていないステータスは Integer.MAX_VALUE として最後に飛ばしている」ことです。
未知の値が来ても、とりあえず末尾に寄せる、という安全な挙動になります。
三つ目は、「BY_BUSINESS_ORDER という名前が“業務順”であることを示している」ことです。
クラス名+フィールド名で、仕様をそのまま表現しています。
複数条件を組み合わせたカスタムComparator
「年齢の降順、同じ年齢なら名前の昇順」
業務では、「まずこれで並べて、同じならこれで…」という複合条件がよく出ます。
Comparator はそれをとても素直に書けます。
import java.util.Comparator;
public final class UserComparators {
private UserComparators() {}
public static final Comparator<User> BY_AGE_DESC_THEN_NAME_ASC =
Comparator.comparingInt(User::getAge)
.reversed()
.thenComparing(User::getName);
}
Java使う側はこうです。
import java.util.ArrayList;
import java.util.List;
public class UserSortSample {
public static void main(String[] args) {
List<User> users = new ArrayList<>(
List.of(
new User("山田", 30),
new User("佐藤", 25),
new User("鈴木", 30)
)
);
users.sort(UserComparators.BY_AGE_DESC_THEN_NAME_ASC);
}
}
Javaここでの重要ポイントは、
「BY_AGE_DESC_THEN_NAME_ASC という名前と、comparingInt → reversed → thenComparing というチェーンが、
仕様をそのまま表現している」ことです。
if 文で「年齢が同じなら…」と書くより、
Comparator チェーンで書いた方が、
“並び順の仕様”が読み取りやすくなります。
null を含むデータを扱うカスタムComparator
nullsFirst / nullsLast と組み合わせる
現実のデータには null が混ざります。
そのまま compare すると NPE になるので、
「null をどこに置くか」もルールとして決めます。
import java.util.Comparator;
public final class NullSafeComparators {
private NullSafeComparators() {}
public static final Comparator<String> NAME_ASC_NULLS_LAST =
Comparator.nullsLast(Comparator.naturalOrder());
public static final Comparator<String> NAME_ASC_NULLS_FIRST =
Comparator.nullsFirst(Comparator.naturalOrder());
}
Java使い方のイメージです。
import java.util.ArrayList;
import java.util.List;
public class NullSafeSortSample {
public static void main(String[] args) {
List<String> names = new ArrayList<>(
List.of("山田", null, "佐藤")
);
names.sort(NullSafeComparators.NAME_ASC_NULLS_LAST);
// [佐藤, 山田, null] のような並び
}
}
Javaここでのポイントは、
「null の扱いも“並び順の仕様”の一部」だと意識することです。
「null は最後に寄せる」「null は先頭に寄せる」「null は許さない」
どれを選ぶかを、カスタムComparatorに閉じ込めておくと、
後から読んだ人にも意図が伝わります。
まとめ:カスタムComparatorで身につけてほしい感覚
カスタムComparatorは、
単に「好きな順番でソートできるテクニック」ではなく、
「業務仕様としての並び順を、コードに刻むための道具」です。
compare メソッドの中に“ルール”を閉じ込める。
何度も使う並び順は、ユーティリティクラスの public static final Comparator として名前をつける。
業務固有の順番(ステータス順など)は、順位マップ+Comparator で表現する。
複数条件や null の扱いも含めて、「並び順の仕様」を Comparator チェーンで宣言する。
あなたの Comparator が
「アルゴリズム」ではなく「仕様の宣言」に見えるようになってきたら、
それはもう、実務で戦えるカスタムComparatorを扱えている状態です。
