Java入門 / Javaの構文

【Java入門 実用編】Java Stream API を利用したList のソート | コレクションクラスの活用

2026.01.24

株式会社GSI ITエンジニア募集【成長をカタチに】


前回の記事では、Collections.sort や List.sort を利用した、Java の List のソート方法について説明しました。当記事では、処理を直観的な内容で記述することが可能な「Java Stream API」を利用した List のソート方法について説明します。

◆Java入門 記事一覧はこちら

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 コレクションを活用する方法を今後も発信していきますので、皆さんのプログラミングにも是非生かしてください。

株式会社GSI 採用サイト

新しいこと、始めよう

あなたとともに歩を進めるWEBメディア