Java Listをソートする方法まとめ|Comparator・ラムダ式・Stream活用
生徒
「JavaのListに入っているデータを、数字の小さい順や名前の順に並べ替えたいのですが、どうすればいいですか?」
先生
「Javaでは、Collections.sortメソッドや、Listインターフェースのsortメソッドを使って簡単に並べ替えることができますよ。」
生徒
「初心者でも簡単にできる方法はありますか?最近よく聞くラムダ式やStreamというのも気になります!」
先生
「もちろんです。基本から最新の書き方まで、順番に学んでいきましょう。実はとてもシンプルに書けるんですよ!」
1. JavaのListソートの基本概念を知ろう
Javaのプログラミングにおいて、データの集合を扱う「List」は非常によく使われます。例えば、ショッピングサイトの商品一覧を価格の安い順に表示したり、テストの結果を点数の高い順に並べ替えたりする場面です。これを「ソート(並べ替え)」と呼びます。
JavaでListをソートする場合、大きく分けて「自然順序付け」と「Comparator(比較器)による順序付け」の二種類があります。自然順序付けとは、数値なら1, 2, 3...、文字列なら五十音順やアルファベット順といった、データそのものが持っている標準的な順番のことです。一方で、Comparatorを使うと「名前の長さ順」や「特定のオブジェクトの特定の属性順」など、自由なルールで並べ替えることが可能になります。
初心者のうちは、まず標準的なライブラリに備わっているメソッドを使いこなすことから始めましょう。Javaのバージョンが進むにつれて、ソートの記述方法はどんどん短く、読みやすくなっています。以前は数行必要だった処理が、今ではたった一行で書けるようになっているのです。
2. Collections.sortを使った最もスタンダードな方法
Javaの初期から使われている最も一般的な方法が、java.util.Collectionsクラスのsortメソッドを使用する方法です。このメソッドは、引数に渡したListを破壊的に(元のリストの中身を直接)書き換えてソートします。
以下のコード例では、整数のリストを昇順(小さい順)に並べ替える基本的な書き方を示します。インポート文が必要になる点に注意しましょう。
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(50);
numbers.add(10);
numbers.add(30);
System.out.println("ソート前: " + numbers);
// Collections.sortを使って昇順にソート
Collections.sort(numbers);
System.out.println("ソート後: " + numbers);
}
}
ソート前: [50, 10, 30]
ソート後: [10, 30, 50]
この方法は非常にシンプルで分かりやすいのが特徴です。文字列のリスト(StringのList)に対しても同様に使うことができ、その場合は辞書順(あいうえお順やABC順)に並びます。もし降順(大きい順)にしたい場合は、Collections.reverseOrder()を第二引数に渡すことで実現できます。
3. List.sortメソッドとComparatorの活用
Java 8以降では、Listインターフェース自体にsortメソッドが追加されました。これにより、Collections.sort(list)と書く代わりに、list.sort(...)と直感的に記述できるようになりました。このメソッドを使う際は、どのように並べ替えるかを指定する「Comparator」を渡すのが一般的です。
Comparatorは「比較器」という意味で、2つの要素を比べてどちらを前にするかを決定する役割を持ちます。単純な昇順であればComparator.naturalOrder()、降順であればComparator.reverseOrder()を指定します。nullが含まれる可能性があるリストをソートする場合には、Comparator.nullsFirstといった便利なメソッドも用意されています。これにより、実行時にエラー(NullPointerException)が発生するのを防ぎつつ、安全に並べ替えを行うことができます。
4. ラムダ式を使ったスマートなソート記述
Java 8の目玉機能である「ラムダ式」を使うと、独自のソート条件を非常に簡潔に書くことができます。例えば、文字列のリストを「文字の長さが短い順」に並べ替えたい場合、以前は匿名クラスという複雑な書き方が必要でしたが、ラムダ式なら直感的に記述可能です。
ラムダ式は(引数) -> { 処理 }という形式で書きます。ソートにおいては、2つの要素(aとb)を比較するロジックをこの形式で流し込みます。では、実際に文字列の長さを基準にしたソートのコードを見てみましょう。
import java.util.ArrayList;
import java.util.List;
public class LambdaSortExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Kiwi");
fruits.add("Orange");
// ラムダ式を使って文字列の長さ順にソート
fruits.sort((a, b) -> a.length() - b.length());
System.out.println("長さ順にソート: " + fruits);
}
}
長さ順にソート: [Kiwi, Apple, Banana, Orange]
このコードでは、a.length() - b.length()の結果が負であればaが前、正であればbが前というルールに基づいて動いています。このように、特定のルールに基づいた並べ替えをたった一行で表現できるのがラムダ式の強みです。初心者の方も、この書き方に慣れるとJavaプログラミングがぐっと楽しくなるはずです。
5. Stream APIを使った非破壊的なソート
これまでに紹介した方法は、元のリストそのものを並べ替えてしまう「破壊的」な手法でした。しかし、実務では「元のリストはそのまま残しておき、新しく並べ替えたリストを作成したい」という場面が多くあります。そこで役立つのが「Stream API」です。
Stream APIのsorted()メソッドを使用すると、元のデータを変更せずに、ソート済みの新しいストリームを生成できます。最後にcollect(Collectors.toList())を使ってリスト形式に戻すのが一般的な流れです。この手法は、元のデータを壊したくない関数型プログラミング的なアプローチにおいて非常に重要です。また、フィルター(抽出)やマップ(変換)といった他の処理と組み合わせて使うことができるため、複雑なデータ加工も一気に行うことができます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamSortExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("田中", "佐藤", "鈴木", "伊藤");
// Stream APIを使ってソートし、新しいリストを作成
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("元のリスト: " + names);
System.out.println("ソート後リスト: " + sortedNames);
}
}
元のリスト: [田中, 佐藤, 鈴木, 伊藤]
ソート後リスト: [伊藤, 佐藤, 鈴木, 田中]
6. 自作クラスのリストをソートする方法
実際の開発では、StringやIntegerだけでなく、自分で作成したクラス(例えば「Employee」や「Product」クラス)のリストをソートすることがほとんどです。このような場合、どのフィールドを基準にソートするかをコンピュータに教えてあげる必要があります。
Comparator.comparingメソッドを使うと、自作クラスの特定のフィールドをキーにしたソートが驚くほど簡単に実装できます。例えば、社員クラスに「ID」と「名前」がある場合、IDでソートしたり名前でソートしたりといった切り替えがスムーズに行えます。以下の例では、商品の価格をもとにソートする処理を記述しています。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return name + ":" + price + "円";
}
}
public class ObjectSortExample {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("ノートPC", 120000));
products.add(new Product("マウス", 2500));
products.add(new Product("キーボード", 8000));
// 価格の安い順にソート
products.sort(Comparator.comparing(p -> p.price));
for (Product p : products) {
System.out.println(p);
}
}
}
マウス:2500円
キーボード:8000円
ノートPC:120000円
7. 複数の条件を組み合わせた高度なソート
「まずは点数が高い順に並べ、同じ点数の人がいたら名前順に並べたい」といった複数の条件を組み合わせたいこともあります。JavaのComparatorには、こうした複雑な要望に応えるためのthenComparingという便利なメソッドが用意されています。
これを使えば、第一条件を指定した後に、ドットで繋いで第二条件、第三条件と追加していくことができます。コードの可読性も高く、後から条件を追加したり変更したりするのも容易です。実務のアプリケーション開発では、ユーザーの使い勝手を向上させるために、こうした細かいソート順の制御が求められることが多いため、ぜひ覚えておきたいテクニックです。また、逆順にしたい場合はreversed()を組み合わせることで、柔軟な並び替えルールを構築できます。
8. ソート時の注意点とパフォーマンス
Listのソートを行う際に気をつけなければならないのが、変更不可能なリスト(Unmodifiable List)に対する操作です。List.of()やCollections.unmodifiableList()で作成されたリストに対してsort()メソッドを呼び出すと、実行時にエラー(UnsupportedOperationException)が発生します。ソートを行う必要がある場合は、必ずnew ArrayList<>(originalList)のようにして、中身を変更可能な新しいリストにコピーしてから行うようにしましょう。
また、非常に大量のデータをソートする場合のパフォーマンスについても少し触れておきます。Javaの標準ソートアルゴリズムは非常に効率的(TimSortなどが採用されています)ですが、それでも要素数が数百万件を超えるような場合は、処理時間が無視できなくなります。そのような場合は、Stream APIのparallelStream()を利用した並列ソートを検討することもあります。しかし、並列処理はオーバーヘッドもあるため、基本的には通常のソートで十分なケースがほとんどです。まずは正しい書き方をマスターし、必要に応じて最適化を考えていくのが良いでしょう。
9. 実践的なソートの使い分けガイド
最後に、どの方法をいつ使うべきかの指針を整理しましょう。最もシンプルな数値や文字列の破壊的ソートであればCollections.sort()やList.sort(null)で十分です。独自のルールを適用したい場合は、ラムダ式を用いたList.sort((a, b) -> ...)が適しています。一方、元のデータを保持したまま加工後のリストを得たい場合や、他のデータ処理とチェーンさせたい場合はStream APIのsorted()一択となるでしょう。
初心者の方は、まず自分が今扱っているリストを「変えてもいいのか(破壊的)」「変えてはいけないのか(非破壊的)」を意識することから始めてみてください。それが決まれば、自ずと使うべきメソッドが見えてくるはずです。Javaは非常に型に厳格で安全な言語ですが、ソート周りのライブラリは驚くほど柔軟に作られています。色々なパターンを試して、自在にデータを操れるようになりましょう。ここまで学んだ内容を実際のコードに取り入れることで、あなたのプログラムの品質は一段と向上するはずです。