BiFunction と 2 引数の関数
BiFunction と 2 引数の関数 とは
2 つの引数を受け取って 1 つの値を返す BiFunction、同じ型同士を扱う BinaryOperator、そして BiPredicate / BiConsumer といった「2 引数版」の関数型インターフェースを。
2 引数の関数を表す BiFunction
ここまでのレッスンでは、Function<T, R> や Predicate<T> のように 引数 1 個 を受け取る関数型インターフェースを見てきました。しかし実際のコードでは、add(int a, int b) のように 引数 2 個 で 1 つの値を返したくなる場面が圧倒的に多いです。Java の java.util.function パッケージには、そんなときのために BiFunction というインターフェースが用意されています。
BiFunction<T, U, R> は「型 T の値と 型 U の値を受け取り、型 R の値を返す関数」を表します。たとえば BiFunction<Integer, Integer, Integer> なら、Integer を 2 つ受け取って Integer を 1 つ返す関数、つまり「足し算」「掛け算」「最大値」などをひとまとめに表現できる型です。Stream の reduce や Map.merge、computeIfPresent など、標準ライブラリのあちこちで顔を出すので、ここで一気に押さえてしまいましょう。
Functionは型パラメータが 2 個 (Function<T, R>)、BiFunctionは型パラメータが 3 個 (BiFunction<T, U, R>) と、戻り値を含めた数で覚えると混乱しません。「Bi は 引数 2 個、最後のRが戻り値」と唱えるのがおすすめです。
上の図のように、BiFunction は 2 本の入り口から値を受け取り、1 本の出口に値を返します。型 T と U は別の型でも構いません。たとえば BiFunction<String, Integer, String> なら「文字列と整数を受け取って、文字列を返す」関数になります。
apply で呼び出す
Function の呼び出しメソッドが apply(t) だったように、BiFunction の呼び出しメソッドは apply(t, u) の 2 引数版です。ラムダ式で関数本体を書き、apply で実際に動かす、という二段構えは Function と同じです。
Java
import java.util.function.BiFunction;
public class Demo {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> sum = (x, y) -> x + y;
BiFunction<Integer, Integer, Integer> max = (x, y) -> x > y ? x : y;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(sum.apply(3, 5)); // 8
System.out.println(max.apply(10, 7)); // 10
System.out.println(repeat.apply("ab", 3)); // ababab
}
}ラムダの書き方も、引数が 2 つになっただけで Function と同じ感覚で書けます。引数が 2 個以上のときは (x, y) -> ... のようにかっこが必須になることだけ覚えておけば大丈夫です。
引数 1 個のラムダは
x -> x * 2のようにかっこを省略できますが、(x, y) -> x + yの 2 引数以上はかっこ省略不可 です。x, y -> x + yと書くと文法エラーになります。Javaのラムダの細かいルールはここだけ気をつけましょう。
同じ型同士なら BinaryOperator が便利
BiFunction<Integer, Integer, Integer> のように 3 つの型がすべて同じ になることはよくあります。整数同士の足し算、文字列同士の連結、BigDecimal 同士の積、などです。こういうときは BinaryOperator<T> を使うとシンプルに書けます。
BinaryOperator<T> は BiFunction<T, T, T> を継承したサブインターフェースで、「T を 2 つ受け取って T を 1 つ返す」専用です。型パラメータが 1 個で済むので、見た目もスッキリします。
Java
import java.util.function.BinaryOperator;
import java.util.stream.Stream;
public class Demo {
public static void main(String[] args) {
BinaryOperator<Integer> sum = (x, y) -> x + y;
BinaryOperator<String> concat = (a, b) -> a + b;
// Stream.reduce は BinaryOperator を受け取る
int total = Stream.of(1, 2, 3, 4).reduce(0, sum);
System.out.println(total); // 10
}
}Stream.reduce や Map.merge、Collectors.reducing などは、引数として BinaryOperator を要求します。BiFunction<T, T, T> の値を渡しても型推論で動くケースが多いですが、宣言する側で BinaryOperator<T> と書いておくと意図が伝わりやすくなります。
標準ライブラリには
Integer::sumMath::maxString::concatのような メソッド参照 が用意されており、これらはそのままBinaryOperator<Integer>やBinaryOperator<String>として渡せます。reduce(0, Integer::sum)のように書けると一気にプロらしいコードになります。
BiPredicate と BiConsumer
2 引数版は BiFunction だけではありません。Predicate と Consumer にも対応する 2 引数版があります。
| 1 引数版 | 2 引数版 | シグネチャ |
|---|---|---|
Function<T, R> | BiFunction<T, U, R> | R apply(T, U) |
Predicate<T> | BiPredicate<T, U> | boolean test(T, U) |
Consumer<T> | BiConsumer<T, U> | void accept(T, U) |
UnaryOperator<T> | BinaryOperator<T> | T apply(T, T) |
BiPredicate は「2 つの値を比べて true / false を返す関数」で、たとえば (s, n) -> s.length() == n のような長さチェックに使えます。BiConsumer は「2 つの値を受け取って戻り値なしで処理する関数」で、Map.forEach((key, value) -> ...) のキーと値を順に取り出す処理がまさにこれです。
Java
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
public class Demo {
public static void main(String[] args) {
BiPredicate<String, Integer> lengthIs = (s, n) -> s.length() == n;
System.out.println(lengthIs.test("hello", 5)); // true
BiConsumer<String, Integer> printer = (k, v) -> System.out.println(k + "=" + v);
Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 80);
scores.put("bob", 95);
scores.forEach(printer);
}
}このように Bi シリーズを覚えておくと、Map を扱う場面で ラムダだけで全部書ける ようになり、for ループでイテレータを取り出すような古い書き方とサヨナラできます。
3 引数以上が欲しくなったら?
標準ライブラリは Bi 止まりで、TriFunction のような 3 引数版は用意されていません。これは「3 引数以上欲しくなる場面は限られているし、そういうときは自分で定義してね」という Java の設計方針によるものです。
3 引数以上の関数を扱いたいときは、次のどちらかを選びます。
- 自前で
@FunctionalInterface interface TriFunction<A, B, C, R> { R apply(A a, B b, C c); }を宣言する BiFunctionを 2 段階に分けて使う (関数合成、カリー化)- 引数をひとまとめにした レコード や DTO クラス を作って、それを
Functionに渡す
業務コードでは 3 つ目の「引数をレコードにまとめる」が最も読みやすくなるケースが多いです。「BiFunction まではあって、それ以上は自作 or 設計を見直すサイン」と覚えておきましょう。
よくある間違い
2 引数の関数型インターフェースを使うときに、初学者がつまずきやすいポイントを 3 つ紹介します。
- 型パラメータの数を間違える —
BiFunction<Integer, Integer>のように 2 個しか書かないと「型パラメータが足りない」エラーになります。BiFunctionは引数 2 個 + 戻り値 1 個の 合計 3 個 が必須です。逆にFunction<T, R>は 2 個、Predicate<T>は 1 個、と数が違うので、ペアで覚えるのがコツです - 3 引数以上を
BiFunctionで書こうとする — 標準ライブラリにはBiFunctionまでしかありません。TriFunctionをimportしようとしてcannot find symbolで立ち止まる人が多いですが、これは 自作するか、引数をまとめる のが正解です。java.util.function.TriFunctionというクラスは存在しません - 1 引数の
Functionと混同する —Function<Integer, Integer> f = (x, y) -> x + y;は文法エラーです。Functionは引数 1 個なので、ラムダも(x) -> ...かx -> ...の形である必要があります。2 引数を受け取りたいなら型をBiFunctionに変えるか、ラムダの引数を 1 つにします
エラーメッセージで
incompatible types: bad return type in lambda expressionやLambda expression not expected hereが出たら、ほぼ間違いなく「インターフェースの引数の数」と「ラムダの引数の数」がズレています。落ち着いて両者を見比べてみてください。
やってみよう
それでは、BiFunction を使った課題に挑戦しましょう。やることは次のとおりです。
Solution.combine(int a, int b)メソッドの中で、BiFunction<Integer, Integer, Integer> sum = (x, y) -> x + y;を宣言する- その
sumに対してapply(a, b)を呼ぶ - 戻り値をそのまま
returnする
直接 return a + b; と書いても結果は同じになりますが、今回はあえて BiFunction を経由することで「2 引数のラムダ + apply 呼び出し」の流れを手に馴染ませることが目的です。シンプルな課題ですが、ここで型と呼び出しの形をしっかり覚えると、後の Stream.reduce や Map.merge でラクができます。
慣れてきたら、sum の中身を (x, y) -> x * y に変えて掛け算にしてみたり、BinaryOperator<Integer> に書き換えてみたり、Integer::sum のようにメソッド参照に置き換えてみたりすると、引き出しがどんどん増えていきます。次のレッスンではこれらを Stream の reduce で活用していくので、本レッスンで「2 引数ラムダの書き方」と「apply の呼び方」をしっかり押さえておきましょう。
よくある質問
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 のコードを読む経験も大きく成長を促します。
次のレッスン
次は 関数型インターフェース まとめクイズ で、関数型インターフェース まとめクイズ を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- BiFunction の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. BiFunction とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.util.function.BiFunction;を書くこと - メソッド本体で
BiFunction<Integer, Integer, Integer>型の変数を(x, y) -> x + yのラムダで初期化すること sum.apply(a, b)のようにapplyを呼び出した戻り値をreturnすること (直接a + bを返さない)
入出力例
test-cases.txt
combine(3, 5) → 8
combine(0, 0) → 0
combine(-1, 1) → 0
combine(100, 250) → 350
combine(-7, -3) → -10