Java BiFunctionインターフェイスのガイド

1. 前書き

Java 8はlink:/java-8-functional-interfaces[functional style programming]を導入し、関数を渡すことで汎用メソッドをパラメーター化できます。
おそらく、_Function _、_ Predicate、_、および_Consumer_などの単一パラメーターのJava 8機能インターフェイスに最も精通しているでしょう。
*このチュートリアルでは、2つのパラメーターを使用する機能インターフェイスを見ていきます*。 このような関数はバイナリ関数と呼ばれ、_BiFunction_関数インターフェースを使用してJavaで表されます。

2. 単一パラメーター関数

link:/java-8-streams-introduction[streams]で行うように、単一パラメーター関数または単項関数の使用方法を簡単にまとめましょう。
List<String> mapped = Stream.of("hello", "world")
  .map(word -> word + "!")
  .collect(Collectors.toList());

assertThat(mapped).containsExactly("hello!", "world!");
ご覧のとおり、_map_は_Function_を使用します。これは単一のパラメーターを取り、その値に対して操作を実行して新しい値を返します。

3. 2つのパラメーター操作

  • Java Streamライブラリは、ストリームの要素を結合できる_reduce_関数を提供します*。 次の項目を追加して、これまでに蓄積した値がどのように変換されるかを表現する必要があります。

    _reduce_関数は、機能インターフェイス_BinaryOperator <T> _を使用します。これは、入力と同じタイプの2つのオブジェクトを受け取ります。
    新しいアイテムをダッシュ​​区切りで先頭に配置することで、ストリーム内のすべてのアイテムを結合したいと考えてみましょう。 次のセクションでこれを実装するいくつかの方法を見ていきます。

3.1. ラムダを使用する

a _BiFunction_のラムダの実装には、括弧で囲まれた2つのパラメーターが先頭に付いています。
String result = Stream.of("hello", "world")
  .reduce("", (a, b) -> b + "-" + a);

assertThat(result).isEqualTo("world-hello-");
ご覧のとおり、_a_と_b_の2つの値は_Strings_です。 これらを組み合わせて目的の出力を作成するラムダを作成し、2番目の出力を最初に、間にダッシュを挿入します。
_reduce_は開始値(この場合は空の文字列)を使用することに注意してください。 したがって、ストリームの最初の値がそれに結合されるため、上記のコードでは最後にダッシュが付きます。
また、Javaの型推論により、ほとんどの場合、パラメーターの型を省略できることに注意する必要があります。 ラムダの型がコンテキストから明確でない場合、パラメーターに型を使用できます。
String result = Stream.of("hello", "world")
  .reduce("", (String a, String b) -> b + "-" + a);

3.2. 関数を使用する

上記のアルゴリズムの最後にダッシュを付けないようにしたい場合はどうでしょうか? *ラムダにもっとコードを書くこともできますが、それは面倒になるかもしれません。*代わりに関数を抽出しましょう。
private String combineWithoutTrailingDash(String a, String b) {
    if (a.isEmpty()) {
        return b;
    }
    return b + "-" + a;
}
そしてそれを呼び出します。
String result = Stream.of("hello", "world")
  .reduce("", (a, b) -> combineWithoutTrailingDash(a, b));

assertThat(result).isEqualTo("world-hello");
ご覧のとおり、ラムダは関数を呼び出すため、より複雑な実装をインラインに配置するよりも読みやすくなります。

3.3. メソッドリファレンスの使用

一部のIDEでは、上記のラムダをメソッド参照に変換するよう自動的に求められます。これは読みやすいことが多いためです。
メソッド参照を使用するようにコードを書き直しましょう。
String result = Stream.of("hello", "world")
  .reduce("", this::combineWithoutTrailingDash);

assertThat(result).isEqualTo("world-hello");
*多くの場合、メソッド参照により、機能コードがよりわかりやすくなります。*

4. _BiFunction_の使用

これまで、両方のパラメーターが同じタイプの関数の使用方法を示してきました。 * _BiFunction_インターフェースにより、異なるタイプのパラメーターを使用することができ、* 3番目のタイプの戻り値があります。
要素の各ペアに対して操作を実行することにより、等しいサイズの2つのリストを3番目のリストに結合するアルゴリズムを作成していると想像してみましょう。
List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);

List<String> result = new ArrayList<>();
for (int i=0; i < list1.size(); i++) {
    result.add(list1.get(i) + list2.get(i));
}

assertThat(result).containsExactly("a1", "b2", "c3");

4.1. 関数を一般化する

  • BiFunction *をコンバイナとして使用して、この特殊な関数を一般化できます。

private static <T, U, R> List<R> listCombiner(
  List<T> list1, List<U> list2, BiFunction<T, U, R> combiner) {
    List<R> result = new ArrayList<>();
    for (int i = 0; i < list1.size(); i++) {
        result.add(combiner.apply(list1.get(i), list2.get(i)));
    }
    return result;
}
ここで何が起こっているのか見てみましょう。 パラメーターには3つのタイプがあります。最初のリストの項目のタイプの_T_、2番目のリストのタイプの_U_、および組み合わせ関数が返すタイプの_R_です。
  • _apply_メソッドを呼び出してこの関数に提供される_BiFunction_を使用して結果を取得します。

4.2. 一般化された関数の呼び出し

コンバイナは_BiFunction_であり、入力と出力のタイプに関係なく、アルゴリズムを挿入できます。 試してみましょう:
List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);

List<String> result = listCombiner(list1, list2, (a, b) -> a + b);

assertThat(result).containsExactly("a1", "b2", "c3");
また、これをまったく異なるタイプの入力と出力にも使用できます。
最初のリストの値が2番目のリストの値よりも大きいかどうかを判断し、_boolean_の結果を生成するアルゴリズムを挿入しましょう。
List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, (a, b) -> a > b);

assertThat(result).containsExactly(true, true, false);

4.3. _BiFunction_メソッドリファレンス

上記のコードを、抽出されたメソッドとメソッドリファレンスで書き直しましょう。
List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, this::firstIsGreaterThanSecond);

assertThat(result).containsExactly(true, true, false);

private boolean firstIsGreaterThanSecond(Double a, Float b) {
    return a > b;
}
メソッド_firstIsGreaterThanSecond_はメソッド参照として挿入されたアルゴリズムを記述するため、これによりコードが少し読みやすくなることに注意してください。

4.4. _this_を使用した_BiFunction_メソッド参照

上記の__BiFunction -__ basedアルゴリズムを使用して、2つのリストが等しいかどうかを判断したいと考えてみましょう。
List<Float> list1 = Arrays.asList(0.1f, 0.2f, 4f);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, (a, b) -> a.equals(b));

assertThat(result).containsExactly(true, true, true);
実際にソリューションを簡素化できます。
List<Boolean> result = listCombiner(list1, list2, Float::equals);
これは、_Float_の_equals_関数が_BiFunction_と同じシグネチャを持っているためです。 _this、_の暗黙の最初のパラメーター、_Float_型のオブジェクトを取ります。 タイプ_Object_の2番目のパラメーター_other_は、比較する値です。

5. _BiFunctions_の作成

メソッド参照を使用して、数値リスト比較の例と同じことを行うことができたらどうでしょうか?
List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Double> list2 = Arrays.asList(0.1d, 0.2d, 4d);

List<Integer> result = listCombiner(list1, list2, Double::compareTo);

assertThat(result).containsExactly(1, 1, -1);
これは例に近いですが、元の_Boolean_ではなく_Integer_を返します。 これは、_Double_の_compareTo_メソッドが_Integer_を返すためです。
  • _andThen_を使用して関数を作成することにより、オリジナルを実現するために必要な追加の動作を追加できます*。 これにより、最初に2つの入力で1つのことを実行し、次に別の操作を実行する_BiFunction_が生成されます。

    次に、メソッド参照_Double

    compareTo_を_BiFunction_に強制する関数を作成しましょう。

private static <T, U, R> BiFunction<T, U, R> asBiFunction(BiFunction<T, U, R> function) {
    return function;
}
*ラムダまたはメソッド参照は、メソッド呼び出しによって変換された後にのみ_BiFunction_になります。*このヘルパー関数を使用して、ラムダを_BiFunction_オブジェクトに明示的に変換できます。
これで、_andThen_を使用して、最初の関数の上に動作を追加できます。
List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Double> list2 = Arrays.asList(0.1d, 0.2d, 4d);

List<Boolean> result = listCombiner(list1, list2,
  asBiFunction(Double::compareTo).andThen(i -> i > 0));

assertThat(result).containsExactly(true, true, false);

6. 結論

このチュートリアルでは、提供されたJava Streamsライブラリと独自のカスタム関数に関して_BiFunction_と_BinaryOperator_を調査しました。 ラムダとメソッド参照を使用して_BiFunctions_を渡す方法を調べ、関数を構成する方法を見てきました。
Javaライブラリーは、1パラメーターおよび2パラメーターの機能インターフェースのみを提供します。 より多くのパラメーターが必要な状況については、https://www.baeldung.com/java-currying [currying]の記事で詳細を参照してください。
いつものように、完全なコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-8-2[GitHub上]で入手できます。