Consumer と Supplier

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

Consumer と Supplier とは

java.util.function パッケージの代表的な関数型インターフェース Consumer と Supplier の役割と使い分けを学び、List.forEach や Stream.peek での活用までを身につ。

ConsumerSupplier ─ 入出力で覚える関数型インターフェース

前のレッスンでは Function<T, R>Predicate<T> を学びました。今回はその仲間である Consumer<T>Supplier<T> を取り上げます。どちらも java.util.function パッケージにいる代表的な関数型インターフェースで、Stream API やコレクションのメソッドと組み合わせて毎日のように出てきます。覚え方はとてもシンプルで、入力と出力のどちらがあるか だけ意識すればすぐに使い分けられるようになります。

Consumer<T> は名前のとおり「T を消費する」インターフェースで、引数を 1 つ受け取り、戻り値はありません。accept(T t) という抽象メソッドが定義されていて、ラムダ式で書くと x -> System.out.println(x) のような形になります。一方の Supplier<T> は「T を供給する」インターフェースで、引数はなし、戻り値だけがあります。get() という抽象メソッドがあって、ラムダ式は () -> "hi" のように書きます。Java の標準ライブラリ全体を眺めると、この 2 つは FunctionPredicate と並んでとびきり登場頻度が高い顔ぶれです。

diagram (will load when visible)

4 つの代表的な関数型インターフェースは「入力ありなし × 出力ありなし」の 2 軸でほぼ整理できます。Function<T, R> は入力あり出力あり、Predicate<T> は入力あり出力 booleanConsumer<T> は入力あり出力なし、Supplier<T> は入力なし出力あり、という覚え方が定番です。

Consumer<T> ─ 引数 1 つ、戻り値なし (accept)

Consumer<T> は「副作用を起こすこと」が役割の関数型インターフェースです。println のような出力、List.add のような状態変更、Logger.info のようなログ書き出しなど、戻り値を返さずに何かを実行する 場面で出番がきます。インターフェース定義をざっくり書くと次のとおりです。

Java

@FunctionalInterface public interface Consumer<T> { void accept(T t); }

抽象メソッドは void 戻り値の accept(T t) だけ。ラムダ式で Consumer<String> を作るとこんな感じになります。

Java

import java.util.function.Consumer; public class Demo { public static void main(String[] args) { Consumer<String> printer = s -> System.out.println("hello " + s); printer.accept("alice"); // hello alice printer.accept("bob"); // hello bob } }

変数 printer の型は Consumer<String> で、これは「String を受け取って何か副作用を起こす関数」の容器です。accept を呼び出すことで、ラムダの中身が実行されます。Windows のターミナルでも Android Studio のログでも、デバッグで「とりあえず途中経過を表示したい」ような場面に Consumer は重宝します。

Consumer の戻り値が void なのは、設計上の強い意味があります。「この関数は副作用しかしない、結果は呼び出し元に戻さない」と型レベルで宣言することで、Stream API などのパイプラインで安心して使えるようになっています。

Supplier<T> ─ 引数なし、戻り値あり (get)

Supplier<T> はその逆で、引数を受け取らずに値を 1 つ返すインターフェースです。定義は次のとおりです。

Java

@FunctionalInterface public interface Supplier<T> { T get(); }

抽象メソッド get() は引数なしで T を返します。ラムダ式は () -> "hi" のように、左側のかっこが空っぽになります。

Java

import java.util.function.Supplier; public class Demo { public static void main(String[] args) { Supplier<String> hello = () -> "hi"; String value = hello.get(); System.out.println(value); // hi } }

Supplier の出番は「呼ばれたときに初めて値を作る」場面です。たとえば Optional.orElseGet(Supplier<T>) は、値が空のときだけ Supplier を呼んでデフォルト値を生成します。Logger.fine(() -> heavyMessage()) のように、ログレベルが有効なときだけ重い処理を走らせる遅延評価でもおなじみです。Stream.generate(Supplier<T>) で無限ストリームを作るときの素にもなります。

Supplier を「ファクトリ」と呼ぶ人もいます。新しいインスタンスをその場で作って返す () -> new ArrayList<>() のような書き方は、Java 8 以降のスタイルでは至るところで出てきます。コンストラクタ参照 ArrayList::newSupplier<ArrayList<?>> として扱えます。

BiConsumer BiFunction の予告

ConsumerFunction は「引数 1 つ」のインターフェースですが、引数を 2 つ受け取る兄弟分として BiConsumer<T, U>BiFunction<T, U, R> が用意されています。

Java

import java.util.function.BiConsumer; import java.util.function.BiFunction; BiConsumer<String, Integer> log = (name, age) -> System.out.println(name + ": " + age); BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; log.accept("alice", 20); // alice: 20 int sum = add.apply(2, 3); // 5

Map.forEach((key, value) -> ...) のように Map を回すときは BiConsumer がそのまま当てはまります。Map.mergereduce のような畳み込みでは BiFunction が大活躍します。今回の本題ではありませんが、「2 引数版もある」とだけ頭の片隅に置いておきましょう。

List.forEach Stream.peek での活用

Consumer の具体的な活用先として、まず List.forEach(Consumer<? super T>) があります。これは「リストの全要素に対して Consumer を順番に適用する」拡張 for ループの代わりです。

Java

List<String> names = List.of("alice", "bob", "carol"); names.forEach(name -> System.out.println("hi " + name)); names.forEach(System.out::println);

下の行ではメソッド参照 System.out::println を使っていますが、これも Consumer<String> の一種として渡せます。Javafor (var x : list) を書くより forEach の方がスッキリすると感じる人も多いです。

もうひとつ、Stream API の peek(Consumer<? super T>) も Consumer が活躍する場所です。peek はストリームを流れる要素を変換せずに「のぞき見る」中間操作で、デバッグやログ用途にぴったりです。

Java

int total = Stream.of(1, 2, 3, 4) .peek(n -> System.out.println("in: " + n)) .mapToInt(Integer::intValue) .sum();

Supplier の方は、Stream.generate Optional.orElseGet Objects.requireNonNullElseGet Logger.fine など、「あとから値が必要になったときだけ計算したい」場面の標準 API でずらりと使われています。書き手のうちはあまり意識しなくても、ライブラリのシグネチャを読むと頻繁に登場するので、Supplier と見たら「引数なしで値を返すラムダだな」とすぐに変換できるようになっておきましょう。

よくある間違い

初学者が ConsumerSupplier を扱うときにつまずきやすいポイントを 3 つまとめておきます。エラーメッセージで悩む前に、ここをチェックしてみてください。

  • Consumer の中で return を書いてしまうConsumer<String> c = s -> { return s.length(); }; のように戻り値を書くとコンパイルエラーになります。accept の戻り値は void なので、ラムダの中も値を返してはいけません。値を返したいのは Function<T, R> の役割です
  • Supplier.get() を毎回呼ぶのに副作用付きのラムダを書いてしまうSupplier<Integer> next = () -> counter++; のように、内側でフィールドを書き換える Supplier は危険です。同じ Supplier を get() するたびに別の値が返ってくるので、ライブラリ側がいつ呼ぶかわからない (orElseGet Stream.generate 等) 文脈で予測不能なバグになります。Supplier は基本的に副作用なしで書きましょう
  • hello.get のようにかっこを忘れて参照だけ書くhelloSupplier<String> ですが、String s = hello; と書くと型が違うとエラーになります。値を取り出すには必ず hello.get() のようにかっこ付きで get() を呼びます。Consumer も同様で、呼び出すときは printer.accept("x") の形が必要です

細かい話だと、Consumer<T> には andThen(Consumer<T>) という合成メソッドがあって、c1.andThen(c2) で「c1 のあと c2」を順に適用する Consumer が作れます。一方の Supplier<T> には andThencompose がありません。Supplier の合成は実用上あまり意味がない (出力を別の Supplier に流せない) ので、API として用意されていない、という設計理由があります。

関数型インターフェースは「あくまで型の名前」であって、特別な魔法ではありません。Consumer<String> という型は「void accept(String) というメソッドを持つ何か」を入れる容器にすぎず、ラムダ式 s -> ... は内部的にこのメソッドを実装した無名クラスとして扱われます。仕組みが見えてくると、@FunctionalInterface の意味も自然と腑に落ちるはずです。

やってみよう

今回の課題は、Supplier<String> を作って get() の結果をそのまま返す Solution.forwardSupplier() を書くことです。次の手順で組み立ててみましょう。

  1. java.util.function.Supplierimport する
  2. メソッドの中で Supplier<String> hello = () -> "hi"; を作る
  3. return hello.get();"hi" を返す

テストは戻り値が文字列 "hi" であることだけを確認します。Supplier の宣言を変数に取らずに return ((Supplier<String>) () -> "hi").get(); のように 1 行で書いても OK ですが、はじめは変数に入れて 2 行で書くと読みやすいです。

余裕があれば、Consumer<String> を 1 つ作って "hi" をそのまま System.out.println するコードや、Supplier<Integer> counter = () -> 42; のように別の型でも遊んでみてください。同じインターフェースを違う型パラメータで使いまわせる、というジェネリクスの良さが体感できます。次のレッスンでは、ここまでに見た Function Predicate Consumer Supplier を組み合わせて、Stream のパイプラインを設計する考え方に進みます。

よくある質問

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 のコードを読む経験も大きく成長を促します。

次のレッスン

次は BiFunction と 2 引数の関数 で、BiFunction と 2 引数の関数 を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. ファイル先頭で import java.util.function.Supplier; を書くこと
  2. メソッド内で Supplier<String> をラムダ式 () -> "hi" で生成すること
  3. 生成した Supplierget() を呼び出して、その戻り値を return すること (固定文字列を直接返すのは NG)

入出力例

test-cases.txt

forwardSupplier()"hi"

ヒント

main.java
main.java
学習モード

メモ

Consumer と Supplier

⌘S で保存