java-recipes

ホーム 関数型プログラミング › Func-02

Func-02: Stream API と関数型インターフェースの組み合わせ

Stream API の各メソッドは、内部で関数型インターフェースを受け取る設計になっています。filter() には Predicate、map() には Function、forEach() には Consumer、generate() には Supplier を渡します。 この対応関係を理解することで、Stream API をより自在に使いこなせるようになります。

Stream メソッドと関数型インターフェースの対応

Stream API のメソッドに何を渡せばよいか迷ったときは、このテーブルを参考にしてください。 受け取る関数型インターフェースを理解すると、「どんなラムダ式を書けばよいか」が自然にわかるようになります。

Stream メソッド受け取る型ラムダ式の形用途
filter()Predicate<T>T → boolean条件に合う要素だけ残す
map()Function<T, R>T → R各要素を別の型に変換する
forEach()Consumer<T>T → void各要素に対して処理を実行する
generate()Supplier<T>() → T無限ストリームを生成する
collect(groupingBy())Function<T, K>T → K(グループキー)Map にグルーピングする

サンプルコード

StreamFunctionSample.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class StreamFunctionSample {

    static class Product {
        private final String name;
        private final String category;
        private final int price;

        Product(String name, String category, int price) {
            this.name = name;
            this.category = category;
            this.price = price;
        }

        public String getName() { return name; }
        public String getCategory() { return category; }
        public int getPrice() { return price; }

        @Override
        public String toString() {
            return name + "(" + price + "円)";
        }
    }

    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
            new Product("ノートPC", "電子機器", 80000),
            new Product("マウス", "電子機器", 3000),
            new Product("コーヒー", "食品", 500),
            new Product("紅茶", "食品", 400),
            new Product("キーボード", "電子機器", 8000),
            new Product("スナック", "食品", 200)
        );

        System.out.println("=== filter() + Predicate ===");
        Predicate<Product> isElectronics = p -> p.getCategory().equals("電子機器");
        Predicate<Product> isExpensive = p -> p.getPrice() >= 5000;

        products.stream()
            .filter(isElectronics.and(isExpensive))
            .forEach(System.out::println);

        System.out.println("\n=== map() + Function ===");
        Function<Product, String> toSummary =
            p -> p.getName() + " - " + p.getPrice() + "円";

        products.stream()
            .map(toSummary)
            .forEach(System.out::println);

        System.out.println("\n=== forEach() + Consumer ===");
        Consumer<Product> logProduct = p ->
            System.out.println("[" + p.getCategory() + "] " + p.getName());
        products.stream().forEach(logProduct);

        System.out.println("\n=== Stream.generate() + Supplier ===");
        Supplier<Integer> randomInt = () -> (int)(Math.random() * 100);
        List<Integer> randoms = Stream.generate(randomInt)
            .limit(5)
            .collect(Collectors.toList());
        System.out.println("乱数5件: " + randoms);

        System.out.println("\n=== collect() + groupingBy ===");
        Map<String, List<Product>> byCategory = products.stream()
            .collect(Collectors.groupingBy(Product::getCategory));
        byCategory.forEach((cat, items) ->
            System.out.println(cat + ": " + items));

        System.out.println("\n=== reduce() でカテゴリ別合計 ===");
        int totalElectronics = products.stream()
            .filter(isElectronics)
            .mapToInt(Product::getPrice)
            .sum();
        System.out.println("電子機器合計: " + totalElectronics + "円");
    }
}

Java 8 では Stream の各中間操作に対応する関数型インターフェースが決まっています。filter() は Predicate、map() は Function、forEach() は Consumer、generate() は Supplier を受け取ります。これらを変数に切り出すことで、ロジックを再利用しやすくなります。

よくあるミス・注意点

⚠️ Stream は遅延評価 — 終端操作を呼ばないと何も実行されない

Stream の filter()map() などの「中間操作」は、 それだけでは実行されません。collect()forEach()count() などの「終端操作」を呼んだときに初めて処理が走ります。 「filter() を書いたのに処理されない」と感じたときは、終端操作の呼び忘れがないか確認しましょう。

⚠️ Stream は一度しか使えない — 再利用しようとすると例外が発生する

Stream オブジェクトは終端操作を呼んだ後に「消費済み」になります。 同じ Stream オブジェクトに対して再度終端操作を呼ぶとIllegalStateException が発生します。 Stream を複数回使いたい場合は、元のコレクションから都度stream() を呼び直してください。

テストする観点

  • filter().collect() で条件に合う要素だけが残ること(境界値: 価格がちょうど 5000 円のとき含まれるか)
  • Predicate.and() で2条件をともに満たす要素だけに絞れること
  • groupingBy() の結果として、各カテゴリに正しい要素が振り分けられること
  • mapToInt().sum() でカテゴリ別の合計金額が正しく計算されること
  • 空のリストに対して Stream 操作を行っても例外が発生せず、空の結果が返ること

GitHub でソースコードを見る →