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() が走る
JavaisDebugEnabled() が 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());
Javaenabled が 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]
}
}
Javarepeat は、「N 回 supplier.get() を呼んでリストに詰める」という“枠組み”だけを提供し、
「何を作るか」「どう作るか」は caller に任せています。
ここで Supplier<T> は、実質「T の工場」です。
もう少し現実的にするなら、例えば「毎回新しいオブジェクトを生成する」ファクトリを渡すことができます。
List<User> users = repeat(3, () -> new User("noname", 0));
Javanew 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 や、自作メソッドの「ファクトリ引数」として活躍すること
あたりです。
