- JAVA
Java Comparatorで複数キーソート|Comparableとの違いも表で
Javaで複数キーのソートをするには、Comparator.comparing(1つ目のキー).thenComparing(2つ目のキー)... のように、比較のルールをつなげて1つの Comparator を作り、list.sort(...) に渡します。 降順にしたいキーだけ、そのキーの Comparator に reversed() を付けるか、Comparator.reverseOrder() を渡します。
Comparator は「並べ方のルール」を表すインターフェースで、Comparable は「クラス自身が持つ、いつもの並び順」を表すインターフェースです。この記事では、まず逆引きの早見表と2つの違いの比較表で答えを示し、そのあと record を使った「部署 昇順 → 年齢 降順 → 名前 昇順」のコピペできるコードと実行結果、チェーンの途中に書いた reversed() が全体を逆にしてしまう落とし穴の再現、null を含むときの NullPointerException と nullsFirst/nullsLast、Collator を使った日本語の並び、List.sort・Stream.sorted・TreeMap・PriorityQueue での使い方、compare を引き算で書いたときのオーバーフローまで解説します。コードはすべて OpenJDK 21.0.10 で実行し、Java 25 の API は公式ドキュメントで確認しました。
Comparator 逆引き早見表
やりたいことから、そのまま書き方を引ける表です。 E は並べたい要素の型(例:Employee)です。
| やりたいこと | 書き方 |
|---|---|
| 1つのキーで昇順 | Comparator.comparing(E::name) |
| int のキーで昇順(ボクシングなし) | Comparator.comparingInt(E::age) |
| 1つのキーで降順 | Comparator.comparing(E::name).reversed() または Comparator.comparing(E::name, Comparator.reverseOrder()) |
| 2つ目以降のキーを足す | .thenComparing(E::name) / .thenComparingInt(E::age) |
| 2つ目のキーだけ降順 | .thenComparing(E::age, Comparator.reverseOrder()) または .thenComparing(Comparator.comparingInt(E::age).reversed()) |
| 全体を逆順 | 最後に .reversed() |
| 自然順序(Comparable の順) | Comparator.naturalOrder() |
| 自然順序の逆 | Comparator.reverseOrder() |
| キーが null のものを最後に | Comparator.comparing(E::stock, Comparator.nullsLast(Comparator.naturalOrder())) |
| 要素そのものの null を先頭に | Comparator.nullsFirst(Comparator.naturalOrder()) |
| 日本語(かな)を辞書順に | Comparator.comparing(E::kana, Collator.getInstance(Locale.JAPANESE)) |
| 任意の順番(指定リストの順) | Comparator.comparingInt((E e) -> order.indexOf(e.dept())) |
| 数値を自前で比べる | Integer.compare(a, b)(引き算 a - b は使わない) |
どこに渡すかの早見表です。
| 渡す先 | 書き方 | 特徴 |
|---|---|---|
| List を並べ替える | list.sort(c) |
リスト自身が変わる。安定ソート |
| 新しい並びを受け取る | list.stream().sorted(c).toList() |
元のリストは変わらない |
| 配列 | Arrays.sort(array, c) |
オブジェクト配列のみ(int[] には Comparator を渡せない) |
| 並んだ Map | new TreeMap<>(c) |
キーを常にその順で保つ |
| 並んだ Set | new TreeSet<>(c) |
compare が 0 のものは重複として1つになる |
| 優先度付きキュー | new PriorityQueue<>(c) |
poll で最小(先頭)から取り出す |
| 最大・最小 | Collections.max(list, c) / stream.max(c) |
並べ替えずに1件だけ取り出す |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Comparatorで複数キーソート|Comparableとの違いも表で」
Comparable と Comparator の違い
Comparable は「そのクラスの標準の並び順を、クラス自身に1つだけ持たせる」もの、Comparator は「並べ方を外から何通りでも渡す」ものです。
| 観点 | Comparable | Comparator |
|---|---|---|
| パッケージ | java.lang |
java.util |
| 実装するメソッド | int compareTo(T other) |
int compare(T a, T b) |
| どこに書くか | 並べたいクラス自身に implements |
クラスの外(ラムダ、メソッド参照、定数) |
| 並び順の数 | 1つ(自然順序) | 何通りでも |
| 呼び方 | Collections.sort(list)、list.sort(null)、new TreeSet<>() |
list.sort(c)、new TreeSet<>(c) |
| 標準で実装しているもの | String、Integer、LocalDate、BigDecimal、enum など | ― |
| 変更できないクラスにも使えるか | 使えない(ソースを書き換える必要がある) | 使える |
| Java 8 以降の便利メソッド | なし | comparing、thenComparing、reversed、nullsFirst など |
| 向いている場面 | 誰が見ても順番が1つに決まる値(バージョン番号、日付など) | 画面ごとに並べ方が変わる一覧、複数キー |
戻り値のルールはどちらも同じです。
| 戻り値 | 意味 |
|---|---|
| 負の数 | 1つ目(this/a)が前 |
| 0 | 同じ順位 |
| 正の数 | 1つ目(this/a)が後ろ |
値は -1/0/1 である必要はなく、符号だけが意味を持ちます。
Comparable を実装した record と、していない record
// ComparableDemo.java(java ComparableDemo.java で単一ファイル実行)
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
public class ComparableDemo {
// Comparable:クラス自身が「いつもの並び順(自然順序)」を持つ
record Version(int major, int minor) implements Comparable<Version> {
private static final Comparator<Version> ORDER =
Comparator.comparingInt(Version::major).thenComparingInt(Version::minor);
@Override
public int compareTo(Version other) {
return ORDER.compare(this, other);
}
@Override
public String toString() { return major + "." + minor; }
}
// Comparable を実装していない record
record Task(String title, int priority) { }
public static void main(String[] args) {
List<Version> versions = new ArrayList<>(List.of(
new Version(1, 10), new Version(1, 2), new Version(2, 0), new Version(1, 9)));
Collections.sort(versions); // compareTo が使われる
System.out.println("自然順序 : " + versions);
versions.sort(Comparator.reverseOrder()); // 自然順序の逆
System.out.println("reverseOrder : " + versions);
System.out.println("max : " + Collections.max(versions));
// Comparable でないものを TreeSet に入れる
TreeSet<Task> tasks = new TreeSet<>();
try {
tasks.add(new Task("備品発注", 2));
} catch (ClassCastException e) {
System.out.println("TreeSet : " + e.getMessage());
}
// Comparator を渡せば Comparable でなくても並べられる
TreeSet<Task> byPriority = new TreeSet<>(
Comparator.comparingInt(Task::priority).thenComparing(Task::title));
byPriority.add(new Task("備品発注", 2));
byPriority.add(new Task("週報作成", 1));
System.out.println("Comparator : " + byPriority);
}
}
実行結果(java -Dstdout.encoding=UTF-8 ComparableDemo.java、OpenJDK 21.0.10):
自然順序 : [1.2, 1.9, 1.10, 2.0]
reverseOrder : [2.0, 1.10, 1.9, 1.2]
max : 2.0
TreeSet : class ComparableDemo$Task cannot be cast to class java.lang.Comparable (ComparableDemo$Task is in unnamed module of loader com.sun.tools.javac.launcher.Main$MemoryClassLoader @34a3d150; java.lang.Comparable is in module java.base of loader 'bootstrap')
Comparator : [Task[title=週報作成, priority=1], Task[title=備品発注, priority=2]]
ポイントは3つです。
- バージョンを文字列 "1.10" と "1.9" で比べると "1.10" が先になりますが、数値の major・minor で比べる compareTo を持たせたので 1.9 → 1.10 の順になっています。
- compareTo の中身を Comparator の定数で書くと、比較のロジックを1行で書けて、引き算の間違い(後述)も起きません。
- Comparable でない Task を、Comparator なしの TreeSet に入れると 実行時に ClassCastException になります(
@34a3d150の部分は実行のたびに変わります)。
Collections.sort(tasks) のようにリストを渡す場合は、コンパイル時にエラーになります。
NgSortCompile.java:9: error: no suitable method found for sort(List<Task>)
Collections.sort(tasks);
^
method Collections.<T#1>sort(List<T#1>) is not applicable
(inference variable T#1 has incompatible bounds
equality constraints: Task
upper bounds: Comparable<? super T#1>)
method Collections.<T#2>sort(List<T#2>,Comparator<? super T#2>) is not applicable
(cannot infer type-variable(s) T#2
(actual and formal argument lists differ in length))
where T#1,T#2 are type-variables:
T#1 extends Comparable<? super T#1> declared in method <T#1>sort(List<T#1>)
T#2 extends Object declared in method <T#2>sort(List<T#2>,Comparator<? super T#2>)
1 error
upper bounds: Comparable<? super T#1> が出たら「Comparable を実装していないものを、Comparator なしで並べようとしている」合図です。Comparator を2つ目の引数に渡すか、クラスに Comparable を実装します。
複数キーでソートする:部署昇順 → 年齢降順 → 名前昇順
ここが本題のコピペ用コードです。 前提は Java 16 以降(record を使うため)、import は java.util だけです。
// MultiKeySort.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class MultiKeySort {
record Employee(String dept, int age, String name) { }
public static void main(String[] args) {
List<Employee> list = new ArrayList<>(List.of(
new Employee("営業", 28, "Sato"),
new Employee("開発", 35, "Ito"),
new Employee("営業", 41, "Kato"),
new Employee("開発", 35, "Abe"),
new Employee("総務", 30, "Ueda"),
new Employee("開発", 24, "Mori"),
new Employee("営業", 28, "Endo")
));
// 部署 昇順 → 年齢 降順 → 名前 昇順
Comparator<Employee> byDeptAgeDescName =
Comparator.comparing(Employee::dept)
.thenComparing(Comparator.comparingInt(Employee::age).reversed())
.thenComparing(Employee::name);
list.sort(byDeptAgeDescName);
list.forEach(System.out::println);
}
}
実行結果(java -Dstdout.encoding=UTF-8 MultiKeySort.java、OpenJDK 21.0.10):
Employee[dept=営業, age=41, name=Kato]
Employee[dept=営業, age=28, name=Endo]
Employee[dept=営業, age=28, name=Sato]
Employee[dept=総務, age=30, name=Ueda]
Employee[dept=開発, age=35, name=Abe]
Employee[dept=開発, age=35, name=Ito]
Employee[dept=開発, age=24, name=Mori]
出力を読み解くと、3つのキーが順番に効いていることがわかります。
| 行 | 効いているキー |
|---|---|
| 営業 → 総務 → 開発 | 1つ目:部署の昇順 |
| 営業の中で 41 → 28 | 2つ目:年齢の降順 |
| 28歳の Endo → Sato、35歳の Abe → Ito | 3つ目:名前の昇順(年齢が同じときだけ使われる) |
thenComparing の動きは「前のキーで比べて 0(同じ順位)だったときだけ、次のキーで比べる」です。2つ目以降のキーは、前のキーが同じもの同士の並びを決めるために使われます。
部署の並びが「営業 → 総務 → 開発」になる理由
String の自然順序は、文字の Unicode のコード番号の順です。営(U+55B6)、総(U+7DCF)、開(U+958B)の順に番号が小さいので、この並びになります。漢字の読み(えいぎょう・かいはつ・そうむ)の順ではありません。日本語の並びについては後半の Collator の節で扱います。
画面に出したい順番が決まっているときは、順番のリストを作って indexOf で比べると簡単です。
// CustomOrder.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class CustomOrder {
record Employee(String dept, int age, String name) { }
public static void main(String[] args) {
// 画面に出したい部署の順番(文字コード順ではなく任意の順)
List<String> deptOrder = List.of("開発", "営業", "総務");
List<Employee> list = new ArrayList<>(List.of(
new Employee("営業", 28, "Sato"),
new Employee("総務", 30, "Ueda"),
new Employee("開発", 24, "Mori"),
new Employee("営業", 41, "Kato")));
list.sort(Comparator.comparingInt((Employee e) -> deptOrder.indexOf(e.dept()))
.thenComparing(Employee::age, Comparator.reverseOrder()));
list.forEach(e -> System.out.println(e.dept() + " " + e.age() + " " + e.name()));
}
}
実行結果(OpenJDK 21.0.10):
開発 24 Mori
営業 41 Kato
営業 28 Sato
総務 30 Ueda
リストにない部署は indexOf が -1 を返すので先頭に来ます。部署が enum なら、enum の自然順序(定義した順)がそのまま使えるので Comparator.comparing(Employee::dept) だけで済みます。
降順キーの書き方3通りの使い分け
| 書き方 | 例 | 向いている場面 |
|---|---|---|
| キーの Comparator を reversed() | .thenComparing(Comparator.comparingInt(E::age).reversed()) |
int・long・double のキー(ボクシングしない) |
| キー抽出+reverseOrder() | .thenComparing(E::age, Comparator.reverseOrder()) |
読みやすさ重視。Comparable なキー全般 |
| 符号を反転したラムダ | .thenComparingInt(e -> -e.age()) |
おすすめしません。Integer.MIN_VALUE で反転できない |
reversed() の掛かる範囲の落とし穴
reversed() は「その時点までにつなげた Comparator 全体」を逆にします。 チェーンの最後に書くと、1つ目のキーまで逆になります。4通りの書き方を並べて実行しました。
// ReversedPitfall.java
import java.util.Comparator;
import java.util.List;
public class ReversedPitfall {
record Employee(String dept, int age, String name) { }
static final List<Employee> DATA = List.of(
new Employee("営業", 28, "Sato"),
new Employee("開発", 35, "Ito"),
new Employee("営業", 41, "Kato"),
new Employee("開発", 24, "Mori")
);
static void show(String title, Comparator<Employee> c) {
System.out.println("--- " + title);
DATA.stream().sorted(c).forEach(e ->
System.out.println(e.dept() + " " + e.age() + " " + e.name()));
}
public static void main(String[] args) {
// NG:年齢だけ降順にしたつもり → 部署まで逆順になる
show("A: comparing(dept).thenComparingInt(age).reversed()",
Comparator.comparing(Employee::dept)
.thenComparingInt(Employee::age)
.reversed());
// OK①:年齢の Comparator だけを reversed() してから渡す
show("B: thenComparing(comparingInt(age).reversed())",
Comparator.comparing(Employee::dept)
.thenComparing(Comparator.comparingInt(Employee::age).reversed()));
// OK②:キー抽出+reverseOrder() を渡す
show("C: thenComparing(age, reverseOrder())",
Comparator.comparing(Employee::dept)
.thenComparing(Employee::age, Comparator.reverseOrder()));
// 参考:先頭キーで reversed() すると、その時点までの全体(部署だけ)が逆になる
show("D: comparing(dept).reversed().thenComparingInt(age)",
Comparator.comparing(Employee::dept)
.reversed()
.thenComparingInt(Employee::age));
}
}
実行結果(OpenJDK 21.0.10):
--- A: comparing(dept).thenComparingInt(age).reversed()
開発 35 Ito
開発 24 Mori
営業 41 Kato
営業 28 Sato
--- B: thenComparing(comparingInt(age).reversed())
営業 41 Kato
営業 28 Sato
開発 35 Ito
開発 24 Mori
--- C: thenComparing(age, reverseOrder())
営業 41 Kato
営業 28 Sato
開発 35 Ito
開発 24 Mori
--- D: comparing(dept).reversed().thenComparingInt(age)
開発 24 Mori
開発 35 Ito
営業 28 Sato
営業 41 Kato
A は年齢を降順にしたつもりが、部署も「開発 → 営業」と逆になっています。 .reversed() が comparing(dept).thenComparingInt(age) 全体に掛かったためです。B と C が「部署は昇順、年齢だけ降順」という意図どおりの結果です。
| 書き方 | 部署 | 年齢 | 意図(部署昇順・年齢降順)との一致 |
|---|---|---|---|
A:最後に .reversed() |
降順 | 降順 | 一致しない |
| B:年齢の Comparator を先に reversed | 昇順 | 降順 | 一致 |
C:thenComparing(キー, reverseOrder()) |
昇順 | 降順 | 一致 |
D:1つ目の直後に .reversed() |
降順 | 昇順 | 一致しない(部署降順にしたいなら正解) |
覚え方は「reversed() は、左側にあるもの全部をひっくり返す」です。特定のキーだけ逆にしたいときは、そのキー用の Comparator を作ってから reversed() するか、reverseOrder() を2つ目の引数に渡します。
ラムダで書くと reversed() でコンパイルエラーになる
メソッド参照ではなくラムダでキーを書き、そのまま .reversed() をつなげると、型が推論できずにエラーになります。
// LambdaReversed.java
import java.util.Comparator;
public class LambdaReversed {
record Employee(String dept, int age, String name) { }
public static void main(String[] args) {
Comparator<Employee> c = Comparator.comparing(e -> e.name()).reversed();
}
}
LambdaReversed.java:6: error: cannot find symbol
Comparator<Employee> c = Comparator.comparing(e -> e.name()).reversed();
^
symbol: method name()
location: variable e of type Object
LambdaReversed.java:6: error: incompatible types: Comparator<Object> cannot be converted to Comparator<Employee>
Comparator<Employee> c = Comparator.comparing(e -> e.name()).reversed();
^
2 errors
.reversed() をつなげると、左辺の Comparator<Employee> から型を推論できなくなり、e が Object 扱いになります。直し方は、メソッド参照 Employee::name を使うか、ラムダの引数に型を書く (Employee e) -> e.name() です。上の CustomOrder.java で (Employee e) -> と書いたのはこのためです。
null を含むときの並べ方
Comparator.comparing はキーが null だと NullPointerException になります。 null が入りうるキーには、nullsFirst/nullsLast で「null をどこに置くか」を決めた Comparator を渡します。
// NullSort.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class NullSort {
record Item(String code, Integer stock) { }
public static void main(String[] args) {
List<Item> items = new ArrayList<>(List.of(
new Item("B-02", 5),
new Item("A-01", null), // 在庫数が未登録
new Item("C-03", 12),
new Item("D-04", null)
));
// ① キーに null がある状態で comparing を使う
try {
items.sort(Comparator.comparing(Item::stock));
} catch (NullPointerException e) {
System.out.println("① " + e);
}
// ② nullsLast:null は最後、それ以外は自然順序
items.sort(Comparator.comparing(Item::stock,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(Item::code));
System.out.println("② nullsLast : " + items);
// ③ nullsFirst+降順
items.sort(Comparator.comparing(Item::stock,
Comparator.nullsFirst(Comparator.reverseOrder()))
.thenComparing(Item::code));
System.out.println("③ nullsFirst : " + items);
// ④ 要素そのものが null の場合
List<String> names = new ArrayList<>(Arrays.asList("ito", null, "abe"));
try {
names.sort(Comparator.naturalOrder());
} catch (NullPointerException e) {
System.out.println("④ " + e);
}
names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println("⑤ " + names);
}
}
実行結果(OpenJDK 21.0.10):
① java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because the return value of "java.util.function.Function.apply(Object)" is null
② nullsLast : [Item[code=B-02, stock=5], Item[code=C-03, stock=12], Item[code=A-01, stock=null], Item[code=D-04, stock=null]]
③ nullsFirst : [Item[code=A-01, stock=null], Item[code=D-04, stock=null], Item[code=C-03, stock=12], Item[code=B-02, stock=5]]
④ java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "c1" is null
⑤ [null, abe, ito]
try-catch を付けずに①を実行したときのスタックトレース(先頭部分)は次のとおりです。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because the return value of "java.util.function.Function.apply(Object)" is null
at java.base/java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:473)
at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
at java.base/java.util.TimSort.sort(TimSort.java:220)
at java.base/java.util.Arrays.sort(Arrays.java:1308)
at java.base/java.util.ArrayList.sort(ArrayList.java:1804)
at NpeTrace.main(NpeTrace.java:9)
メッセージの「Function.apply(Object) is null」は「キーを取り出したら null だった」という意味です。Java 14 以降の詳しい NullPointerException メッセージ(JEP 358)のおかげで、原因がキーの null だと読み取れます。
| 状況 | 書き方 | ②③の結果から読み取れること |
|---|---|---|
| キーの null を最後に | comparing(E::key, nullsLast(naturalOrder())) |
null 以外は昇順、null の2件は code 順で最後 |
| キーの null を先頭に+降順 | comparing(E::key, nullsFirst(reverseOrder())) |
null が先頭、残りは 12 → 5 の降順 |
| 要素そのものが null | nullsFirst(naturalOrder()) を直接渡す |
⑤のとおり null が先頭 |
nullsFirst/nullsLast は「null の位置」だけを決め、null でないもの同士は引数の Comparator で比べます。 降順にしたいときは nullsLast(...).reversed() ではなく、中に reverseOrder() を渡すと null の位置を保ったまま降順にできます(reversed() を外側に付けると null の位置も逆になります)。
日本語を並べる:Collator
String の自然順序は文字コード順なので、ひらがなとカタカナ、半角と全角が別々のかたまりに分かれます。 日本語の辞書順に近づけたいときは java.text.Collator を Comparator として渡します。
// JapaneseSort.java
import java.text.Collator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class JapaneseSort {
record Member(String name, String kana) { }
public static void main(String[] args) {
List<String> words = List.of("さくら", "アイス", "いちご", "あめ", "カメラ", "Zebra", "apple", "Box", "うどん");
List<String> a = new ArrayList<>(words);
a.sort(Comparator.naturalOrder());
System.out.println("自然順序 : " + a);
Collator collator = Collator.getInstance(Locale.JAPANESE);
List<String> b = new ArrayList<>(words);
b.sort(collator);
System.out.println("Collator : " + b);
// 漢字は読みを持っていないので、読み(カナ)の列で並べる
List<Member> members = new ArrayList<>(List.of(
new Member("渡辺", "ワタナベ"),
new Member("阿部", "アベ"),
new Member("佐藤", "サトウ"),
new Member("伊藤", "イトウ")
));
members.sort(Comparator.comparing(Member::name));
System.out.println("漢字のまま: " + members.stream().map(Member::name).toList());
members.sort(Comparator.comparing(Member::kana, collator));
System.out.println("読みで並べ: " + members.stream().map(Member::name).toList());
}
}
実行結果(OpenJDK 21.0.10):
自然順序 : [Zebra, apple, あめ, いちご, うどん, さくら, アイス, カメラ, Box]
Collator : [apple, Zebra, アイス, あめ, いちご, うどん, カメラ, さくら, Box]
漢字のまま: [伊藤, 佐藤, 渡辺, 阿部]
読みで並べ: [阿部, 伊藤, 佐藤, 渡辺]
結果からわかることをまとめます。
| 比べ方 | 英字 | かな | 漢字 |
|---|---|---|---|
| 自然順序 | 大文字が先(Z が a より前) | ひらがなの後にカタカナ | コード番号順(読みとは無関係) |
| Collator(日本語) | 大文字小文字を区別せず a → Z | ひらがな・カタカナを混ぜて五十音順(アイス → あめ → いちご) | 読みはわからない |
| 読みの列+Collator | ― | ― | 読み(カナ)の五十音順(阿部 → 伊藤 → 佐藤 → 渡辺) |
漢字の名前を五十音順に並べるには、読みがなの列を持っておき、その列で並べるのが確実です。また、この実行では全角の「Box」が最後に来ました。全角英数字を半角と同じように並べたい場合は、java.text.Normalizer の NFKC で正規化した値をキーにする方法があります。
List.sort・Stream.sorted・TreeMap・PriorityQueue での使い方
作った Comparator は、並べ替えの種類によって渡す先を選びます。 1つの Comparator を定数にしておけば、どこにでも使い回せます。
// WhereToUse.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.TreeMap;
import java.util.TreeSet;
public class WhereToUse {
record Task(String title, int priority, String owner) { }
static final Comparator<Task> BY_PRIORITY =
Comparator.comparingInt(Task::priority).thenComparing(Task::title);
public static void main(String[] args) {
List<Task> src = List.of(
new Task("備品発注", 2, "ueda"),
new Task("棚卸し", 3, "ito"),
new Task("週報作成", 1, "abe"),
new Task("会議室予約", 1, "ito"));
// 1) List.sort:リスト自身を並べ替える(変更可能なリストが必要)
List<Task> list = new ArrayList<>(src);
list.sort(BY_PRIORITY);
System.out.println("List.sort : " + list.stream().map(Task::title).toList());
// 2) Stream.sorted:元のリストは変えずに、並べた結果を受け取る
List<String> sorted = src.stream().sorted(BY_PRIORITY.reversed()).map(Task::title).toList();
System.out.println("Stream.sorted : " + sorted);
System.out.println("元のリスト : " + src.stream().map(Task::title).toList());
// 3) Arrays.sort:配列(オブジェクト配列)
Task[] array = src.toArray(new Task[0]);
Arrays.sort(array, Comparator.comparing(Task::owner).thenComparing(BY_PRIORITY));
System.out.println("Arrays.sort : " + Arrays.stream(array).map(t -> t.owner() + ":" + t.title()).toList());
// 4) TreeMap:キーを Comparator の順で保つ
TreeMap<String, Integer> countByOwner = new TreeMap<>(Comparator.reverseOrder());
for (Task t : src) countByOwner.merge(t.owner(), 1, Integer::sum);
System.out.println("TreeMap : " + countByOwner);
// 5) PriorityQueue:いつでも「最小」を先頭から取り出せる
PriorityQueue<Task> queue = new PriorityQueue<>(BY_PRIORITY);
for (Task t : src) queue.offer(t);
StringBuilder sb = new StringBuilder();
while (!queue.isEmpty()) sb.append(queue.poll().title()).append(" ");
System.out.println("PQ poll順 : " + sb.toString().trim());
// 6) 落とし穴:TreeSet は compare が 0 の要素を「同じもの」とみなす
TreeSet<Task> byPriorityOnly = new TreeSet<>(Comparator.comparingInt(Task::priority));
byPriorityOnly.addAll(src);
System.out.println("TreeSet(優先度だけ) size=" + byPriorityOnly.size() + " " +
byPriorityOnly.stream().map(Task::title).toList());
}
}
実行結果(java -Dstdout.encoding=UTF-8 WhereToUse.java、OpenJDK 21.0.10):
List.sort : [会議室予約, 週報作成, 備品発注, 棚卸し]
Stream.sorted : [棚卸し, 備品発注, 週報作成, 会議室予約]
元のリスト : [備品発注, 棚卸し, 週報作成, 会議室予約]
Arrays.sort : [abe:週報作成, ito:会議室予約, ito:棚卸し, ueda:備品発注]
TreeMap : {ueda=1, ito=2, abe=1}
PQ poll順 : 会議室予約 週報作成 備品発注 棚卸し
TreeSet(優先度だけ) size=3 [週報作成, 備品発注, 棚卸し]
1行ずつのポイントです。
| 使い方 | 結果からわかること | 注意点 |
|---|---|---|
list.sort(c) |
優先度1の2件がタイトル順、その後 2 → 3 | List.of(...) のような変更できないリストに呼ぶと UnsupportedOperationException。new ArrayList<>(...) で包む |
stream().sorted(c) |
元のリストの並びは変わっていない | Stream の詳しい使い方は Stream API の記事で扱っています |
Arrays.sort(array, c) |
owner → 優先度の2キー | int[] などプリミティブ配列には Comparator を渡せない |
new TreeMap<>(c) |
キーが ueda → ito → abe の降順で保たれる | TreeMap.comparator() で後から確認できる |
new PriorityQueue<>(c) |
poll すると常に「最小」から出てくる | toString や for 文での並びは保証されない。順に欲しいときは poll で取り出す |
new TreeSet<>(c) |
優先度1の「会議室予約」が消えて size=3 | compare が 0 のものは重複扱い。最後のキーに一意な値(ID やタイトル)を足す |
TreeSet・TreeMap の落とし穴は特に気づきにくいので注意してください。TreeSet は equals ではなく compare(compareTo)の結果で「同じかどうか」を判断します。優先度だけで比べると、優先度が同じタスクは1件しか残りません。並び順だけでなく「区別」にも使われるコレクションでは、最後のキーまで含めて compare が 0 にならないようにすると、意図どおりに要素が残ります。
List.sort と Arrays.sort(オブジェクト配列)は安定ソートです。安定ソートとは、compare が 0 の要素どうしの元の順番が保たれる並べ替えのことで、Javadoc で保証されています。2回に分けて sort するより、thenComparing で1つの Comparator にまとめるほうが、意図が1か所で読み取れます。
compare を自前で書くときの注意:引き算はオーバーフローする
(a, b) -> a - b は昔からよく見かける書き方ですが、差が int の範囲を超えるとオーバーフローして符号が逆になり、並びが崩れます。
// SubtractOverflow.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SubtractOverflow {
public static void main(String[] args) {
List<Integer> values = List.of(5, Integer.MIN_VALUE, -3, Integer.MAX_VALUE, 0);
// NG:引き算で比較(大きな差でオーバーフローする)
Comparator<Integer> bySubtract = (a, b) -> a - b;
List<Integer> ng = new ArrayList<>(values);
ng.sort(bySubtract);
System.out.println("a - b : " + ng);
System.out.println("MAX - MIN の結果 : " + (Integer.MAX_VALUE - Integer.MIN_VALUE));
// OK:Integer.compare(または Comparator.comparingInt / naturalOrder)
List<Integer> ok = new ArrayList<>(values);
ok.sort(Integer::compare);
System.out.println("Integer.compare : " + ok);
}
}
実行結果(OpenJDK 21.0.10):
a - b : [0, 5, 2147483647, -2147483648, -3]
MAX - MIN の結果 : -1
Integer.compare : [-2147483648, -3, 0, 5, 2147483647]
引き算版では、最大値 2147483647 の後ろに最小値 -2147483648 が来ています。 Integer.MAX_VALUE - Integer.MIN_VALUE の本来の答えは約 43 億ですが、int に収まらず -1 になりました。「MAX は MIN より小さい」と判定されたわけです。
| 書き方 | 正しく比べられるか | おすすめ |
|---|---|---|
(a, b) -> a - b |
差が大きいとオーバーフロー | 使わない |
(a, b) -> Integer.compare(a, b) |
○ | ○ |
Comparator.comparingInt(E::age) |
○(内部で Integer.compare) | ◎ |
Comparator.naturalOrder() |
○ | ◎(Integer そのものを並べるとき) |
(a, b) -> Double.compare(a, b) |
○(NaN や -0.0 も一貫して扱う) | double のとき |
compare を自前で書くときは、Integer.compare・Long.compare・Double.compare を使うか、comparing 系のメソッドで組み立てるのが確実です。
compare を自前で書くときのチェックリスト
| チェック | 内容 |
|---|---|
| 符号の対称性 | compare(a, b) と compare(b, a) の符号が逆になっている |
| 推移性 | a < b かつ b < c なら a < c になる(守られないと TimSort が Comparison method violates its general contract! を出すことがある) |
| 0 の扱い | TreeSet・TreeMap に使うなら、区別したいもの同士で 0 を返さない |
| null | キーに null が来るなら nullsFirst/nullsLast で包む |
| 数値の比較 | 引き算ではなく Integer.compare などを使う |
Java 25 での変更点
Comparator インターフェースのメソッドは、Java 25 でも Java 8 で追加されたものから変わっていません。 Java SE 25 の API ドキュメント(Comparator)を確認すると、comparing、comparingInt、thenComparing、reversed、nullsFirst、nullsLast、naturalOrder、reverseOrder などはいずれも「Since 1.8」で、新しいメソッドの追加はありませんでした。この記事のコードは Java 21 で実行していますが、同じ書き方が Java 25 でもそのまま使えます(Java 25 では動かしていないため、公式ドキュメントでの確認です)。
record(Java 16)と Stream.toList()(Java 16)を使っているため、この記事のコードを動かすには Java 16 以降が必要です。
よくある質問
Q. Comparable と Comparator、どちらを使えばよいですか? A. 「そのクラスにとって誰が見ても1つに決まる順番」があるなら Comparable、画面や用途によって並べ方が変わるなら Comparator です。迷ったら Comparator から始めると、クラスを変えずに並べ方を追加できます。
Q. thenComparing と thenComparingInt はどちらを使えばよいですか?
A. キーが int・long・double なら thenComparingInt/thenComparingLong/thenComparingDouble を使うと、Integer へのボクシングが起きずに済みます。降順にしたいときは thenComparing(Comparator.comparingInt(E::age).reversed()) のように、comparingInt で作った Comparator を reversed() して渡します。
Q. Comparator をラムダで書くとき、型を書かないとエラーになるのはなぜですか?
A. Comparator.comparing(e -> e.name()).reversed() のようにメソッドをつなげると、左辺の型からラムダの引数の型を推論できなくなるためです。メソッド参照(Employee::name)を使うか、(Employee e) -> e.name() と型を書くと解決します。
まとめ
- 複数キーソートは
comparing(キー1).thenComparing(キー2)...で1つの Comparator を作って sort に渡す。前のキーが同じときだけ次のキーが使われる。 - Comparable はクラス自身の自然順序(1つ)、Comparator は外から渡す並べ方(何通りでも)。
- reversed() は左側の Comparator 全体を逆にする。特定のキーだけ降順なら
comparingInt(キー).reversed()を thenComparing に渡すか、thenComparing(キー, reverseOrder())。 - キーに null があると comparing は NullPointerException。
nullsFirst/nullsLastで null の位置を決める。 - 日本語は Collator で五十音順に近づき、漢字は読みの列で並べる。String の自然順序は文字コード順。
- TreeSet・TreeMap は compare が 0 だと重複扱い、PriorityQueue は poll で順に取り出す。
- compare の自前実装で引き算は使わず、Integer.compare か comparingInt を使う。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Comparatorで複数キーソート|Comparableとの違いも表で」
株式会社RJCでは、JavaのComparatorを使った並べ替えの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。