Java | Java 標準ライブラリ:Comparator

Java Java
スポンサーリンク

Comparator の役割をざっくりイメージする

Comparator は一言でいうと、

「オブジェクト同士を“どういう順番で並べるか”を後付けで教えるためのオブジェクト」

です。

intString は、すでに「自然な順番(昇順・辞書順)」を持っています。
でも、自分で作ったクラス UserProduct は、「何順で並べるべきか」が決まっていません。

そこで登場するのが Comparator<T>

「年齢昇順で並べたい」
「名前の辞書順で並べたい」
「点数の降順、そのあと名前昇順で並べたい」

といった“並べ方のルール”をオブジェクトとして表現するのが、Comparator の仕事です。


Comparator の基本:compare メソッドの意味

compare の返り値のルール

Comparator<T> は、基本的にこの 1 メソッドだけで考えてかまいません。

int compare(T o1, T o2);
Java

返り値の意味はこうです。

負の値 …「o1 は o2 より小さい(前に来る)」
0 …「o1 と o2 は等しい(順序上は同じ)」
正の値 …「o1 は o2 より大きい(後ろに来る)」

たとえば「数値の昇順」を定義する Comparator を、あえて手書きするとこうなります。

Comparator<Integer> asc = new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return Integer.compare(a, b);  // a < b なら負、a == b なら0、a > b なら正
    }
};
Java

実際にはわざわざこう書きませんが、「compare の意味」はこのイメージで理解しておいてください。


ArrayList の sort に Comparator を渡してみる

自作クラスを「年齢昇順」でソートする例

まずは、Comparator がいちばん使われる場面からいきます。
List のソートです。

自作クラス User を定義してみます。

class User {
    String name;
    int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }
}
Java

この User 達を「年齢昇順」で並べたいとします。

import java.util.*;

public class ComparatorBasic {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("Alice", 30));
        users.add(new User("Bob", 20));
        users.add(new User("Carol", 25));

        Comparator<User> byAgeAsc = new Comparator<User>() {
            @Override
            public int compare(User u1, User u2) {
                return Integer.compare(u1.age, u2.age);
            }
        };

        users.sort(byAgeAsc);

        System.out.println(users);  // [Bob(20), Carol(25), Alice(30)]
    }
}
Java

ここでやっているのは、

users.sort に、“年齢で比較するルール” を渡している」

ということです。

Integer.compare(u1.age, u2.age) の部分が、

u1 の方が若い → 負
同じ年齢 → 0
u1 の方が年上 → 正

となるので、ソート結果が年齢昇順になります。


ラムダ式で Comparator をもっとシンプルに書く

Java 8 以降の書き方(ほぼこれだけでいい)

上の byAgeAsc を、Java 8 以降のラムダ式で書くとこうなります。

Comparator<User> byAgeAsc = (u1, u2) -> Integer.compare(u1.age, u2.age);
Java

さらに、Comparator.comparing を使うと、ほぼ自然語になります。

Comparator<User> byAgeAsc =
        Comparator.comparing(u -> u.age);
Java

もしくはメソッド参照も使えます(ゲッターがある場合)。

class User {
    String name;
    int age;

    int getAge() { return age; }

    // コンストラクタなどは省略
}

Comparator<User> byAgeAsc =
        Comparator.comparing(User::getAge);
Java

users.sort(byAgeAsc); のところを、
次のように一行にまとめることもできます。

users.sort(Comparator.comparing(User::getAge));
Java

「User を getAge の結果で比較してね」という宣言に近い読み方ができるので、
可読性がかなり上がります。


さらに一歩:降順・複合条件のソート

降順にしたいときは reversed()

「年齢の高い順(降順)にしたい」なら、reversed() を使います。

Comparator<User> byAgeDesc =
        Comparator.comparing(User::getAge).reversed();

users.sort(byAgeDesc);
Java

reversed() は、もとの比較結果を単純に反転します。

もともと「若いほど前に来る」
→ reversed すると「若いほど後ろに行く(=年齢降順)」

というイメージです。

年齢昇順+同年齢なら名前昇順(thenComparing)

「年齢が低い順。年齢が同じなら名前の辞書順」
みたいな複合条件は、thenComparing を使うととても綺麗に書けます。

Comparator<User> byAgeThenName =
        Comparator.comparing(User::getAge)
                  .thenComparing(u -> u.name);
Java

これで動きはこうなります。

年齢が違う → 年齢で比較結果が決まる
年齢が同じ → 名前で比較する

実際に使うとこんな感じです。

List<User> users = new ArrayList<>();
users.add(new User("Bob",   20));
users.add(new User("Alice", 20));
users.add(new User("Carol", 25));

users.sort(
    Comparator.comparing(User::getAge)
              .thenComparing(u -> u.name)
);

System.out.println(users);
// [Alice(20), Bob(20), Carol(25)]
Java

現場のコードでは、この comparingthenComparing の組み合わせが頻出します。
読み書きできるようになると、ソート周りで一気に戦闘力が上がります。


TreeSet / TreeMap と Comparator の関係

「順序付きコレクション」に独自の順番を教える

TreeSetTreeMap は、「常にソートされた順で保持してくれるコレクション」でした。

この「ソート順」を決めるのも、Comparator の重要な役割です。

たとえば、「名前の辞書順でユーザーを保持する TreeSet」を作りたければ、こう書けます。

TreeSet<User> set = new TreeSet<>(
        Comparator.comparing(u -> u.name)
);
Java

これで、

set に add した瞬間に、Comparator に従って「正しい位置」に挿入され、
常に「名前昇順」の Set になります。

TreeMap も同様で、キーの並び順を Comparator で決められます。

TreeMap<String, User> map = new TreeMap<>(
        Comparator.reverseOrder()  // キー(String)を逆順に
);
Java

「自然順序(Comparable)ではなく、特定の場面専用の順序で管理したい」
というとき、Comparator は Tree 系コレクションとセットでよく使われます。


Comparable との違いを整理する(ここを理解すると一段進める)

Comparable は「クラス自身に順序を埋め込む」

Comparable<T> インターフェースは、クラス自体に

「自分同士をこう比較します」

という“自然順序”を持たせる仕組みです。

class User implements Comparable<User> {
    String name;
    int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(this.age, other.age);  // 自然順序=年齢昇順と決める
    }
}
Java

こうしておけば、

Collections.sort(List<User>)
new TreeSet<User>()

のように、Comparator を渡さなくても「年齢順」で並びます。

Comparator は「場面ごとの順序を後付けで与える」

一方で Comparator は、「クラスの外側から順序を提供する」仕組みです。

同じ User でも、

ある画面では年齢順
別の画面では名前順
また別のロジックでは入会日の新しい順

といったように、「場面によって並べ方を変えたい」ことはよくあります。

このとき、

「クラス自体に一つの順序を埋め込む」のは困る
「必要な場面ごとに違う Comparator を使い分けたい」

という設計のほうが柔軟です。

なので実務では、

Comparable で自然順序を一応持たせつつ
場面によっては Comparator を使って明示的に順序を指定

という使い分けをすることが多いです。

初心者の段階では、まず「色々な並べ方を作れるのが Comparator」
と理解しておけば十分です。


まとめ:Comparator を自分の中でこう位置づける

ポイントだけギュッとまとめます。

Comparator は「オブジェクト同士の並べ方(順序)」をオブジェクトとして表現するもの。
compare(o1, o2) の返り値で「o1 が o2 より前か後ろか、同じか」を決める。
List#sort, Collections.sort, TreeSet, TreeMap などで「並べ方」を指定するときに使う。
Java 8 以降は Comparator.comparing, reversed, thenComparing を組み合わせるのが実務での定番。
Comparable が「クラス自身の標準の並び方」なのに対し、Comparator は「場面ごとの並び方」を後からいくつでも作れる。

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