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

Java Java
スポンサーリンク

Supplier を一言でいうと

java.util.function.Supplier<T> は、

「引数は何も受け取らずに、T 型の値を 1 つ“供給する(用意してくれる)”関数」

を表す関数型インターフェースです。

数式で書くと () -> T
「今は値を渡さないけど、『必要になったときに値をくれる仕組み』だけ先に渡しておく」ために使います。

例えば、

  • 「毎回新しいオブジェクトを生成する工場」
  • 「ログを取りつつ値を計算する処理」
  • 「遅延評価(本当に必要になるまで計算を遅らせる)」

などで活躍します。


Supplier の中身をまず押さえる

シグネチャ(メソッド定義)を読む

Supplier<T> は、ざっくりこう定義されています。

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

抽象メソッドはたった 1 つ:

T get();
Java

これを分解すると、

引数:なし
戻り値:T

つまり、「何も受け取らずに、T を 1 つ返す関数」 です。

Function<T,R> が「T を受け取って R を返す」のに対して、
Supplier<T> は「何も受け取らずに T を返す」と覚えておくと整理しやすいです。


一番シンプルな例:固定の値/ランダムな値を返す Supplier

例1:常に同じ文字列を返す Supplier

import java.util.function.Supplier;

public class SupplierBasic {
    public static void main(String[] args) {
        Supplier<String> helloSupplier = () -> "Hello";

        String s1 = helloSupplier.get();
        String s2 = helloSupplier.get();

        System.out.println(s1); // Hello
        System.out.println(s2); // Hello
    }
}
Java

ここでの対応関係はこうです。

Supplier<String>
→ 「() -> String の関数の型」

() -> "Hello"
→ 「何も受け取らずに "Hello" を返す処理」

helloSupplier.get()
→ 実際に値をもらう瞬間

Supplier は、「値そのもの」ではなく「値の“作り方”」を持っている、とイメージしてください。

例2:毎回ランダムな数を返す Supplier

import java.util.Random;
import java.util.function.Supplier;

public class RandomSupplierExample {
    public static void main(String[] args) {
        Random random = new Random();

        Supplier<Integer> dice =
                () -> random.nextInt(6) + 1;   // 1〜6 の乱数

        System.out.println(dice.get());
        System.out.println(dice.get());
        System.out.println(dice.get());
    }
}
Java

この dice は、「サイコロを 1 回振る処理」を表しています。

dice.get() を呼ぶたびに、
実際に random.nextInt(6) + 1 が実行されて、新しい値が返ってきます。


Supplier の本質:値ではなく「値の供給元」を渡す

値そのもの vs. Supplier の比較でイメージする

例えば、「ログを出しつつ、計算結果を返す」メソッドを作るとします。

値を直接渡す版:

public static <T> T logAndReturn(T value) {
    System.out.println("value = " + value);
    return value;
}
Java

呼ぶ側:

String s = logAndReturn(expensiveCompute());  // 高コストな計算
Java

ここでは、expensiveCompute()必ず呼ばれます
たとえ内部で「ログ出さなくていい」と判断されても、
呼び出し時点で引数が評価されてしまっています。

これを Supplier<T> で書き直すとこうなります。

import java.util.function.Supplier;

public static <T> T logAndReturn(Supplier<T> supplier) {
    T value = supplier.get();
    System.out.println("value = " + value);
    return value;
}
Java

呼ぶ側:

String s = logAndReturn(() -> expensiveCompute());
Java

ここでは、

() -> expensiveCompute() が「値の供給元(Supplier)」
logAndReturn の中で初めて supplier.get() が呼ばれる

という形になっています。

ここが Supplier の本質です。

「今すぐ値を渡す」のではなく、
「必要になったときに値を返す“工場”を渡す」。

このおかげで、「本当に必要になるまで評価しない」ように制御できたり、
「状況によってはそもそも計算をスキップする」ことも可能になります。


遅延評価(Lazy Evaluation)と Supplier

条件によって「重い処理を実行するかどうか」を後回しにする

よくあるパターンを具体的なコードで見ます。

例えば、「ログレベルが DEBUG のときだけ重い処理の結果を出力したい」という状況。

値を直接渡すとこうなります。

public static void debug(boolean enabled, String message) {
    if (enabled) {
        System.out.println(message);
    }
}

debug(isDebugEnabled(), expensiveComputeMessage()); // これだと毎回 expensiveComputeMessage() が走る
Java

isDebugEnabled() が false でも、
引数評価のせいで expensiveComputeMessage() は必ず実行されてしまいます。

Supplier を使うと、こうできます。

import java.util.function.Supplier;

public static void debug(boolean enabled, Supplier<String> messageSupplier) {
    if (enabled) {
        System.out.println(messageSupplier.get());
    }
}

// 呼び出し側
debug(isDebugEnabled(), () -> expensiveComputeMessage());
Java

enabled が false の時は messageSupplier.get() を呼ばないため、
expensiveComputeMessage() も実行されません。

「呼ばれるかどうか分からない重い処理」を、
Supplier として渡しておき、
必要になったときだけ get() する。

これが「遅延評価」と Supplier の典型的な組み合わせです。


コンストラクタやファクトリメソッドと Supplier

「インスタンスを作る方法」を引数に渡す

クラスのインスタンスを作るときにも、Supplier は相性が良いです。

例えば「何かを N 回実行して、その結果をリストに詰める」ユーティリティを作ります。

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class RepeatExample {

    public static <T> List<T> repeat(int n, Supplier<T> supplier) {
        List<T> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            list.add(supplier.get());
        }
        return list;
    }

    public static void main(String[] args) {
        List<String> list = repeat(3, () -> "Hello");
        System.out.println(list);  // [Hello, Hello, Hello]
    }
}
Java

repeat は、「N 回 supplier.get() を呼んでリストに詰める」という“枠組み”だけを提供し、
「何を作るか」「どう作るか」は caller に任せています。

ここで Supplier<T> は、実質「T の工場」です。

もう少し現実的にするなら、例えば「毎回新しいオブジェクトを生成する」ファクトリを渡すことができます。

List<User> users = repeat(3, () -> new User("noname", 0));
Java

new User(...) の部分を外から差し込めるので、テストや差し替えも容易です。


Stream と Supplier(やや応用)

Stream.generate で無限ストリームを作る

Stream.generate は、Supplier を使って無限ストリームを生成するメソッドです。

import java.util.Random;
import java.util.stream.Stream;
import java.util.function.Supplier;

public class StreamGenerateExample {
    public static void main(String[] args) {
        Random random = new Random();

        Supplier<Integer> dice = () -> random.nextInt(6) + 1;

        Stream<Integer> diceStream = Stream.generate(dice);

        diceStream
            .limit(5)
            .forEach(System.out::println);
    }
}
Java

ここでは、

Stream.generate(dice)
dice.get() を何度も呼び出しながら無限に値を供給する

limit(5)
→ そのうち 5 個だけを取り出す

という動きになります。

Supplier は、「必要なときにいくらでも値を供給できる源泉」としての役割を担っています。


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

4 つの基本インターフェースの中での位置付け

ラムダでよく使う 4 つの基本インターフェースを並べてみます。

Supplier<T>
() -> T
→ 何も受け取らずに T を返す(生成・提供)

Consumer<T>
T -> void
→ T を受け取って何かする(消費・副作用)

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

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

この中で Supplier は、「唯一、引数を取らない」インターフェースです。

「値を使う側」ではなく、「値を提供する側」
「データの入り口を定義するもの」

として頭の中に置いておくと、使い所がイメージしやすくなります。


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

Supplier<T> を一文でまとめると、

「引数を何も取らずに、T を 1 つ返す ‘値の供給元’ を表す関数型インターフェース」
です。

特に大事なのは、

T get() というシンプルなシグネチャ
「今すぐ値」ではなく「値の作り方/供給元」を渡す、という発想
遅延評価(本当に必要になるまで重い処理を実行しない)に使えること
Stream.generate や、自作メソッドの「ファクトリ引数」として活躍すること

あたりです。

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