CodeGym /コース /JAVA 25 SELF /Stream API による関数型スタイル

Stream API による関数型スタイル

JAVA 25 SELF
レベル 49 , レッスン 3
使用可能

1. 命令型 vs 関数型スタイル

まずは素朴な問いから始めましょう。そもそも「関数型スタイル」はなぜ必要なのでしょうか。従来のループ中心の書き方と比べて何が良いのでしょうか。そもそも Java における「関数型スタイル」とは何を指すのでしょうか。

命令型スタイル

命令型スタイルとは、コンピューターに どのように 処理するかを手順で指示する書き方です。例えば、文字列のリストから各文字列の長さを取り出し、奇数だけを残して降順に並べたいとします。命令型では次のように書きます:

List<String> words = Arrays.asList("キリン", "ゾウ", "オランウータン", "トラ", "カンガルー");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

ここでは中間リストを明示的に作成し、手作業で要素を追加し、ソートしています。まさに手順どおりの実装です。

関数型スタイル

関数型スタイルでは、実装手順ではなく 何を 得たいかを記述します。Java ではこれを Stream API で実現します:

List<String> words = Arrays.asList("キリン", "ゾウ", "オランウータン", "トラ", "カンガルー");

List<Integer> result = words.stream()
    .map(String::length)
    .filter(len -> len % 2 != 0)
    .sorted(Comparator.reverseOrder())
    .toList();

System.out.println(result); // [7, 5, 3]

ここではデータ処理の「コンベヤ」を組み立てているようなものです。まず単語をその長さに変換し(map)、次に奇数だけをフィルタし(filter)、最後に並べ替えます(sorted)。すべてを1つのチェーンで、明示的な中間コレクションやループなしに表現できます。

比較: 反復(命令型) vs 関数型スタイル

慣れ親しんだ命令型アプローチでは、ループの中で逐一やることを記述します。各要素を走査し、条件を判定し、新しいリストに追加したり出力したりします。動きますが行数が増え、課題が複雑になるほど読み書きが大変になります。

関数型スタイルでは、処理そのものではなく目標を記述できます。長いループの代わりに操作の連鎖で「フィルタして、変換して、結果を集める」と表現します。より短く、見通しが良く、可変コレクションを手で扱う場面が減る分、バグの余地も減ります。

もちろん裏返しの面もあります。初心者には、複数のラムダが連なるチェーンは単純なループより読みにくく映るかもしれません。関数型スタイルは簡潔さと表現力で優れますが、慣れが必要です。

2. Stream API の基本操作

Stream API は単なる「新しい種類のループ」ではなく、関数型スタイルでコレクションを処理するための道具箱です。主要な操作を整理してみましょう。

Stream の取得方法

List<String> list = List.of("a", "bb", "ccc");
Stream<String> stream = list.stream();

中間操作

  • map — 要素を変換する
  • filter — 条件で要素を絞り込む
  • flatMap — 各要素をストリームに変換して平坦化する
  • sorted — 並べ替え
  • distinct — 重複を除去する
  • limit / skip — 先頭から制限する/スキップする

終端操作

  • forEach — 各要素に対して処理を実行する
  • collect — 結果をコレクションに収集する
  • reduce — ストリームを1つの値に畳み込む(例: 合計)
  • count — 要素数を数える
  • anyMatch, allMatch, noneMatch — 条件の判定

例: 処理のチェーン

List<String> names = List.of("Anna", "Boris", "Vika", "Gleb", "Dasha");

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

System.out.println(filtered); // [ANNA, BORIS, DASHA, GLEB, VIKA]

「コンベヤ」のビジュアル図:

[Anna, Boris, Vika, Gleb, Dasha]
   | filter (length>3)
[Anna, Boris, Vika, Gleb, Dasha]
   | map (toUpperCase)
[ANNA, BORIS, VIKA, GLEB, DASHA]
   | sorted
[ANNA, BORIS, DASHA, GLEB, VIKA]
   | toList

各操作は元のコレクションを変更しません。新しいストリームが生成されます.

3. ストリームの実行と遅延評価

中間操作と終端操作

Stream には2種類の操作があります。最初は中間操作で、mapfiltersorted などです。これらは新しいストリームを返し、何かを「やるつもり」だと約束するだけで、まだ実行しません。次に終端操作があり、例えば forEachcollectreduce です。これらが実際に処理全体を起動します。重要なのは、終端操作を呼ぶまではストリームは「遅延」しており、計算は始まらないという点です。

例:

Stream<String> stream = List.of("a", "bb", "ccc").stream()
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    });

System.out.println("forEach の前");
stream.forEach(System.out::println);

出力:

forEach の前
map: a
A
map: bb
BB
map: ccc
CCC

mapforEach が始まるまで実行されないことがわかります。

なぜこれは便利か?

このアプローチにより、無駄なくどれだけでも長い変換チェーンを組み立てられます。ストリームは本当に必要になった時にだけ動作します。これにより、巨大なデータや無限列さえ問題なく処理できます。また、遅延評価はメモリの節約や効率的な計算にも役立ちます。

4. 実践: 「文字列 → 長さ → 奇数のみ → 降順」

次の課題を段階的に解いてみましょう。「文字列のリストから長さのリストを得て、奇数だけを残し、降順に並べ替える。」

命令型の解法

List<String> words = Arrays.asList("キリン", "ゾウ", "オランウータン", "トラ", "カンガルー");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

Stream API を使った関数型の解法

List<String> words = Arrays.asList("キリン", "ゾウ", "オランウータン", "トラ", "カンガルー");

List<Integer> result = words.stream()
    .map(String::length) // 文字列を長さに変換
    .filter(len -> len % 2 != 0) // 奇数の長さだけ残す
    .sorted(Comparator.reverseOrder()) // 降順に並べ替える
    .toList(); // List に収集(Java 16+)

System.out.println(result); // [7, 5, 3]

補足:

  • map(String::length) — 各文字列の長さを取り出す。
  • filter(len -> len % 2 != 0) — 奇数の長さだけ残す。
  • sorted(Comparator.reverseOrder()) — 降順に並べ替え。
  • toList() — ストリームを新しいリストに収集。
たとえ

工場のベルトコンベヤのようなものです。各工程で部品が別の形に処理され、最後に箱に収められます。

5. さらに例: map, filter, forEach, collect

例1: フィルタして出力

List<String> names = List.of("Anna", "Boris", "Viktor", "Gleb", "Dasha");

names.stream()
    .filter(name -> name.contains("a"))
    .forEach(System.out::println);
// 出力: Anna, Dasha

例2: 変換して Set に収集

Set<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toSet());

System.out.println(upperNames); // [ANNA, BORIS, VIKTOR, GLEB, DASHA]

例3: すべての文字列の長さの合計を得る

int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();

System.out.println("合計の長さ: " + totalLength);

例4: Predicate と Function の使用

Predicate<String> longName = name -> name.length() > 4;
Function<String, String> greet = name -> "こんにちは、" + name + "!";

names.stream()
    .filter(longName)
    .map(greet)
    .forEach(System.out::println);
// こんにちは、Boris!
// こんにちは、Dasha!

6. Stream API による関数型スタイルの特性

可変状態を持たない

Stream API は副作用のない「純粋」な関数を推奨します。つまり、ラムダの中で外部の変数を変更しないほうがよいということです。

悪い例:

List<String> result = new ArrayList<>();
names.stream()
    .filter(name -> name.startsWith("A"))
    .forEach(result::add); // side-effect!

より良い例:

List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .toList();

操作の合成

mapfiltersorted などのメソッドを組み合わせて、非常に長いチェーンを構築できます。ただしやりすぎは禁物です。チェーンが画面より長くなったら、適切に分割することを検討しましょう。

遅延評価

Stream API は、終端操作に到達するまで何もしません。これにより、リソースを節約し、効率的なパイプラインを構築できます。

元のコレクションは不変

Stream は元のコレクションを変更しません。すべての変換は新しいストリーム/コレクションを返します。

7. Stream API を使うべきとき

Stream API が特に適しているのは次のような場合です:

  • コレクションを手早く処理したい(フィルタ、変換、ソート)。
  • 簡潔で読みやすいコードにしたい。
  • 中間コレクションを手で作りたくない。
  • 簡単に並列化したい(parallelStream())。

命令型スタイル のほうが望ましいこともあります:

  • 入れ子のループや条件が多い複雑なロジックが必要。
  • 性能クリティカルな箇所で最大限のパフォーマンスが必要(Stream API はわずかに遅い場合がある)。
  • 可変状態を扱う必要がある(その場で要素を更新するなど)。

8. Stream API でよくある誤り

誤り1: forEach を使ってコレクションを集める。 初学者がやりがちなのは、forEach で新しいコレクションに要素を追加することです。これは関数型スタイルではありません!代わりに collecttoList() を使いましょう。

誤り2: 早すぎる最適化。 いきなり parallelStream() を使わないでください。並列化は本当に大きなコレクションや CPU 集約的な処理がある場合にだけ検討します。

誤り3: Stream API とミュータブルなコレクションの混用。 Stream API は不変データを前提とするスタイルです。ラムダの中でコレクションの要素を変更するのは避けましょう。

誤り4: 結果の取りこぼし。 終端操作を呼び忘れると、何も起こりません。

誤り5: ラムダが複雑すぎる。 ラムダ式が1〜2行を超えるなら、意味のわかる名前のメソッドに切り出しましょう。

1
タスク
JAVA 25 SELF, レベル 49, レッスン 3
ロック未解除
マジカルインベントリの評価(テキストアドベンチャー) 🧙‍♂️
マジカルインベントリの評価(テキストアドベンチャー) 🧙‍♂️
1
タスク
JAVA 25 SELF, レベル 49, レッスン 3
ロック未解除
謎の遠征のための集落解析 🗺️
謎の遠征のための集落解析 🗺️
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION