sorted で並び替え

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

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 を返す中間操作 だという点です。元の Listint[] は書き換わりません。Collections.sortvoid で破壊的に並べ替えるのと対照的で、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> のラムダ式です。ab より小さければ負の値を返し、a を先に並べてもらいます。逆に大きければ正の値を返して b を先に並べてもらいます。0 なら同順です。

引き算で比較するイディオム (a - b) は整数オーバーフローの危険があります。Integer.MIN_VALUEInteger.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) のように メソッド参照 を渡すと、ラムダ式より短くて意図が伝わりやすくなります。読み手にとっても「Stringlength で並べているんだな」と一目で分かるので、業務コードではメソッド参照を積極的に使うのがおすすめです。

ソートの安定性と並列ストリーム

もう 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 パイプラインの中で何が起きているかを図で追ってみましょう。

diagram (will load when visible)

図のポイントは、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() で昇順に並べ替えます。IntStreamsorted() は引数を取らず、常に昇順固定です (降順にしたいときは後で boxed() してから Comparator.reverseOrder() を渡します)。

そのあと mapToObj(String::valueOf) で各 intString に変換し、最後に Collectors.joining(","), 区切りの 1 つの文字列に連結します。joining は内部で StringBuilder を使って効率的につないでくれるので、自分で += で文字列を結合するより圧倒的に速く、エラーも起きにくい安全な書き方です。

空配列のガードを忘れると、Collectors.joining(",") は空文字列 "" を返してくれるので結果としては大丈夫なのですが、明示的に if (arr.length == 0) return ""; と書いておくと、コードの意図が読み手にも伝わりやすくなります。「空のときは空文字列」というルールはレビューでよく問われる箇所なので、ガードを書いておく癖をつけましょう。

よくある間違い

Stream.sorted() まわりで初心者がハマる代表的な罠は下記のとおりです。

  • sorted(null) のように Comparatornull を渡す — 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) を完成させてみましょう。手順は下記の通りです。

  1. メソッドの先頭で if (arr.length == 0) return ""; の空配列ガードを書く
  2. Arrays.stream(arr)IntStream を作る
  3. .sorted() で昇順に並び替える
  4. .mapToObj(String::valueOf)intString に変換する
  5. .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 つができることを確認しましょう。

  1. sorted の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. sorted とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. クラス名は Solution、メソッド名は sortedJoined、引数は int[] arr ひとつにすること
  2. 戻り値の型は String で、Stream API の sorted() を使って昇順に並べ替え、, 区切りの 1 つの文字列にして返すこと
  3. 配列が空 (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"

ヒント

main.java
main.java
学習モード

メモ

sorted で並び替え

⌘S で保存