Stream の sorted とは、ストリームの要素を並べ替えた新しいストリームを返す中間操作です。 引数なしの sorted() は自然な順序(数値なら小さい順、文字列なら文字コード順)で並べ、sorted(Comparator) は渡した比較ルールで並べます。元のリストは書き換えず、並べ替えた結果を toList() などで受け取って使います。

この記事では、昇順・降順・複数条件・一部だけ降順・null対応・大文字小文字無視・文字列の数値順を、コピペできる1行と実行結果の早見表にまとめました。あわせて、ClassCastException、ラムダ式に reversed を付けたときのコンパイルエラー、reversed の位置で結果が変わる例を実際に再現し、直し方を並べています。後半では sorted と limit・skip を組み合わせた上位N件の取得とページング、書く順序で結果が変わる例まで扱います。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(java ファイル名.java の単一ファイル実行)。掲載した出力とエラー文は、すべてこの環境で実行して得たものです。

Stream sortedの早見表

早見表では、次のデータを使っています。Item は record Item(String name, String category, Integer price, int stock) です。

変数 中身
nums 30, 5, 12, 8
words banana, Cherry, apple, Date
numStr "10", "9", "100", "2"(数字の文字列)
withNull "b", null, "a"
items ノート(文具,300) / ペン(文具,120) / マウス(機器,1800) / ケーブル(機器,600) / 付箋(文具,null)
priced items から価格が null の付箋を除いた4件
やりたいこと sorted に書く1行 実行結果
昇順 nums.stream().sorted() 5, 8, 12, 30
降順 nums.stream().sorted(Comparator.reverseOrder()) 30, 12, 8, 5
項目で昇順 sorted(Comparator.comparing(Item::price)) ペン, ノート, ケーブル, マウス
項目で降順 sorted(Comparator.comparing(Item::price).reversed()) マウス, ケーブル, ノート, ペン
int の項目で昇順 sorted(Comparator.comparingInt(Item::stock)) ケーブル, マウス, 付箋, ノート, ペン
複数条件(分類→価格) sorted(Comparator.comparing(Item::category).thenComparing(Item::price)) ペン, ノート, ケーブル, マウス
一部だけ降順(分類は昇順、価格は降順) sorted(Comparator.comparing(Item::category).thenComparing(Item::price, Comparator.reverseOrder())) ノート, ペン, マウス, ケーブル
null の要素を最後へ sorted(Comparator.nullsLast(Comparator.naturalOrder())) a, b, null
null の要素を最初へ sorted(Comparator.nullsFirst(Comparator.naturalOrder())) null, a, b
null の項目を最後へ sorted(Comparator.comparing(Item::price, Comparator.nullsLast(Comparator.naturalOrder()))) ペン, ノート, ケーブル, マウス, 付箋
文字列をそのまま words.stream().sorted() Cherry, Date, apple, banana
大文字小文字を無視 words.stream().sorted(String.CASE_INSENSITIVE_ORDER) apple, banana, Cherry, Date
数字の文字列をそのまま numStr.stream().sorted() 10, 100, 2, 9
数字の文字列を数値順 sorted(Comparator.comparingInt(Integer::parseInt)) 2, 9, 10, 100
文字数の短い順 sorted(Comparator.comparingInt(String::length)) Date, apple, banana, Cherry
文字数→辞書順 sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder())) Date, apple, Cherry, banana
上位3件 sorted(降順のComparator).limit(3) 並べ替えた先頭3件
2ページ目(1ページ3件) sorted(...).skip(3).limit(3) 4〜6件目

「分類→価格」がペンから始まるのは、文字コード順で「文具」が「機器」より前に来るためです。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Stream sortedの使い方 降順・複数条件・null対応」

早見表を確かめたコード(そのまま動く完成形)

早見表の結果は、次の1ファイルを実行して得ました。SortCheat.java という名前で保存し、java SortCheat.java で動きます。

import java.util.*;
import java.util.stream.*;

public class SortCheat {
    record Item(String name, String category, Integer price, int stock) {}

    public static void main(String[] args) {
        List<Integer> nums = List.of(30, 5, 12, 8);
        List<String> words = List.of("banana", "Cherry", "apple", "Date");
        List<String> numStr = List.of("10", "9", "100", "2");
        List<String> withNull = Arrays.asList("b", null, "a");
        List<Item> items = List.of(
            new Item("ノート", "文具", 300, 12),
            new Item("ペン", "文具", 120, 40),
            new Item("マウス", "機器", 1800, 5),
            new Item("ケーブル", "機器", 600, 0),
            new Item("付箋", "文具", null, 7));
        List<Item> priced = items.stream().filter(i -> i.price() != null).toList();

        System.out.println("1 昇順        : " + nums.stream().sorted().toList());
        System.out.println("2 降順        : " + nums.stream().sorted(Comparator.reverseOrder()).toList());
        System.out.println("3 項目で昇順  : " + names(priced.stream().sorted(Comparator.comparing(Item::price))));
        System.out.println("4 項目で降順  : " + names(priced.stream().sorted(Comparator.comparing(Item::price).reversed())));
        System.out.println("5 int項目     : " + names(items.stream().sorted(Comparator.comparingInt(Item::stock))));
        System.out.println("6 複数条件    : " + names(priced.stream().sorted(Comparator.comparing(Item::category).thenComparing(Item::price))));
        System.out.println("7 一部だけ降順: " + names(priced.stream().sorted(Comparator.comparing(Item::category).thenComparing(Item::price, Comparator.reverseOrder()))));
        System.out.println("8 null要素を後: " + withNull.stream().sorted(Comparator.nullsLast(Comparator.naturalOrder())).toList());
        System.out.println("9 null要素を先: " + withNull.stream().sorted(Comparator.nullsFirst(Comparator.naturalOrder())).toList());
        System.out.println("10 null項目を後: " + names(items.stream().sorted(Comparator.comparing(Item::price, Comparator.nullsLast(Comparator.naturalOrder())))));
        System.out.println("11 そのまま    : " + words.stream().sorted().toList());
        System.out.println("12 大小無視    : " + words.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList());
        System.out.println("13 文字列のまま: " + numStr.stream().sorted().toList());
        System.out.println("14 数値順      : " + numStr.stream().sorted(Comparator.comparingInt(Integer::parseInt)).toList());
        System.out.println("15 文字数順    : " + words.stream().sorted(Comparator.comparingInt(String::length)).toList());
        System.out.println("16 文字数→辞書: " + words.stream().sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder())).toList());
    }

    static List<String> names(Stream<Item> s) {
        return s.map(i -> i.name() + "(" + i.price() + ")").toList();
    }
}

出力は早見表の「実行結果」列のとおりです。5番の「int項目」は在庫数(stock)の昇順で、ケーブルが0、マウスが5、付箋が7、ノートが12、ペンが40の順に並びます。

sortedの基本:昇順と降順、元のリストは変わらない

sorted には2つの形があります。

書き方 並び順 使う場面
sorted() 自然な順序(要素が Comparable を実装している必要がある) Integer・String・LocalDate など
sorted(Comparator) 渡した Comparator の順 降順、自作クラス、複数条件、null対応
import java.util.*;

public class Basic {
    public static void main(String[] args) {
        List<Integer> scores = new ArrayList<>(List.of(72, 95, 58, 81));

        List<Integer> asc = scores.stream().sorted().toList();
        List<Integer> desc = scores.stream().sorted(Comparator.reverseOrder()).toList();

        System.out.println("昇順: " + asc);
        System.out.println("降順: " + desc);
        System.out.println("元  : " + scores);

        scores.sort(Comparator.naturalOrder());
        System.out.println("List.sort後の元: " + scores);
    }
}

実行結果:

昇順: [58, 72, 81, 95]
降順: [95, 81, 72, 58]
元  : [72, 95, 58, 81]
List.sort後の元: [58, 72, 81, 95]

sorted を通しても、元のリスト scores の並びは 72, 95, 58, 81 のままです。並べ替えた結果は新しいリストとして受け取ります。一方、最後の行の List.sort は元のリストそのものを並べ替えます。

方法 元のリスト 戻り値 向いている場面
list.stream().sorted(...).toList() 変わらない 並べ替え済みの新しい List 元の順序も残したい、filter や map と続けて書きたい
list.sort(...) 並べ替えられる なし(void) そのリスト自体を並べ替えたい

自作クラスを並べ替える:Comparator.comparing

自作クラスや record を並べ替えるときは、「どの項目で比べるか」を Comparator.comparing にメソッド参照で渡すのが基本です。

書き方 意味
Comparator.comparing(Item::price) price の昇順
Comparator.comparing(Item::price).reversed() price の降順
Comparator.comparing(Item::price, Comparator.reverseOrder()) price の降順(第2引数で指定)
Comparator.comparingInt(Item::stock) int の項目の昇順
(a, b) -> Integer.compare(a.stock(), b.stock()) ラムダ式で直接書く形

ラムダ式で比較を書くときは、引き算ではなく Integer.compare を使うと、大きな値でも正しく並びます。次の出力は、同じ4つの数(2000000000, -2000000000, 0, 5)を2通りの書き方で並べたものです。

a - b          : [2000000000, -2000000000, 0, 5]
Integer.compare: [-2000000000, 0, 5, 2000000000]

a - b は差が int の範囲を超えると符号が反転するため、並びが崩れます。比較は Integer.compare か Comparator.comparingInt に任せると安心です。

複数条件と「一部だけ降順」:thenComparing と reversed の位置

複数条件は、comparing(第1条件).thenComparing(第2条件) とつなげます。第1条件が同じ要素どうしだけ、第2条件で順番が決まります。

つまずきやすいのは降順の混ぜ方です。reversed() は「そこまでに組み立てた Comparator 全体」を逆にするので、付ける位置で結果が変わります。4通りを並べて実行しました。

import java.util.*;
import java.util.stream.*;

public class RevPos {
    record Item(String name, String category, int price) {}

    public static void main(String[] args) {
        List<Item> items = List.of(
            new Item("ノート", "B", 300), new Item("ペン", "B", 120),
            new Item("マウス", "A", 1800), new Item("ケーブル", "A", 600));

        show("A 末尾にreversed            ", items.stream().sorted(
            Comparator.comparing(Item::category).thenComparing(Item::price).reversed()));
        show("B 価格だけ降順(第2引数)     ", items.stream().sorted(
            Comparator.comparing(Item::category).thenComparing(Item::price, Comparator.reverseOrder())));
        show("C 価格だけ降順(入れ子)      ", items.stream().sorted(
            Comparator.comparing(Item::category).thenComparing(Comparator.comparing(Item::price).reversed())));
        show("D 分類だけ降順              ", items.stream().sorted(
            Comparator.comparing(Item::category).reversed().thenComparing(Item::price)));
    }

    static void show(String label, Stream<Item> s) {
        System.out.println(label + ": " + s.map(i -> i.category() + "-" + i.price()).collect(Collectors.joining(", ")));
    }
}

実行結果:

A 末尾にreversed            : B-300, B-120, A-1800, A-600
B 価格だけ降順(第2引数)     : A-1800, A-600, B-300, B-120
C 価格だけ降順(入れ子)      : A-1800, A-600, B-300, B-120
D 分類だけ降順              : B-120, B-300, A-600, A-1800
記号 書き方 分類 価格 解説
A comparing(分類).thenComparing(価格).reversed() 降順 降順 末尾の reversed が両方を逆にする
B comparing(分類).thenComparing(価格, Comparator.reverseOrder()) 昇順 降順 第2引数でその条件だけ降順にする
C comparing(分類).thenComparing(Comparator.comparing(価格).reversed()) 昇順 降順 降順の Comparator を入れ子で渡す
D comparing(分類).reversed().thenComparing(価格) 降順 昇順 reversed より後ろの条件は逆にならない

「分類は昇順、価格だけ降順」にしたくて A のように末尾へ reversed を付けると、分類まで降順になります。一部だけ降順にしたいときは B の形(thenComparing の第2引数に Comparator.reverseOrder())が、読みやすく取り違えにくい書き方です。

条件が3つ以上でも、thenComparing を続けて書けます。Comparator を変数に入れておくと sorted(order) と短く書け、同じ並び順を使い回せます。

エラーになる例と直し方1:ClassCastException

sorted() を引数なしで呼ぶと、要素を Comparable として比べます。Comparable を実装していないクラスの要素を引数なしの sorted に通すと、実行時に ClassCastException で止まります。

import java.util.*;

public class Cce {
    static class Task {
        final String title; final int priority;
        Task(String title, int priority) { this.title = title; this.priority = priority; }
        @Override public String toString() { return title + ":" + priority; }
    }

    public static void main(String[] args) {
        List<Task> tasks = List.of(new Task("資料作成", 2), new Task("見積確認", 1), new Task("会議準備", 3));
        System.out.println(tasks.stream().sorted().toList());
    }
}

実行結果(メッセージ後半のクラスローダー情報と、3行目以降のスタックトレースは省略):

Exception in thread "main" java.lang.ClassCastException: class Cce$Task cannot be cast to class java.lang.Comparable
    at java.base/java.util.Comparators$NaturalOrderComparator.compare(Comparators.java:47)

コンパイルは通り、終端操作(ここでは toList)を実行した時点で例外になるのがポイントです。公式ドキュメントにも、要素が Comparable でない場合は終端操作の実行時に ClassCastException が投げられることがある、と書かれています。なお、同じクラスでも要素が0件や1件のときは比較が行われないため、この環境では例外にならずそのまま結果が返りました。テストデータが1件だけだと気づきにくいので、2件以上で確かめるのがおすすめです。

直し方は2つあります。

import java.util.*;

public class CceFix {
    // 直し方2: Comparable を実装して「自然な順序」を持たせる
    record Task(String title, int priority) implements Comparable<Task> {
        @Override public int compareTo(Task o) { return Integer.compare(priority, o.priority); }
    }
    // Comparable を持たないクラス
    record Memo(String title, int priority) {}

    public static void main(String[] args) {
        List<Memo> memos = List.of(new Memo("資料作成", 2), new Memo("見積確認", 1), new Memo("会議準備", 3));
        // 直し方1: sorted に Comparator を渡す
        System.out.println(memos.stream().sorted(Comparator.comparingInt(Memo::priority)).toList());

        List<Task> tasks = List.of(new Task("資料作成", 2), new Task("見積確認", 1), new Task("会議準備", 3));
        System.out.println(tasks.stream().sorted().toList());
    }
}

実行結果:

[Memo[title=見積確認, priority=1], Memo[title=資料作成, priority=2], Memo[title=会議準備, priority=3]]
[Task[title=見積確認, priority=1], Task[title=資料作成, priority=2], Task[title=会議準備, priority=3]]

並び順が場面ごとに違うときやクラスを変更できないときは Comparator を渡す方法、そのクラスに「いつもこの順」という自然な順序があるときは Comparable の実装が向いています。

エラーになる例と直し方2:ラムダ式に reversed を付けたとき

降順にしようとして、ラムダ式で書いた comparing に reversed() を続けると、コンパイルエラーになります。

import java.util.*;

public class RevErr {
    record Item(String name, int price) {}

    public static void main(String[] args) {
        List<Item> items = List.of(new Item("ノート", 300), new Item("ペン", 120), new Item("マウス", 1800));
        System.out.println(items.stream()
            .sorted(Comparator.comparing(i -> i.price()).reversed())
            .toList());
    }
}

javac RevErr.java の結果:

RevErr.java:9: error: cannot find symbol
            .sorted(Comparator.comparing(i -> i.price()).reversed())
                                               ^
  symbol:   method price()
  location: variable i of type Object
1 error

i が Object 型として扱われ、price() が見つからないというエラーです。comparing(i -> i.price()) だけなら、sorted が求める型からラムダ式の引数の型が推論されます。ところが 後ろに .reversed() を続けると、comparing の時点では型の手がかりがなくなり、引数が Object と推論されます。

もう1つ、ラムダ式そのものに reversed() を付けた場合のエラーも再現しました。

            .sorted(((a, b) -> a.price() - b.price()).reversed())
RevErr2.java:9: error: lambda expression not expected here
            .sorted(((a, b) -> a.price() - b.price()).reversed())
                     ^
1 error

ラムダ式は、代入先や引数の型が決まって初めて Comparator になるので、ラムダ式に直接メソッドを続けることはできません。

直し方は5通りあり、どれも同じ結果になります。

import java.util.*;

public class RevFix {
    record Item(String name, int price) {}

    public static void main(String[] args) {
        List<Item> items = List.of(new Item("ノート", 300), new Item("ペン", 120), new Item("マウス", 1800));

        // 1. メソッド参照にする(いちばん短い)
        System.out.println(items.stream().sorted(Comparator.comparing(Item::price).reversed()).toList());
        // 2. ラムダの引数に型を書く
        System.out.println(items.stream().sorted(Comparator.comparing((Item i) -> i.price()).reversed()).toList());
        // 3. comparing に型引数を付ける
        System.out.println(items.stream().sorted(Comparator.<Item, Integer>comparing(i -> i.price()).reversed()).toList());
        // 4. reversed を使わず、第2引数で降順を指定する
        System.out.println(items.stream().sorted(Comparator.comparing(i -> i.price(), Comparator.reverseOrder())).toList());
        // 5. ラムダ式を降順用に書く
        System.out.println(items.stream().sorted((a, b) -> Integer.compare(b.price(), a.price())).toList());
    }
}

実行結果(5行とも次の同じ出力でした。1行だけ掲載します):

[Item[name=マウス, price=1800], Item[name=ノート, price=300], Item[name=ペン, price=120]]
番号 直し方 ひとこと
1 メソッド参照 Item::price にする いちばん短く、まず選びたい形
2 ラムダ式の引数に型を書く (Item i) -> i.price() 計算した値で比べたいときに便利
3 Comparator.<Item, Integer>comparing(...) と型引数を書く 型を明示したいとき
4 reversed を使わず第2引数に Comparator.reverseOrder() メソッドを後ろにつなげないので推論が効く
5 降順のラムダ式 (a, b) -> Integer.compare(b.price(), a.price()) a と b を入れ替えて降順にする

null を含むデータを並べ替える:nullsFirst と nullsLast

null が混ざったリストを引数なしの sorted に通すと、NullPointerException で止まります。

import java.util.*;

public class NullErr {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("sato", null, "abe");
        System.out.println(names.stream().sorted().toList());
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "c1" is null

null をどこに置くかを Comparator.nullsFirst / nullsLast で決めておけば、そのまま並べ替えられます。 「要素そのものが null」の場合と「比べる項目が null」の場合で、書く位置が変わります。

import java.util.*;

public class NullFix {
    record Member(String name, Integer age) {}

    public static void main(String[] args) {
        List<String> names = Arrays.asList("sato", null, "abe");
        System.out.println("null要素を最後: " + names.stream().sorted(Comparator.nullsLast(Comparator.naturalOrder())).toList());
        System.out.println("null要素を最初: " + names.stream().sorted(Comparator.nullsFirst(Comparator.naturalOrder())).toList());
        System.out.println("降順でnullは最後: " + names.stream().sorted(Comparator.nullsLast(Comparator.reverseOrder())).toList());
        System.out.println("nullを除いて並べる: " + names.stream().filter(Objects::nonNull).sorted().toList());

        List<Member> members = List.of(new Member("佐藤", 34), new Member("阿部", null), new Member("井上", 28));
        System.out.println("項目がnull(最後): " + members.stream()
            .sorted(Comparator.comparing(Member::age, Comparator.nullsLast(Comparator.naturalOrder()))).toList());
        System.out.println("項目がnull(降順・最後): " + members.stream()
            .sorted(Comparator.comparing(Member::age, Comparator.nullsLast(Comparator.reverseOrder()))).toList());
        try {
            System.out.println(members.stream().sorted(Comparator.comparing(Member::age)).toList());
        } catch (NullPointerException e) {
            System.out.println("nullsLastなし: " + e);
        }
    }
}

実行結果:

null要素を最後: [abe, sato, null]
null要素を最初: [null, abe, sato]
降順でnullは最後: [sato, abe, null]
nullを除いて並べる: [abe, sato]
項目がnull(最後): [Member[name=井上, age=28], Member[name=佐藤, age=34], Member[name=阿部, age=null]]
項目がnull(降順・最後): [Member[name=佐藤, age=34], Member[name=井上, age=28], Member[name=阿部, age=null]]
nullsLastなし: java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because the return value of "java.util.function.Function.apply(Object)" is null

ポイントは2つです。

  • 項目が null のときは、comparing の第2引数に nullsLast(...) を渡します。 Comparator.nullsLast(Comparator.comparing(Member::age)) と外側を包む書き方は「要素(Member)が null」の場合の対応で、age が null の場合には効きません。
  • 「降順にしつつ null は最後」にしたいときは、nullsLast(Comparator.reverseOrder()) と中を降順にします。全体に reversed() を付けると null の位置も逆(最初)になります。

文字列の並べ替え:大文字小文字・数値順・日本語

文字列の自然な順序は文字コード順です。そのため、次の3点は Comparator を指定すると意図どおりに並びます。

やりたいこと そのままの結果 指定する Comparator 指定後の結果
大文字小文字を区別しない Cherry, Date, apple, banana String.CASE_INSENSITIVE_ORDER apple, banana, Cherry, Date
数字の文字列を数値順に 10, 100, 2, 9 Comparator.comparingInt(Integer::parseInt) 2, 9, 10, 100
日本語を辞書に近い順に apple, みかん, りんご, アップル, 林檎 Collator.getInstance(Locale.JAPANESE) apple, アップル, みかん, りんご, 林檎

大文字は小文字より文字コードが小さいので、そのまま並べると Cherry と Date が先に来ます。数字の文字列は先頭の文字から比べるため "100" が "2" より前になります。数値として比べたいときは Integer::parseInt を通します。

日本語は、ひらがな→カタカナ→漢字の順(文字コード順)に並びます。java.text.Collator は Comparator として sorted に渡せ、上の表のとおり、ひらがなとカタカナをまたいで並べられます。漢字の読みまでは判断しないので、読みの順に並べたいときは、ふりがなの項目を持たせてその項目で並べます。

sorted と limit・skip:上位N件とページング

並べ替えてから limit(n) を付けると上位N件、skip と limit を続けるとページングになります。

import java.util.*;

public class TopN {
    record Article(String title, int views) {}

    public static void main(String[] args) {
        List<Article> articles = List.of(
            new Article("A", 120), new Article("B", 980), new Article("C", 450),
            new Article("D", 30), new Article("E", 760), new Article("F", 510), new Article("G", 205));
        Comparator<Article> byViewsDesc = Comparator.comparingInt(Article::views).reversed();

        System.out.println("上位3件: " + articles.stream().sorted(byViewsDesc).limit(3).toList());
        System.out.println("下位3件: " + articles.stream().sorted(Comparator.comparingInt(Article::views)).limit(3).toList());

        int size = 3;
        for (int page = 1; page <= 3; page++) {
            List<String> titles = articles.stream()
                .sorted(byViewsDesc)
                .skip((long) (page - 1) * size)
                .limit(size)
                .map(Article::title)
                .toList();
            System.out.println(page + "ページ目: " + titles);
        }
        System.out.println("4ページ目: " + articles.stream().sorted(byViewsDesc).skip(9).limit(size).toList());

        System.out.println("limitが先 : " + articles.stream().limit(3).sorted(byViewsDesc).map(Article::title).toList());
        System.out.println("sortedが先: " + articles.stream().sorted(byViewsDesc).limit(3).map(Article::title).toList());
        System.out.println("limit→skip: " + articles.stream().sorted(byViewsDesc).limit(3).skip(3).map(Article::title).toList());
        System.out.println("skip→limit: " + articles.stream().sorted(byViewsDesc).skip(3).limit(3).map(Article::title).toList());

        System.out.println("最大1件: " + articles.stream().max(Comparator.comparingInt(Article::views)).orElseThrow());
        System.out.println("最小1件: " + articles.stream().min(Comparator.comparingInt(Article::views)).orElseThrow());
    }
}

実行結果:

上位3件: [Article[title=B, views=980], Article[title=E, views=760], Article[title=F, views=510]]
下位3件: [Article[title=D, views=30], Article[title=A, views=120], Article[title=G, views=205]]
1ページ目: [B, E, F]
2ページ目: [C, G, A]
3ページ目: [D]
4ページ目: []
limitが先 : [B, C, A]
sortedが先: [B, E, F]
limit→skip: []
skip→limit: [C, G, A]
最大1件: Article[title=B, views=980]
最小1件: Article[title=D, views=30]

ページングの式

1ページあたり size 件、page を1始まりとすると、次の形になります。

.sorted(並び順)
.skip((long) (page - 1) * size)
.limit(size)

データは7件なので、3ページ目は残りの1件(D)、4ページ目は空のリストになりました。skip は、要素数より大きい数を渡しても例外にならず、空のストリームを返します。

書く順序で結果が変わる

書いた順 結果 何が起きているか
sorted(降順).limit(3) B, E, F 全体を並べてから先頭3件を取る(閲覧数の上位3件)
limit(3).sorted(降順) B, C, A 元の並びの先頭3件(A, B, C)だけを取り出してから並べる
sorted(降順).skip(3).limit(3) C, G, A 4〜6件目(2ページ目)
sorted(降順).limit(3).skip(3) 空 3件に絞ったあとで3件飛ばすので残らない

「上位N件」は sorted → limit、「ページング」は sorted → skip → limit の順と覚えておくと取り違えません。条件で絞る filter は、sorted より前に書くと並べ替える件数が減ります。

1件だけ欲しいなら max・min

最大や最小の1件だけが欲しいときは、並べ替えずに max(Comparator) / min(Comparator) を使えます。戻り値は Optional なので、orElseThrow() や orElse(...) で取り出します。上の実行結果の最後の2行がその出力です。

結果の受け取り方:toList() と Collectors.toList() の違い

sorted は中間操作なので、終端操作で受け取るまで何も起きません。受け取り方は3通りあります。

import java.util.*;
import java.util.stream.*;

public class Receive {
    public static void main(String[] args) {
        List<Integer> source = new ArrayList<>(List.of(3, 1, 2));

        List<Integer> a = source.stream().sorted().toList();                        // Java 16以降
        List<Integer> b = source.stream().sorted().collect(Collectors.toList());    // Java 8以降
        List<Integer> c = source.stream().sorted().collect(Collectors.toCollection(ArrayList::new));

        System.out.println("元のリスト: " + source);
        System.out.println("toList()  : " + a + " " + a.getClass().getName());
        System.out.println("Collectors.toList(): " + b + " " + b.getClass().getName());

        b.add(99);
        c.add(99);
        System.out.println("追加できる: " + b + " / " + c);
        try {
            a.add(99);
        } catch (UnsupportedOperationException e) {
            System.out.println("toList()に追加: " + e);
        }

        source.stream().sorted();
        System.out.println("終端操作なし: " + source);
    }
}

実行結果:

元のリスト: [3, 1, 2]
toList()  : [1, 2, 3] java.util.ImmutableCollections$ListN
Collectors.toList(): [1, 2, 3] java.util.ArrayList
追加できる: [1, 2, 3, 99] / [1, 2, 3, 99]
toList()に追加: java.lang.UnsupportedOperationException
終端操作なし: [3, 1, 2]
受け取り方 使える版 返るリスト あとから追加・変更
.toList() Java 16以降 変更できないリスト できない(UnsupportedOperationException)
.collect(Collectors.toList()) Java 8以降 この環境では ArrayList この環境ではできた
.collect(Collectors.toCollection(ArrayList::new)) Java 8以降 ArrayList と明示 できる

並べ替えた結果を読むだけなら toList()、あとから要素を足すなら Collectors.toCollection(ArrayList::new) と使い分けます。最後の行「終端操作なし」は、source.stream().sorted(); だけを書いても元のリストに何も起きないことを示しています。

toList() が Java 16 からであることは、同じコードを Java 15 向けにコンパイルして確かめました。

$ javac --release 15 T15.java
T15.java:4: error: cannot find symbol
        List<Integer> a = List.of(3, 1, 2).stream().sorted().toList();
                                                            ^
  symbol:   method toList()
  location: interface Stream<Integer>
1 error

--release 16 ではエラーなくコンパイルできました。Java 8〜15 向けのコードでは collect(Collectors.toList()) を使います。

知っておくと役立つ sorted の性質

次の出力は、表に載せた書き方を1つのファイルにまとめて実行した結果です。

安定: [ペン, ノート, 電池, のり]
IntStream昇順: [5, 8, 12, 30]
IntStream降順: [30, 12, 8, 5]
値の降順: [ペン=40, ノート=12, 付箋=7, マウス=5]
キーの昇順: [ノート=12, ペン=40, マウス=5, 付箋=7]
LinkedHashMap: {ペン=40, ノート=12, 付箋=7, マウス=5}
a - b          : [2000000000, -2000000000, 0, 5]
Integer.compare: [-2000000000, 0, 5, 2000000000]
文字コード順: [apple, みかん, りんご, アップル, 林檎]
Collator    : [apple, アップル, みかん, りんご, 林檎]
distinct: [1, 2, 3]
  通過 3
  通過 1
  通過 2
  結果 1
性質 内容
安定ソート 価格が同じ300の3件は、元の並び(ノート→電池→のり)のまま。List のように順序のあるストリームでは、同じ値どうしの順序が保たれる
IntStream の降順 IntStream の sorted は引数なしの昇順のみ。降順は IntStream.of(arr).boxed().sorted(Comparator.reverseOrder()).mapToInt(Integer::intValue).toArray()
Map を値の降順に stock.entrySet().stream().sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
Map をキーの昇順に stock.entrySet().stream().sorted(Map.Entry.comparingByKey())
並べた順のまま Map に戻す collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (x, y) -> x, LinkedHashMap::new))
重複を除いて並べる Stream.of(3, 1, 3, 2, 1).distinct().sorted()
全要素を受け取ってから並べる Stream.of(3, 1, 2).peek(...).sorted().limit(1) の出力のとおり、3件すべてが sorted に届いてから結果が流れる。limit(1) を後ろに付けても、並べ替え自体は全件に対して行われる

comparingByValue().reversed() と続けるときは、上で扱った型推論の事情で Map.Entry.<String, Integer>comparingByValue() と型引数を書く必要があります。

最後の性質から、終わりのない無限ストリームに sorted を付けると、全要素が届くのを待ち続けることになります。Stream.iterate や generate で作ったストリームは、limit で件数を決めてから sorted に渡します。

書いたあとのチェックリスト

  • 引数なしの sorted() を使う要素は Comparable を実装しているか
  • reversed() が「全体」を逆にしていないか(一部だけ降順なら第2引数で指定)
  • ラムダ式に reversed() を続けていないか(メソッド参照か型つきラムダにする)
  • null が入る可能性のある項目に nullsFirst / nullsLast を指定したか
  • limit・skip は sorted のあとに書いたか
  • 結果を toList() などで受け取っているか(sorted だけでは何も起きない)

よくある質問

Q. sorted を呼んだのに、元のリストの順番が変わりません。 A. sorted は元のリストを書き換えず、並べ替えた新しいストリームを返します。List<Integer> result = list.stream().sorted().toList(); のように結果を変数で受け取ってください。元のリスト自体を並べ替えたい場合は list.sort(...) を使います。

Q. 降順にするには reversed() と Comparator.reverseOrder() のどちらを使いますか。 A. Integer や String をそのまま降順にするなら sorted(Comparator.reverseOrder())、項目を指定するなら Comparator.comparing(Item::price).reversed() が短く書けます。複数条件のうち一部だけ降順にしたいときは、thenComparing(Item::price, Comparator.reverseOrder()) のように第2引数で指定すると、ほかの条件に影響しません。

Q. toList() と collect(Collectors.toList()) はどう違いますか。 A. toList() は Java 16 で追加された書き方で、変更できないリストを返します。追加や削除をすると UnsupportedOperationException になります。あとから変更したい場合は collect(Collectors.toCollection(ArrayList::new)) を使うと、ArrayList であることを明示できます。

まとめ

  • sorted() は自然な順序、sorted(Comparator) は指定した順序で並べ、元のリストは変わらない
  • 降順は Comparator.reverseOrder() か reversed()。reversed() はそこまでの条件全体を逆にするので、一部だけ降順なら thenComparing の第2引数で指定する
  • Comparable を実装していないクラスに引数なしの sorted() を使うと、終端操作の実行時に ClassCastException。Comparator を渡すか Comparable を実装する
  • ラムダ式の comparing に reversed() を続けると型が Object と推論されてコンパイルエラー。メソッド参照か型つきラムダにする
  • null は nullsFirst / nullsLast で位置を決める。項目が null の場合は comparing の第2引数に渡す
  • 上位N件は sorted → limit、ページングは sorted → skip → limit。順序を入れ替えると結果が変わる
  • 受け取りは toList()(Java 16以降、変更できないリスト)。変更したいなら Collectors.toCollection(ArrayList::new)

参考・出典

  • Oracle「Java SE 21 API仕様 Stream」https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html
  • Oracle「Java SE 21 API仕様 Comparator」https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Stream sortedの使い方 降順・複数条件・null対応」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?