Javaプログラミングにおいて、コレクション内の要素を特定の順序で並び替える「ソート」は、実務で最も頻繁に利用される操作の一つです。

ユーザー一覧を名前順に表示したり、売上データを金額の高い順に並べたりと、その用途は多岐にわたります。

Javaには、古くから使われているCollections.sort()から、Java 8で導入されたList.sort()、さらには宣言的に記述できるStream APIまで、複数の手法が用意されています。

本記事では、JavaのListをソートするための主要な手法を、基礎から応用まで徹底的に解説します。

単なる構文の紹介にとどまらず、オブジェクトの複数条件ソートやNull値のハンドリング、パフォーマンス上の注意点についても詳しく触れていきます。

この記事を読み終える頃には、プロジェクトの要件に合わせた最適なソート手法を選択し、実装できるようになっているはずです。

JavaにおけるListソートの基本概念

JavaでListをソートする際、まず理解しておくべきは「どのように並べるか」を決定する仕組みです。

Javaには、オブジェクト自体に順序を定義するComparableインターフェースと、外部から順序を指定するComparatorインターフェースの2種類が存在します。

自然順序(Natural Ordering)とは、数値なら昇順、文字列なら辞書順といった、そのデータ型が本来持っている標準的な並び順のことです。

これに対し、特定のプロパティ(年齢や更新日時など)に基づいて自由に定義する順序をカスタム順序と呼びます。

JavaのListをソートする主要な手段は以下の3つに大別されます。

  1. Collections.sort() メソッド(従来の標準的な方法)
  2. List.sort() メソッド(Java 8以降の推奨される破壊的ソート)
  3. Stream APIsorted() (非破壊的なソート)

それぞれの特徴を表にまとめると、以下のようになります。

手法導入バージョン特徴元のListへの影響
Collections.sort()Java 1.2古くからある汎用的な方法破壊的(書き換わる)
List.sort()Java 8Listのデフォルトメソッドを使用破壊的(書き換わる)
Stream APIJava 8関数型プログラミングのスタイル非破壊的(新しいListを生成)

これらの手法を使い分けることで、効率的で読みやすいコードを記述することが可能になります。

Collections.sortによる基本的なソート

Collections.sort()は、Javaの初期から存在する最もポピュラーなソート手法です。

このメソッドは、引数として渡されたListをその場で書き換える(破壊的変更)ため、戻り値はありません。

数値や文字列の昇順ソート

ラッパークラス(Integer, Doubleなど)やStringクラスは、既にComparableインターフェースを実装しているため、特に追加の設定なしでソートが可能です。

Java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class BasicSortExample {
    public static void main(String[] args) {
        // 数値のリストを作成
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(2);
        numbers.add(8);
        numbers.add(1);

        // 自然順序(昇順)でソート
        Collections.sort(numbers);

        System.out.println("ソート後の数値: " + numbers);

        // 文字列のリストを作成
        List<String> fruits = new ArrayList<>();
        fruits.add("Orange");
        fruits.add("Apple");
        fruits.add("Banana");

        // 辞書順でソート
        Collections.sort(fruits);

        System.out.println("ソート後の文字列: " + fruits);
    }
}
実行結果
ソート後の数値: [1, 2, 5, 8]
ソート後の文字列: [Apple, Banana, Orange]

降順ソートの実装

降順に並べ替えたい場合は、Collections.reverseOrder()を第2引数に渡します。

Java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ReverseSortExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(10, 50, 20, 40));

        // 降順にソート
        Collections.sort(numbers, Collections.reverseOrder());

        System.out.println("降順ソート結果: " + numbers);
    }
}
実行結果
降順ソート結果: [50, 40, 20, 10]

List.sortメソッドによるモダンなソート

Java 8以降では、Listインターフェース自体にsort()メソッドが追加されました。

内部的にはCollections.sort()を呼び出していますが、インスタンスメソッドとして呼び出せるため、より直感的な記述が可能です。

ラムダ式を用いたComparatorの定義

List.sort()は引数にComparatorを要求します。

Java 8以降のラムダ式を用いることで、簡潔に比較ロジックを記述できます。

Java
import java.util.ArrayList;
import java.util.List;

public class ListSortLambda {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Taro", "Jiro", "Saburo"));

        // 文字列の長さ順にソート
        names.sort((a, b) -> a.length() - b.length());

        System.out.println("長さ順: " + names);
    }
}
実行結果
長さ順: [Taro, Jiro, Saburo]

Comparator.naturalOrder()を使用すれば、従来のCollections.sort(list)と同じ動作をさせることも可能です。

Comparatorインターフェースの高度な活用

実務で最も多いパターンは、独自のクラス(エンティティやDTO)のリストを、特定のフィールドに基づいてソートすることです。

この際、Comparatorの静的メソッドを活用すると、非常に宣言的で可読性の高いコードになります。

独自のオブジェクトをソートする

例えば、以下のようなUserクラスがあるとします。

Java
class User {
    private String name;
    private int age;

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

    public String getName() { return name; }
    public int getAge() { return age; }

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

このUserリストを「年齢順」にソートする場合、Comparator.comparingInt()を使用するのが最適です。

Java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ObjectSortExample {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("田中", 30));
        users.add(new User("佐藤", 25));
        users.add(new User("鈴木", 35));

        // 年齢(ageプロパティ)で昇順ソート
        users.sort(Comparator.comparingInt(User::getAge));

        System.out.println("年齢昇順: " + users);
        
        // 年齢で降順ソート
        users.sort(Comparator.comparingInt(User::getAge).reversed());
        
        System.out.println("年齢降順: " + users);
    }
}
実行結果
年齢昇順: [佐藤(25), 田中(30), 鈴木(35)]
年齢降順: [鈴木(35), 田中(30), 佐藤(25)]

複数条件でのソート

「まずは年齢順に並べ、年齢が同じなら名前順にする」といった複合的な条件も、thenComparing()を繋げるだけで実現できます。

Java
// 年齢昇順、かつ同じ年齢なら名前の辞書順
users.sort(
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName)
);

このように、メソッドチェーンを用いることで複雑なビジネスルールをシンプルに表現できるのが現代的なJavaソートの強みです。

Stream APIによる非破壊的なソート

これまでに紹介したCollections.sort()List.sort()は、元のListを直接書き換える「破壊的」なメソッドでした。

しかし、副作用を避けるべきモダンな開発(特に関数型プログラミングの影響を受けている環境)では、元のリストを保持したまま新しいソート済みリストを取得したい場合があります。

その際に活躍するのがStream APIです。

sorted()メソッドの使い方

Streamのsorted()メソッドは、ソートされた要素の流れを返します。

最後にtoList()(Java 16以降)やcollect(Collectors.toList())で収集します。

Java
import java.util.List;
import java.util.stream.Collectors;

public class StreamSortExample {
    public static void main(String[] args) {
        List<String> original = List.of("Banana", "Apple", "Cherry");

        // Streamを使用してソートされた新しいリストを作成
        List<String> sortedList = original.stream()
                                          .sorted()
                                          .collect(Collectors.toList());

        System.out.println("元のリスト: " + original);
        System.out.println("新しいリスト: " + sortedList);
    }
}
実行結果
元のリスト: [Banana, Apple, Cherry]
新しいリスト: [Apple, Banana, Cherry]

Stream APIを使用するメリットは、フィルタリング(filter)や変換(map)といった他の操作とシームレスに連携できる点にあります。

例えば、「30歳以上のユーザーだけを抽出し、名前順に並べてリストにする」といった処理を一気気通貫で記述できます。

Nullが含まれるリストのソート

実務のデータには、不意にnullが混じることがあります。

標準のComparatorでソートを行うと、要素にnullが含まれていた場合にNullPointerExceptionが発生してしまいます。

これを防ぐには、Comparator.nullsFirst() または Comparator.nullsLast() を使用します。

Java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class NullSortExample {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Zebra", null, "Apple", "Monkey");

        // nullを最後に配置しつつ、他を自然順序でソート
        list.sort(Comparator.nullsLast(Comparator.naturalOrder()));

        System.out.println("Nullを最後に: " + list);
    }
}
実行結果
Nullを最後に: [Apple, Monkey, Zebra, null]

安全なアプリケーションを開発するためには、外部システムやデータベースから取得した不確実なデータをソートする際に、常にNullの存在を考慮しておくことが重要です。

注意点とパフォーマンス

Listのソートを実装するにあたって、いくつか注意すべき技術的背景があります。

1. 不変リスト(Immutable List)のソート

List.of()Collections.unmodifiableList() で作成されたリストは、中身を変更できません。

そのため、list.sort() を呼び出すと UnsupportedOperationException が発生します。

不変リストをソートしたい場合は、一度ArrayListなどの可変リストにコピーするか、前述のStream APIを使用して新しいリストを生成してください。

2. ソートアルゴリズムの安定性

JavaのList.sort()(およびCollections.sort())は、安定ソート(Stable Sort)であることが保証されています。

これは、ソートの基準となる値が同じ要素同士の、元の前後関係が維持されることを意味します。

複数回のソートを組み合わせて複雑な順序を実現する際に、この性質は非常に重要となります。

3. 計算量

Javaの標準的なソートアルゴリズムには、TimSort(マージソートと挿入ソートのハイブリッド)が採用されています。

計算量は平均および最悪時ともに O(n log n) です。

非常に巨大なリスト(数百万件以上)を頻繁にソートする場合は、リストのデータ構造(ArrayList vs LinkedList)や、並列ストリーム(parallelStream())の利用を検討する余地がありますが、通常は標準のメソッドで十分に高速です。

実践的な使い分けガイド

どの手法を使うべきか迷った際は、以下の基準を参考にしてください。

元のリストを書き換えても良い場合

Java 8以降であれば list.sort(Comparator) を第一選択にします。

記述が簡潔で、最も標準的な手法です。

元のリストを保持したい、または加工のついでにソートしたい場合

stream().sorted().toList() を使用します。

フィルタリングやマッピングといった処理が続く場合は、Stream API一択です。

レガシーコードの保守や古いJava環境の場合

Collections.sort(list) を使用します。

また、複雑なソート条件がある場合は、Comparatorを別メソッドや定数として定義しておくと、コードの再利用性とテストのしやすさが向上します。

まとめ

JavaにおけるListのソートは、言語の進化とともに非常に洗練されてきました。

  • 基本List.sort()Comparator.comparing() を組み合わせる。
  • 不変性を重視するなら Stream API の sorted() を利用する。
  • 安全性のために nullsFirst / nullsLast を適切に使い分ける。

これらの手法をマスターすることで、データの並び替えに関する実装で迷うことはなくなるでしょう。

プログラムの要件(破壊的か非破壊的か)を正確に把握し、読みやすくメンテナンスしやすいソート処理を記述することを心がけてください。

特にComparatorのメソッドチェーンを活用した宣言的な記述は、チーム開発においても意図が伝わりやすく、バグの混入を防ぐ強力な武器となります。