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

Java Java
スポンサーリンク

Comparable をざっくりイメージする

まず一言でまとめると、Comparable

このクラスの“標準の並び順”はこうです

と自分自身で宣言するためのインターフェースです。

String なら「辞書順」
Integer なら「数値の昇順」

こういう「自然な順番」が最初から決まっていますよね。
自分で作ったクラスにも、その「自然な順番」を持たせたいときに実装するのが Comparable<T> です。

これを実装しておくと、

Collections.sort(list)
Arrays.sort(array)
new TreeSet<>()new TreeMap<>()

など、「並び順が必要な標準ライブラリ」が、あなたのクラスをいい感じに並べてくれるようになります。


compareTo のルールをしっかり理解する

compareTo のシグネチャと返り値の意味

Comparable<T> は基本的に 1 メソッドだけです。

int compareTo(T other);
Java

ここがすべてと言ってもいいです。

返り値の意味はこう決まっています。

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

たとえば Integer の場合、

3.compareTo(5) は負
5.compareTo(5) は 0
8.compareTo(5) は正

というイメージです。

重要なのは、

「厳密に -1, 0, 1 を返さなきゃいけないわけではない」
「負なら何でもいい、正なら何でもいい、0 だけは“等しい”ときだけ」

という点です。
よく return this.x - other.x; みたいに書くのも、それが理由です(ただしオーバーフローには注意)。


自作クラスに「自然な順序」を持たせる基本例

年齢で比較する User クラス

具体的に手を動かします。
User というクラスを、年齢昇順で並べたいとします。

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

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

    @Override
    public int compareTo(User other) {
        return Integer.compare(this.age, other.age);
    }

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

ここでのポイントをしっかり押さえます。

implements Comparable<User>
→「User 同士を compareTo で比べられます」と宣言

compareTo(User other) の中で
this.ageother.age を比べている
→ 年齢を基準に「どっちが前か」を決めている

この compareTo を書いた瞬間に、User の「自然な順序」は「年齢昇順」だと決まります。

Collections.sort で勝手にソートされる

この UserList に入れてソートするとどうなるか。

import java.util.*;

public class ComparableBasic {
    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));

        Collections.sort(users);   // ここで compareTo が使われる

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

Collections.sort(users) は、Comparator を渡していません。
それでもちゃんと年齢順になっています。

理由は、「User が Comparable を実装しているから」です。
Collections.sort は、「要素が Comparable なら、その compareTo を使う」のが基本仕様だからです。


TreeSet / TreeMap と Comparable の関係(ここは実務で効いてくる)

TreeSet は「自然順序」で自動的にソートされる

TreeSet<User> を作ると、要素は常に「自然な順序(compareTo の定義)」で保持されます。

import java.util.*;

public class ComparableTreeSet {
    public static void main(String[] args) {
        Set<User> set = new TreeSet<>();

        set.add(new User("Alice", 30));
        set.add(new User("Bob",   20));
        set.add(new User("Carol", 25));

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

TreeSet は内部で「木構造+比較」を使って順序を保っているので、

要素を追加した瞬間に「正しい位置」に挿入
「常にソート済み」の集合になる

という性質が生まれます。

ここで使われる「比較」が、User.compareTo です。

TreeMap のキーとして使う場合

TreeMap のキーに User を使うこともできます。

TreeMap<User, String> map = new TreeMap<>();
map.put(new User("Alice", 30), "A");
map.put(new User("Bob",   20), "B");
map.put(new User("Carol", 25), "C");

System.out.println(map);
// {Bob(20)=B, Carol(25)=C, Alice(30)=A}
Java

キーは年齢順に並びます。

Tree 系のコレクションは、「並び順」が前提にあるので、
キーや要素に使うクラスは

Comparable を実装している
または Comparator を渡している

のどちらかが必須です。


compareTo を書くときに絶対意識したい「一貫性」のルール

ここ、少し深掘りします。
Comparable を実装するときに雑にやると、TreeSet / TreeMap で事故ります。

ルール 1:equals と compareTo の意味を揃える

理想的には、

a.equals(b) == true なら a.compareTo(b) == 0

であるべきです。

逆向き(compareTo == 0 なら equals も true)まで絶対ではありませんが、
「等しいかどうか」と「順序上同じかどうか」は、基本的に同じ基準であったほうが安全です。

例として、「ID が同じなら同一ユーザー」という設計のクラスを考えます。

class User implements Comparable<User> {
    String id;
    String name;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof User)) return false;
        User other = (User) o;
        return Objects.equals(this.id, other.id);
    }

    @Override
    public int compareTo(User other) {
        return this.id.compareTo(other.id);  // equals と同じ基準
    }
}
Java

ここで compareTo を「name」で比較してしまうと、
Set / Map の挙動が「等しいはずなのに別物扱いされたり、その逆だったり」と、気持ち悪くなります。

ルール 2:反対称・推移律を守る(難しそうで、実は自然)

難しく書くとこうです。

sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
x.compareTo(y) > 0 かつ y.compareTo(z) > 0 なら、x.compareTo(z) > 0

ざっくりいうと、

x vs y の大小関係と y vs x の大小関係は逆転しているはず
x > y かつ y > z なら、当然 x > z であるべき

という、ごく自然な当たり前を守ろう、という話です。

普通に「1 項目で昇順」「2 項目で昇順」「降順なら minus をつける」
くらいで書いていれば、よほど変なことは起こりません。

変なことをすると TreeSet / TreeMap 内の順序が壊れたり、
無限ループに近い挙動になったりして、バグ沼にハマります。


Comparable と Comparator の違いをきちんと整理する

Comparable:クラス自身が「標準の順序」を持つ

Comparable<T> は、

「このクラスをどう並べるか」をクラスの中に埋め込む

イメージです。

String なら文字列の辞書順
Integer なら数値の昇順

のように、「自然順序」として一つの正解があるものには相性がいいです。

自作クラスでも、「このクラスは基本的にこの順序で見るよね」がハッキリしているもの
(たとえば ID、タイムスタンプ、スコアなど)には、Comparable を実装するのがスッキリします。

Comparator:外から「場面ごとの順序」を後付けする

一方で Comparator<T> は、

「この場面ではこう並べたい」というルールを外側から与える

ためのものです。

同じ User でも、

画面 A:年齢昇順
画面 B:名前昇順
画面 C:入会日降順

というように、「場面ごとに順序が違う」ことはよくあります。

このとき、User 自体の compareTo は変えたくない。
でもソートのたびに別の “並べ方” が必要。

→ そういうときは Comparator の出番です。

Comparable と Comparator の位置づけをまとめると、

「クラスとしての標準順序」→ Comparable
「場面ごとのカスタム順序」→ Comparator

という整理が一番しっくりきます。


Comparable を実装するか迷ったときの考え方

実装したほうがいい場面

クラスに対して、こんな感覚があるなら Comparable を実装する価値が高いです。

「このクラスは、基本的には いつもこの順番で見る よね」

例えば、

ID(数値)で一意に管理されていて、その昇順が自然
日時(LocalDateTime)をラップしたクラスで、時間の早い順が自然
スコア(点数)をラップしたクラスで、数値の昇順が自然

こういうときは、compareTo を定義しておくと、

TreeSet / TreeMap
Collections.sort
stream().sorted()

などで、とても扱いやすくなります。

実装しないほうがいい/Comparator で対応すべき場面

逆に、こういうときは無理に Comparable にしないほうがいいです。

「そもそも“標準の順序”なんて決めにくい」
「場面によって並べ方がコロコロ変わる」

例えば User の場合、

基本は ID 順で扱いたい
でも UI 上は名前順でソートしたいことも多い
管理画面では最後のログイン日時順に並べたい

といったように、「場面依存」が強いなら、
クラス自体の compareTo を固定してしまうと後から苦しくなります。

その場合は、

自然順序は ID で決めておく(またはあえて Comparable にしない)
ソートしたい場面では、必ず Comparator を明示する

という方針が安全です。


まとめ:Comparable を自分の中でどう位置づけるか

Comparable を初心者向けにまとめると、こうなります。

クラス自身に「自然な並び順(標準の順序)」を教えるためのインターフェース。
compareTo の返り値で「this が引数より前か、同じか、後ろか」を決める。
一度実装すると、Collections.sortTreeSet / TreeMap などがその順序で自動的に動く。
実装するときは、equals との意味の整合・比較の一貫性(反対称・推移律)を意識する。
「いつもこの順番で見るよね」というクラスには向いているが、場面ごとに順序が変わるなら Comparator を使う。

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