Java | Java 詳細・モダン文法:ラムダ式・関数型 – Consumer

Java Java
スポンサーリンク

Consumer を一言でいうと

java.util.function.Consumer<T> は、
「T 型の値を 1 つ受け取って“何かする”けれど、結果(戻り値)は返さない関数」 を表す関数型インターフェースです。

数式っぽく書くと T -> void
典型的には「ログを出す」「標準出力に表示する」「リストに追加する」のような“副作用のある処理”に使われます。


Consumer の中身をまず正確に押さえる

メソッドシグネチャと役割

Consumer<T> の定義の核はこの 2 つです。

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);  // 抽象メソッド:ここに“やりたい処理”を書く

    default Consumer<T> andThen(Consumer<? super T> after) {
        // 先に this.accept、その後に after.accept を実行する合成 Consumer を返す
    }
}
Java

特に重要なのは accept です。

  • 引数:T t(処理対象の値)
  • 戻り値:void(何も返さない)

つまり、「T を 1 つ受け取って、その場で何かして終わり」 という性格です。

JavaDoc にも「ほとんどの関数型インターフェースと違い、副作用で動作することが期待される」と書かれています。
ここが Consumer の本質 です。


一番シンプルな使用例:println を Consumer で書く

例:文字列を受け取って表示する Consumer

import java.util.function.Consumer;

public class ConsumerBasic {
    public static void main(String[] args) {
        Consumer<String> printer = s -> System.out.println("値: " + s);

        printer.accept("Hello");
        printer.accept("World");
    }
}
Java

ここでの対応関係を整理します。

  • 型:Consumer<String>
    → 「String -> void の関数の型」
  • ラムダ:s -> System.out.println("値: " + s)
    accept メソッドの中身
  • 呼び出し:printer.accept("Hello")
    "Hello" を受け取って、println するだけ(値は返さない)

「戻り値がない」という点が、Function<T,R>(何かを返す)との一番大きな違いです。


forEach と Consumer:よく出てくる組み合わせ

List.forEach の引数は Consumer

IterableStreamforEach は、要素ごとに何か処理をしたいときに使います。
その「何か」が Consumer<T> です。

import java.util.List;

public class ForEachExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        // その場でラムダを書くパターン
        names.forEach(name -> System.out.println(name));

        // Consumer として変数に切り出すパターン
        java.util.function.Consumer<String> printer =
                s -> System.out.println("Name: " + s);

        names.forEach(printer);
    }
}
Java

forEach のシグネチャはざっくり言うと:

void forEach(Consumer<? super T> action)
Java

つまり、
「T を 1 つ受け取って何かする処理(Consumer)を渡してくれ」 というメソッドです。

names.forEach(name -> ...) のラムダは、その場で Consumer<String> として解釈されています。


andThen で Consumer をつなげる(ここが大事)

「この処理をした後に、もう一つ別の処理もしたい」

Consumer には、複数の処理を順番につなげるための andThen が用意されています。

default Consumer<T> andThen(Consumer<? super T> after)
Java

意味は、「この Consumer の後に、after の処理を続けて実行する Consumer を返す」です。

具体例で見ましょう。

import java.util.function.Consumer;

public class ConsumerAndThenExample {
    public static void main(String[] args) {
        Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
        Consumer<String> printLength = s -> System.out.println("len = " + s.length());

        Consumer<String> combined =
                printUpper.andThen(printLength);

        combined.accept("hello");
    }
}
Java

出力イメージ:

HELLO
len = 5

ここで起きていることは、

  1. combined.accept("hello") を呼ぶ
  2. まず printUpper.accept("hello") が実行される
  3. 続けて printLength.accept("hello") が実行される

という「処理の合成」です。

ラムダ式で 1 つにまとめてしまうこともできますが、

  • 小さな処理単位(Consumer)をいくつか用意しておき
  • それらを andThen で組み合わせてパイプラインを作る

というスタイルが取れるようになると、コードの見通しがかなり良くなります。


Consumer の「副作用」という本質

なぜ戻り値がないのか

JavaDoc にはこう書かれています。

単一の入力引数を受け取って結果を返さないオペレーションを表します。
Consumer は他のほとんどの関数型インタフェースと異なり、副作用を介して動作することを期待されます。

「副作用」とは、そのメソッドの外側に影響を与えることです。例えば:

  • 画面に表示する(System.out.println
  • ファイルに書き込む
  • リストやマップに追加する
  • 外側の変数を更新する

などです。

つまり Consumer は、

「結果を返すよりも、“外側に何か影響を与える”ことが目的の関数」

として設計されています。

逆に、データを変換して次に渡したいときは、
戻り値を持つ Function<T,R> を使う方が自然です。


他の関数型インターフェースとの違いを整理する

Function / Supplier / Predicate との比較

役割の違いをざっくり言葉で整理すると、次のような感じです。

Consumer<T>
T を受け取って何かする(ログ・出力・追加など)。戻り値なし。
T -> void

Function<T,R>
T を受け取って R を返す(変換・マッピング)。
T -> R

Supplier<T>
何も受け取らず T を返す(生成・取得)。
() -> T

Predicate<T>
T を受け取って true/false を返す(条件判定)。
T -> boolean

Consumer は「副作用で完結する処理」、Function は「新しい値を作る処理」として頭の中で分けておくと、
「どっちを使うべきか?」で迷いにくくなります。


原始型向け Consumer(IntConsumer など)

オートボクシングを避けたいときのバリエーション

Consumer<T> は参照型用なので、Consumer<Integer> を使うと
intInteger のオートボクシングが発生します。

数値を大量にさばくような場面では、
プリミティブ専用のバリエーションを使うことで無駄なオーバーヘッドを減らせます。

代表的なもの:

  • IntConsumerint -> void
  • LongConsumerlong -> void
  • DoubleConsumerdouble -> void

例:

import java.util.function.IntConsumer;

public class IntConsumerExample {
    public static void main(String[] args) {
        IntConsumer printer = x -> System.out.println("x = " + x);

        printer.accept(10);  // x = 10
    }
}
Java

IntStream.forEach などと組み合わせてよく使われます。


自分のメソッドの引数として Consumer を使う発想

「やり方だけ外から渡してもらう」メソッド

例えば、「リストの全要素に対して何か処理をする」ユーティリティメソッドを考えます。

import java.util.List;
import java.util.function.Consumer;

public class ForEachUtil {

    public static <T> void forEach(List<T> list, Consumer<? super T> action) {
        for (T t : list) {
            action.accept(t);
        }
    }

    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob");

        forEach(names, s -> System.out.println("Hi, " + s));
    }
}
Java

このメソッドは、

  • 「ループの枠組み」だけを自分で持ち
  • 「各要素に対して何をするか」は Consumer として外から渡してもらう

という設計になっています。

このパターンを使えるようになると、
「アルゴリズム」と「中身の処理」を分離できて、コードの再利用性が一気に上がります。


まとめ:Consumer を自分の中でこう位置づける

Consumer<T> をあなたの言葉にすると、

「T を 1 つ受け取って、副作用だけ起こして終わる処理を表す ‘T -> void’ の関数型インターフェース」

です。

特に意識しておきたいのは、

  • 抽象メソッドは void accept(T t)(戻り値なし)
  • forEach など「要素ごとに何かしたい」場面で頻出
  • andThen で複数の Consumer をつなげて、処理のパイプラインを作れる
  • 「値を返す変換」なら Function、「条件判定」なら Predicate と使い分ける

あたりです。

タイトルとURLをコピーしました