sorted で並び替え
sorted で並び替え とは
Stream の sorted() で要素を昇順に並べ替え、Collectors.joining でカンマ区切り文字列に整形するテクニックを身につけよう。
Stream の sorted() で「並べ替え」をパイプラインに乗せる
ここまでの Stream API のレッスンで、filter で絞り込み、map で変換し、collect で集計する、という流れを学んできました。今回はその仲間に、要素を 並び替える ための sorted() を加えます。int[] や List<Integer> を「小さい順」「大きい順」「文字列の長さ順」に並べ替えたいとき、Stream の sorted() を使えば中間操作の 1 行で書ききれます。
Stream の中間操作は「水を流すパイプを 1 本ずつつないでいく」イメージで考えると整理しやすいです。
filterは「ふるい」、mapは「形を変える機械」、sortedは「並べ替えるベルトコンベア」と覚えておくと、複数の中間操作を組み合わせるときに混乱しません。
このレッスンでは int[] を受け取って sorted() で昇順に並べ替え、, で連結した文字列にして返す sortedJoined メソッドを完成させながら、sorted() の働きと「自然順 (natural order)」「Comparator を使ったカスタム順」「Comparator.reverseOrder などのショートカット」を一気に押さえていきます。
sorted() の働き — 自然順 (natural order)
まず最初の形が、引数なしで呼ぶ sorted() です。これは要素の 自然順 (natural order) に従って並び替えます。Integer なら数値の昇順、String なら辞書順 (Unicode コードポイント順)、Double なら小さい順、という具合です。
Java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Demo {
public static void main(String[] args) {
List<Integer> sorted = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6)
.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sorted); // [1, 1, 2, 3, 4, 5, 6, 9]
}
}大切なのは、sorted() は 新しい Stream を返す中間操作 だという点です。元の List や int[] は書き換わりません。Collections.sort が void で破壊的に並べ替えるのと対照的で、Stream は イミュータブル指向 に作られています。
「
sorted()を呼んだら元のlistも並び替わるのでは?」と勘違いしがちですが、Stream は元データを触りません。並んだ結果はcollectなどで取り出すか、forEachで消費する必要があります。手元のlistをそのまま並べ替えたいときはCollections.sort(list)を使い、Stream は「読み取り専用パイプライン」と覚えましょう。
sorted(Comparator) — カスタム順を渡す
自然順以外で並べ替えたいときは、sorted(Comparator) を使います。Comparator は「2 つの要素 a b を比べて 負/0/正 を返す関数」で、Java 8 以降は ラムダ式 や メソッド参照 でサクッと書けます。
Java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Demo {
public static void main(String[] args) {
List<String> words = Arrays.asList("banana", "fig", "apple", "cherry");
// 文字数の昇順
List<String> byLen = words.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(byLen); // [fig, apple, banana, cherry]
}
}(a, b) -> a.length() - b.length() が Comparator<String> のラムダ式です。a が b より小さければ負の値を返し、a を先に並べてもらいます。逆に大きければ正の値を返して b を先に並べてもらいます。0 なら同順です。
引き算で比較するイディオム (
a - b) は整数オーバーフローの危険があります。Integer.MIN_VALUEとInteger.MAX_VALUEのような極端な値で比較すると桁あふれして結果が逆転することがあるので、安全に書きたいときはInteger.compare(a, b)を使ってください。
Comparator.reverseOrder / comparing(Function) のショートカット
ラムダ式を毎回書くのは面倒なので、Java は便利なショートカットを用意してくれています。代表的なものは下記のとおりです。
Comparator.naturalOrder()— 自然順 (昇順) のComparatorを返すComparator.reverseOrder()— 逆順 (降順) のComparatorを返すComparator.comparing(Function)— 「キー抽出関数」を渡して、その値で並べるComparator.comparingInt(ToIntFunction)—intのキーで並べる (オートボクシング回避)comparator.thenComparing(other)— 第 1 キーが同じだったら第 2 キーで比較する連結
使い方を並べてみると、次のような感じになります。
Java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class Demo {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5);
// 降順
List<Integer> desc = nums.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(desc); // [5, 4, 3, 1, 1]
// 文字列を「文字数」で並べる
List<String> words = Arrays.asList("banana", "fig", "apple");
List<String> byLen = words.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
System.out.println(byLen); // [fig, apple, banana]
}
}Comparator.comparing(String::length) のように メソッド参照 を渡すと、ラムダ式より短くて意図が伝わりやすくなります。読み手にとっても「String の length で並べているんだな」と一目で分かるので、業務コードではメソッド参照を積極的に使うのがおすすめです。
ソートの安定性と並列ストリーム
もう 1 つ知っておきたいのが、sorted() の 安定性 です。Java の sorted() は内部的に Timsort という安定ソートを使っており、「並び替えのキーが同じ要素は、元の順番を保つ」というルールがあります。たとえば文字数 5 の単語が 3 つあったとき、Comparator.comparingInt(String::length) で並べると、それら 3 つは元の Stream での登場順そのままで並びます。
安定ソートは、たとえば「
nameで並べたListを、さらにageで並べ替えたい」というような 多段ソート で威力を発揮します。第 1 ソートをsorted(byName)で行い、続けてsorted(byAge)で並べ替えると、ageが同じ人は名前順のまま残ってくれます。thenComparingを使う方法と並んで、覚えておくと現場で重宝します。
なお、parallelStream() で並列処理した場合でも、Java の sorted() は安定性を保つ実装になっています。ただし並列ストリームはオーバーヘッドが大きく、要素数が 10000 程度では普通の stream() の方が速いことがほとんどです。「並列にすれば速くなる」とは限らないので、必ずベンチマークを取ってから判断してください。
Mermaid 図 — sorted 前後の Stream 状態
ここで、今回の課題 sortedJoined([3, 1, 4, 1, 5, 9, 2, 6]) を実行したときに、Stream パイプラインの中で何が起きているかを図で追ってみましょう。
図のポイントは、sorted の前後でストリームに流れる要素の 順番だけ が変わっており、要素そのものは増えも減りもしていないところです。filter のように要素を捨てたり、map のように値を変換したりはしません。
実装してみる
それでは Solution.sortedJoined(int[] arr) の実装例を見てみましょう。空配列のときは空文字列を返すルールに気をつけてください。
Java
import java.util.Arrays;
import java.util.stream.Collectors;
public class Solution {
public static String sortedJoined(int[] arr) {
if (arr.length == 0) {
return "";
}
return Arrays.stream(arr)
.sorted()
.mapToObj(String::valueOf)
.collect(Collectors.joining(","));
}
}ポイントを順番に読み下していくと、まず Arrays.stream(arr) で int[] から IntStream を作ります。次に sorted() で昇順に並べ替えます。IntStream の sorted() は引数を取らず、常に昇順固定です (降順にしたいときは後で boxed() してから Comparator.reverseOrder() を渡します)。
そのあと mapToObj(String::valueOf) で各 int を String に変換し、最後に Collectors.joining(",") で , 区切りの 1 つの文字列に連結します。joining は内部で StringBuilder を使って効率的につないでくれるので、自分で += で文字列を結合するより圧倒的に速く、エラーも起きにくい安全な書き方です。
空配列のガードを忘れると、
Collectors.joining(",")は空文字列""を返してくれるので結果としては大丈夫なのですが、明示的にif (arr.length == 0) return "";と書いておくと、コードの意図が読み手にも伝わりやすくなります。「空のときは空文字列」というルールはレビューでよく問われる箇所なので、ガードを書いておく癖をつけましょう。
よくある間違い
Stream.sorted() まわりで初心者がハマる代表的な罠は下記のとおりです。
sorted(null)のようにComparatorにnullを渡す — Java 8 の仕様では、sorted(Comparator)にnullを渡すとNullPointerExceptionが飛びます。sorted()(引数なし) は自然順を意味するので、「自然順がほしい」なら 引数を渡さない ことが正解です。nullをデフォルト扱いにしてはいけません- 自然順 (
Comparable) を実装していない型でsorted()を呼ぶ — 自作クラス (たとえばclass Point {}) のリストをsorted()(引数なし) で並べ替えようとすると、実行時にClassCastException: ... cannot be cast to class java.lang.Comparableが出ます。自作クラスを自然順で並べたいならimplements Comparable<Point>を書いてcompareToを実装するか、sorted(Comparator.comparing(...))を渡してください Comparatorに副作用を持ち込む —sorted((a, b) -> { counter++; return a - b; })のようにComparatorの中で外部変数を書き換えると、ソート結果が並列実行や JIT の最適化で予測不能になります。Comparatorは「同じ入力に同じ出力を返す純粋関数」として書く、というのが鉄則です
もう 1 つ、int[] を Arrays.stream(arr).sorted() で並べ替えたあと、.collect(Collectors.toList()) を直接呼ぶとコンパイルエラーになるのも有名な罠です。IntStream には Collectors を受け取る collect がないので、boxed() で Stream<Integer> に箱詰めしてから collect する、または今回のように mapToObj を経由する、という二段構えにする必要があります。
エラーメッセージで
incompatible types: inferred type does not conform to upper bound(s)のような怪しい文言が出てきたら、IntStreamのままでCollectorsを呼んでいないか、まず疑ってみてください。boxed()かmapToObjを 1 行はさむだけで解決することが多いです。
やってみよう
それでは右側のエディタで Solution.sortedJoined(int[] arr) を完成させてみましょう。手順は下記の通りです。
- メソッドの先頭で
if (arr.length == 0) return "";の空配列ガードを書く Arrays.stream(arr)でIntStreamを作る.sorted()で昇順に並び替える.mapToObj(String::valueOf)でint→Stringに変換する.collect(Collectors.joining(","))で,区切りの文字列にまとめてreturnする
テストが緑になったら、sorted() を boxed().sorted(Comparator.reverseOrder()) に書き換えて降順にしてみたり、区切り文字を ", " (カンマ + 半角スペース) に変えて出力の見た目を変えてみたり、いろいろ試してみてください。Stream の中間操作はパイプラインの一部を入れ替えるだけで結果が変わるので、filter map sorted を組み合わせる感覚が早く身につきます。
さらに余力があれば、String[] を受け取って Comparator.comparingInt(String::length) で文字数順に並べてから Collectors.joining("-") でつなぐ、というメソッドを自作してみるのもおすすめです。Comparator の組み立てに慣れておくと、業務で User リストを「年齢 → 名前」の順で並べたいときなどにスッと書けるようになります。次のレッスンでは Stream の終端操作 reduce で集計の幅をさらに広げていきます。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は distinct で重複排除 で、Stream の sorted() で要素を昇順に並べ替え、Collectors を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- sorted の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. sorted とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- クラス名は
Solution、メソッド名はsortedJoined、引数はint[] arrひとつにすること - 戻り値の型は
Stringで、Stream API のsorted()を使って昇順に並べ替え、,区切りの 1 つの文字列にして返すこと - 配列が空 (
arr.length == 0) のときは空文字列""を返すこと
入出力例
test-cases.txt
sortedJoined([3,1,4,1,5,9,2,6]) → "1,1,2,3,4,5,6,9"
sortedJoined([]) → ""
sortedJoined([5]) → "5"
sortedJoined([1,2,3,4,5]) → "1,2,3,4,5"
sortedJoined([9,7,5,3,1]) → "1,3,5,7,9"
sortedJoined([-1,-5,0,3,-2]) → "-5,-2,-1,0,3"
sortedJoined([7,7,7,7]) → "7,7,7,7"