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つに大別されます。
Collections.sort()メソッド(従来の標準的な方法)List.sort()メソッド(Java 8以降の推奨される破壊的ソート)Stream APIのsorted()(非破壊的なソート)
それぞれの特徴を表にまとめると、以下のようになります。
| 手法 | 導入バージョン | 特徴 | 元のListへの影響 |
|---|---|---|---|
Collections.sort() | Java 1.2 | 古くからある汎用的な方法 | 破壊的(書き換わる) |
List.sort() | Java 8 | Listのデフォルトメソッドを使用 | 破壊的(書き換わる) |
Stream API | Java 8 | 関数型プログラミングのスタイル | 非破壊的(新しいListを生成) |
これらの手法を使い分けることで、効率的で読みやすいコードを記述することが可能になります。
Collections.sortによる基本的なソート
Collections.sort()は、Javaの初期から存在する最もポピュラーなソート手法です。
このメソッドは、引数として渡されたListをその場で書き換える(破壊的変更)ため、戻り値はありません。
数値や文字列の昇順ソート
ラッパークラス(Integer, Doubleなど)やStringクラスは、既にComparableインターフェースを実装しているため、特に追加の設定なしでソートが可能です。
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引数に渡します。
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以降のラムダ式を用いることで、簡潔に比較ロジックを記述できます。
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クラスがあるとします。
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()を使用するのが最適です。
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()を繋げるだけで実現できます。
// 年齢昇順、かつ同じ年齢なら名前の辞書順
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())で収集します。
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() を使用します。
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のメソッドチェーンを活用した宣言的な記述は、チーム開発においても意図が伝わりやすく、バグの混入を防ぐ強力な武器となります。






