0Pricing
Java Academy · レッスン

thenComparing による複数キーソート

thenComparing で Comparator を連結し、優先順位に従って複数のフィールドでソートします。

「thenComparing による複数キーソート」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。

複数キーによるソート

thenComparing() は Comparator を連鎖させ、複数のフィールドでソートできるようにします。まず主ソートを行い、等しい場合は副ソート、さらにその次のソートという順に処理します。

基本的な thenComparing

主 Comparator で同順位になった要素を区別するには、thenComparing() を使用します。

import java.util.*;

record Student(String name, int grade, double gpa) {}

List<Student> students = List.of(
    new Student("Alice",   10, 3.9),
    new Student("Bob",     11, 3.7),
    new Student("Charlie", 10, 3.8),
    new Student("Diana",   11, 3.7)
);

Comparator<Student> sort = Comparator.comparingInt(Student::grade)
    .thenComparingDouble(Student::gpa).reversed()
    .thenComparing(Student::name);

students.stream().sorted(sort).forEach(System.out::println);

3 段階のソート

必要な数だけ thenComparing の段階を連鎖させます。

record Employee(String dept, String team, String name, double salary) {}

List<Employee> employees = List.of(
    new Employee("Eng", "Backend", "Alice",   95000),
    new Employee("Eng", "Backend", "Bob",     92000),
    new Employee("Eng", "Frontend","Charlie", 88000),
    new Employee("Mkt", "Growth",  "Diana",   75000)
);

Comparator<Employee> sort = Comparator
    .comparing(Employee::dept)
    .thenComparing(Employee::team)
    .thenComparing(Comparator.comparingDouble(Employee::salary).reversed());

employees.stream().sorted(sort)
    .forEach(e -> System.out.printf("%s > %s > %s: $%.0f%n",
        e.dept(), e.team(), e.name(), e.salary()));

Comparator を使った thenComparing

副キーに逆順やカスタム順序が必要な場合は、Comparator を受け取るオーバーロードを使用します。

Comparator<Employee> sort = Comparator
    .comparing(Employee::dept)
    .thenComparing(Comparator.comparing(Employee::salary).reversed()); // highest paid first
    // equivalent: .thenComparingDouble(Employee::salary, Comparator.reverseOrder())

null フィールドのソート

thenComparing と nullsLast/nullsFirst を組み合わせると、任意のフィールドを適切に処理できます。

record Product(String name, String category, Double rating) {}

Comparator<Product> sort = Comparator
    .comparing(Product::category, Comparator.nullsLast(Comparator.naturalOrder()))
    .thenComparing(Product::rating,  Comparator.nullsLast(Comparator.reverseOrder()))
    .thenComparing(Product::name);

動的なソートキーの選択

ユーザーの設定に基づいて、実行時にソートキーを選択します。

static Comparator<Product> sortBy(String key) {
    return switch (key) {
        case "price"    -> Comparator.comparingDouble(Product::price);
        case "name"     -> Comparator.comparing(Product::name);
        case "stock"    -> Comparator.comparingInt(Product::stock).reversed();
        default         -> Comparator.comparingDouble(Product::price);
    };
}

String userChoice = "stock";
products.stream().sorted(sortBy(userChoice)).forEach(System.out::println);

抽出関数を使った thenComparing

thenComparing(Function<T,U>) オーバーロードは、明示的な Comparator を必要とせず、2 次ソート用に Comparable なキーを抽出します。

record Event(String type, int year, int month, int day) {}

Comparator<Event> chronological = Comparator
    .comparing(Event::type)
    .thenComparingInt(Event::year)
    .thenComparingInt(Event::month)
    .thenComparingInt(Event::day);

レコードのコレクションのソート

アクセサーメソッドを持つレコードは、Comparator の連鎖でメソッド参照をそのまま利用できます。

record Order(String customerId, String status, double total, java.time.LocalDate date) {}

List<Order> orders = loadOrders();

Comparator<Order> byCustomerThenDate = Comparator
    .comparing(Order::customerId)
    .thenComparing(Order::date, Comparator.reverseOrder()); // newest first per customer

orders.stream().sorted(byCustomerThenDate)
    .collect(Collectors.groupingBy(Order::customerId))
    .forEach((cust, ords) -> {
        System.out.println("Customer: " + cust);
        ords.forEach(o -> System.out.println("  " + o.date() + " $" + o.total()));
    });

安定ソートの保証

Java の List.sort() と、オブジェクトを対象とする Arrays.sort() は安定したマージソートを使用します。比較結果が等しい要素は、元の相対的な順序を維持します。

// Stable sort example:
// First sort by name, then by dept
// Equal dept elements preserve the name order from previous sort
List<Employee> sorted = employees.stream()
    .sorted(Comparator.comparing(Employee::name))     // 1st pass
    .sorted(Comparator.comparing(Employee::dept))     // 2nd pass (stable!)
    .collect(Collectors.toList());
// Within each dept, employees remain alphabetically sorted

キー Comparator を使った Comparator.comparing

2 つの引数を取る形式 Comparator.comparing(keyExtractor, keyComparator) を使用すると、キーとその順序の両方を指定できます。

// Sort by name length, then alphabetically within same length
Comparator<String> c = Comparator
    .comparing(String::length, Integer::compare)
    .thenComparing(Comparator.naturalOrder());

List<String> words = new ArrayList<>(List.of("fig","apple","kiwi","banana","pea"));
words.sort(c);
System.out.println(words); // [fig, pea, kiwi, apple, banana]

実世界の例:フライト検索結果

フライト検索結果を、価格、所要時間、出発時刻の順にソートします。

record Flight(String airline, double price, int durationMin, java.time.LocalTime departs) {}

Comparator<Flight> bestFlight = Comparator
    .comparingDouble(Flight::price)
    .thenComparingInt(Flight::durationMin)
    .thenComparing(Flight::departs);

ソート結果の収集

複数キーによるソートとコレクターを組み合わせて、ソート済みのグループやマップを生成します。

Map<String, List<Employee>> byDept = employees.stream()
    .sorted(Comparator.comparing(Employee::dept)
        .thenComparing(Comparator.comparingDouble(Employee::salary).reversed()))
    .collect(Collectors.groupingBy(Employee::dept,
        LinkedHashMap::new,  // preserve insertion (sorted) order
        Collectors.toList()));

byDept.forEach((dept, emps) -> {
    System.out.println(dept + ":");
    emps.forEach(e -> System.out.println("  " + e.name() + " $" + e.salary()));
});

クイックチェック

従業員をまず部署名のアルファベット順、次に給与の降順でソートしたいとします。正しいチェーンはどれですか?

まとめ:thenComparingによる複数キーのソート

重要なポイント:

  • thenComparing() は、主キーが等しい場合に適用される第2のソートキーを追加します
  • 必要な数だけ thenComparing のレベルをチェーンできます
  • 第2のキーを降順にする場合は、thenComparing(Comparator) と reversed() を使用します
  • 任意のフィールドには nullsFirst/nullsLast を組み合わせます
  • Java の List.sort() は安定ソートです。同値の要素は元の順序を保持します
  • ソート方法を動的に選択するには、switch を使用して実行時に適切な Comparator を返します

よくある質問

「thenComparing による複数キーソート」レッスンは無料ですか?

はい。「thenComparing による複数キーソート」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。

「thenComparing による複数キーソート」で何を学びますか?

thenComparing で Comparator を連結し、優先順位に従って複数のフィールドでソートします。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Java Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「thenComparing による複数キーソート」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このJava Academyレッスンでコードを書いて実行できますか?

はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Comparable インターフェース
  2. Comparator とラムダによるソート
  3. thenComparing による複数キーソート
  4. 配列とコレクションのソート実践
← Java Academyに戻る