BiFunction と 2 引数の関数

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

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 つ返す関数、つまり「足し算」「掛け算」「最大値」などをひとまとめに表現できる型です。StreamreduceMap.mergecomputeIfPresent など、標準ライブラリのあちこちで顔を出すので、ここで一気に押さえてしまいましょう。

Function は型パラメータが 2 個 (Function<T, R>)、BiFunction は型パラメータが 3 個 (BiFunction<T, U, R>) と、戻り値を含めた数で覚えると混乱しません。「Bi は 引数 2 個、最後の R が戻り値」と唱えるのがおすすめです。

diagram (will load when visible)

上の図のように、BiFunction は 2 本の入り口から値を受け取り、1 本の出口に値を返します。型 TU は別の型でも構いません。たとえば 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 + y2 引数以上はかっこ省略不可 です。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.reduceMap.mergeCollectors.reducing などは、引数として BinaryOperator を要求します。BiFunction<T, T, T> の値を渡しても型推論で動くケースが多いですが、宣言する側で BinaryOperator<T> と書いておくと意図が伝わりやすくなります。

標準ライブラリには Integer::sum Math::max String::concat のような メソッド参照 が用意されており、これらはそのまま BinaryOperator<Integer>BinaryOperator<String> として渡せます。reduce(0, Integer::sum) のように書けると一気にプロらしいコードになります。

BiPredicateBiConsumer

2 引数版は BiFunction だけではありません。PredicateConsumer にも対応する 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 設計を見直すサイン」と覚えておきましょう。

diagram (will load when visible)

よくある間違い

2 引数の関数型インターフェースを使うときに、初学者がつまずきやすいポイントを 3 つ紹介します。

  • 型パラメータの数を間違えるBiFunction<Integer, Integer> のように 2 個しか書かないと「型パラメータが足りない」エラーになります。BiFunction は引数 2 個 + 戻り値 1 個の 合計 3 個 が必須です。逆に Function<T, R> は 2 個、Predicate<T> は 1 個、と数が違うので、ペアで覚えるのがコツです
  • 3 引数以上を BiFunction で書こうとする — 標準ライブラリには BiFunction までしかありません。TriFunctionimport しようとして 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 expressionLambda expression not expected here が出たら、ほぼ間違いなく「インターフェースの引数の数」と「ラムダの引数の数」がズレています。落ち着いて両者を見比べてみてください。

やってみよう

それでは、BiFunction を使った課題に挑戦しましょう。やることは次のとおりです。

  1. Solution.combine(int a, int b) メソッドの中で、BiFunction<Integer, Integer, Integer> sum = (x, y) -> x + y; を宣言する
  2. その sum に対して apply(a, b) を呼ぶ
  3. 戻り値をそのまま return する

直接 return a + b; と書いても結果は同じになりますが、今回はあえて BiFunction を経由することで「2 引数のラムダ + apply 呼び出し」の流れを手に馴染ませることが目的です。シンプルな課題ですが、ここで型と呼び出しの形をしっかり覚えると、後の Stream.reduceMap.merge でラクができます。

慣れてきたら、sum の中身を (x, y) -> x * y に変えて掛け算にしてみたり、BinaryOperator<Integer> に書き換えてみたり、Integer::sum のようにメソッド参照に置き換えてみたりすると、引き出しがどんどん増えていきます。次のレッスンではこれらを Streamreduce で活用していくので、本レッスンで「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 つができることを確認しましょう。

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. ファイル先頭で import java.util.function.BiFunction; を書くこと
  2. メソッド本体で BiFunction<Integer, Integer, Integer> 型の変数を (x, y) -> x + y のラムダで初期化すること
  3. 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

ヒント

main.java
main.java
学習モード

メモ

BiFunction と 2 引数の関数

⌘S で保存