Java Tips | コレクション:カスタムComparator

Java Java
スポンサーリンク

カスタム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を扱えている状態です。

タイトルとURLをコピーしました