- JAVA
Java Stream sortedの使い方 降順・複数条件・null対応
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期連続の黒字決算。