![]()
【Java入門 実用編】Java Stream API を利用したList のソート | コレクションクラスの活用
2026.01.24
前回の記事では、Collections.sort や List.sort を利用した、Java の List のソート方法について説明しました。当記事では、処理を直観的な内容で記述することが可能な「Java Stream API」を利用した List のソート方法について説明します。
Java Stream API とは
Java Stream API は、Java 8 から導入された、List などのデータの集合を効率的に操作するための仕組みです。
通常の for 文では List からデータを1個ずつ取り出して処理するのに対し、Stream API では List から順に送り出されたデータを、設定した処理に流しこんでいく形をイメージすると良いでしょう。このStream API を利用することで、以下のようなメリットが生まれます。
・処理の流れが見えやすい
「ソートして」「フィルターをかけて」「データを集約する」といった手順が、そのまま連結されたコードとして表現されるため、直感的に読めるようになります。
・元のデータを汚さない(非破壊)
List に対して処理するのではなく、List の「中身」を対象に処理するため、データの保管場所にあたる List には影響を与えません。
・複雑なソートもシンプルに
特にソートは直観的、かつ非常に簡潔に記述できます。
Java Stream API を利用した List のソート
Collections.sort や List.sort との違い
Java でリストをソートする方法はいくつかありますが、従来の Collections.sort(または List.sort)と、Stream API の sorted メソッドには、決定的な違いがあります。
それは、「元のリストの中身を書き換えるかどうか」です。
従来のメソッド:破壊的ソート
Collections.sort(list) や list.sort(...) を実行すると、元のリストそのものの並び順が変更されます。これを「破壊的変更」と呼びます。 一度実行すると、元の並び順には戻せません。
// 従来のやり方
List<Integer> numbers = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(numbers);
// 元の numbers の中身が [1, 2, 3] に書き換わっている!
System.out.println(numbers);Stream API:非破壊的ソート
一方で、Stream API の stream().sorted() で実行される処理では、元のリストには一切触れません。 ソートされた結果は collect メソッドで新しいリストとして受け取る形になります。これは「非破壊的」なソートと呼ばれます。
// Stream API のやり方
List<Integer> numbers = new ArrayList<>(Arrays.asList(3, 1, 2));
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
// sortedNumbers は [1, 2, 3] になっているが...
// 元の numbers は [3, 1, 2] のまま!
System.out.println(numbers);どちらの方法でソートすべきか
現代の Java 開発では、バグを防ぐために「元のデータを勝手に変えない(不変性)」ことが重視されるため、Stream API を使うケースが増えています。
ただし、元の List のデータの並びを「変える必要がある」場合や、データの数が非常に多く、メモリの使用量を節約したい場合など、従来の sort メソッドを利用したほうが良いケースもあります。
| 特徴 | 従来の sort メソッド | Stream API (sorted) |
| 元のリスト | 変更される (破壊的) | 変更されない (非破壊的) |
| 戻り値 | なし (void) | 新しいストリーム / リスト |
| 使い道 | メモリを節約したい時、単純に並べ替えたい時 | 元のデータを残したい時、複雑な条件で抽出・加工したい時 |
イミュータブルなリストの要素を扱う
Java 9 以降、List.of("A", "B", "C") のようにして、簡単にリストを作成できるようになりました。 しかし、こうして作ったリストは「イミュータブル(変更不可)」という特性を持ちます。つまり、中身を追加・削除したり、並び替えたりすることが一切できません。
従来の sort メソッドは使えない
もし、この「変更不可リスト」に対して、従来の Collections.sort を使うとどうなるでしょうか?
// 変更不可のリストを作成
List<String> fruits = List.of("Banana", "Apple", "Orange");
// 従来のソートを実行しようとすると...
Collections.sort(fruits);
// 実行時エラー! (UnsupportedOperationException)
「並び替え」はリストの中身を変更する行為なので、変更不可のリストに対して行うと「その操作は許可されていません」というエラーが発生してしまいます。
Stream API を利用しよう
ここで Stream API の出番です。 Stream API は元のリストを変更せず、並び替えた結果を新しいリストとして生成するため、元のリストがイミュータブルであっても問題なくソートできます。
import java.util.List;
import java.util.stream.Collectors;
public class StreamSortImmutable {
public static void main(String[] args) {
// 変更不可のリスト
List<String> fruits = List.of("Banana", "Apple", "Orange");
// Stream API でソート
List<String> sortedFruits = fruits.stream() // ベルトコンベアに流す
.sorted() // 並び替える
.collect(Collectors.toList()); // 新しい箱に入れる
System.out.println("元のリスト: " + fruits); // [Banana, Apple, Orange]
System.out.println("新しいリスト: " + sortedFruits); // [Apple, Banana, Orange]
}
}自然順序でのソート
Stream API でソートを行うメソッドは sorted メソッドです。 引数を指定しない場合は昇順でのソートとなり、引数に Comparator を渡した場合はソート順序を指定することができます。
まず、単純な数値の List で、昇順、降順それぞれの順序でのソート処理と、結果を確認してみましょう。
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class StreamNaturalSort {
public static void main(String[] args) {
List<Integer> numbers = List.of(3, 1, 5, 2, 4);
// 1. 昇順(小さい順)
List<Integer> ascList = numbers.stream()
.sorted() // 引数なし
.collect(Collectors.toList());
// 2. 降順(大きい順)
List<Integer> descList = numbers.stream()
.sorted(Comparator.reverseOrder()) // 逆順のルールを指定
.collect(Collectors.toList());
System.out.println("元のリスト: " + numbers); // [3, 1, 5, 2, 4]
System.out.println("昇順: " + ascList); // [1, 2, 3, 4, 5]
System.out.println("降順: " + descList); // [5, 4, 3, 2, 1]
}
}
実行結果:
元のリスト: [3, 1, 5, 2, 4]
昇順: [1, 2, 3, 4, 5]
降順: [5, 4, 3, 2, 1]Comparator やラムダ式でソート条件を指定
数値(1, 2, 3)であれば、Java はどちらが大きいかを知っています。 しかし、自分で作ったクラスの場合、Java は「何をもって順序を決めるのか」は分かりません。このため、Stream API のメソッドチェーンの中で、具体的な並び替えのルールを指定する必要がある場合があります。
Comparator.comparing の利用
比較的単純なソート順の指定であれば、Comparator クラスの comparing メソッドを使うことで「オブジェクトの項目と比較内容」について指示することができます。
例として、以下の User クラスの List をソートするケースについて考えてみます。
public class User {
private String name;
private int age;
private boolean isVip; // VIP会員フラグ
public User(String name, int age, boolean isVip) {
this.name = name;
this.age = age;
this.isVip = isVip;
}
// Getter
public String getName() { return name; }
public int getAge() { return age; }
public boolean isVip() { return isVip; }
@Override
public String toString() {
// 出力時にVIPかどうかもわかるようにする
return (isVip ? "[VIP]" : " ") + name + "(" + age + ")";
}
}
この User クラスのリストを、User クラスの年齢(age)の順番に並べたい場合は、Collections.sort などと同じように Comparator.comparing メソッドを利用して、メンバーの getter メソッドで取得した値を比較してソートすることができます。
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class StreamObjectSort {
public static void main(String[] args) {
List<User> users = List.of(
new User("Tanaka", 45, false),
new User("Suzuki", 20, false),
new User("Sato", 30, true)
);
// 年齢(Age)で昇順ソート
List<User> sortedUsers = users.stream()
// 「UserクラスのgetAgeメソッドの値」を使って比べる
.sorted(Comparator.comparing(User::getAge))
.collect(Collectors.toList());
System.out.println("--- 年齢順(昇順) ---");
sortedUsers.forEach(System.out::println);
}
}ラムダ式を利用して条件を指定
もう少し複雑な条件の場合はどうでしょうか。例えば、複数のメンバー変数によってソート順が決定するようなパターンが考えられます。こういった場合は、(a, b) -> { ... } という形式のラムダ式で、比較条件を詳細に指定することができます。
ラムダ式での比較を行う場合は、比較結果を整数で返却する必要があります。2つの引数を ( a, b ) の順に指定してラムダ式を記述した場合は、比較結果を以下の通り返します。
・マイナスの値 (-1など): a の方が小さい(a を先に並べる)
・プラスの値 (1など): a の方が大きい(b を先に並べる)
・0: 等しい(順序はどちらでもよい)
先ほどの User クラスの List を、「VIP会員を常に優先とし、VIP会員同士(またはVIP以外同士)の場合は年齢順に並べる」という例を見てみましょう。
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class User {
private String name;
private int age;
private boolean isVip; // VIP会員フラグ
public User(String name, int age, boolean isVip) {
this.name = name;
this.age = age;
this.isVip = isVip;
}
// Getter
public String getName() { return name; }
public int getAge() { return age; }
public boolean isVip() { return isVip; }
@Override
public String toString() {
// 出力時にVIPかどうかもわかるようにする
return (isVip ? "[VIP]" : " ") + name + "(" + age + ")";
}
}
public class StreamComplexSort {
public static void main(String[] args) {
List<User> users = List.of(
new User("Tanaka", 45, false), // 一般
new User("Suzuki", 20, false), // 一般
new User("Sato", 30, true) // VIP
);
List<User> sortedUsers = users.stream()
.sorted((u1, u2) -> {
// --- ここに独自の比較ロジックを書く ---
// 1. VIP判定:もし u1 だけが VIP なら、u1 が先(マイナスを返す)
if (u1.isVip() && !u2.isVip()) {
return -1;
}
// 2. VIP判定:もし u2 だけが VIP なら、u2 が先(プラスを返す)
if (!u1.isVip() && u2.isVip()) {
return 1;
}
// 3. どちらも同じ(両方VIP または 両方一般)なら、年齢で比較
// Integer.compare(x, y) は、x<yなら-1, x>yなら1を返す
return Integer.compare(u1.getAge(), u2.getAge());
})
.collect(Collectors.toList());
System.out.println("--- VIP優先・年齢順ソート ---");
sortedUsers.forEach(System.out::println);
}
}
実行結果:
--- VIP優先・年齢順ソート ---
[VIP]Sato(30)
Suzuki(20)
Tanaka(45)
VIPである Sato さんが最優先され、残りの二人は年齢が若い順(20歳 → 45歳)に並んでいることがわかります。このように、ラムダ式を利用することで、直観的で柔軟なソートが可能になります。
いかがでしたでしょうか。Stream API を利用すると、ソート以外にも「特定の条件に合致したものだけを抽出」といった処理を、ソートなど他の処理と同時に行うことも可能です。このように Java コレクションを活用する方法を今後も発信していきますので、皆さんのプログラミングにも是非生かしてください。



