ラムダ式で3つ以上の引数に対応しよう

引数が2つ以下の場合

Java8既存のFunctionインターフェースを使えば、うまく対応できます。
例:

1
2
3
4
5
6
7
// 引数が1つしかない
Function<String, String> func1 = (str1) -> str1+"!";
System.out.println(func1.apply("Aha")); //Aha!

// 引数が2つしかない
BiFunction<String, String, String> combineStrings = (str1, str2) -> str1+str2;
System.out.println(combineStrings.apply("Hello ", "World")); //Hello World

なお、既存のFunctionインターフェースを分かりやすくまとめてくれた図表を見つけましたので、転載します

Java 8 Functional Interface Naming Guide

引数が3つ以上の場合

解決法1: 自作関数型インターフェース

Java8のFunctionインターフェースにはインプットが3つ以上のインターフェースが提供されていません。
ならば、Functionインターフェースを自作してみましょう。
実は、ありがたいことに、Java8がただ単純にインプットが3つ以上の関数型インターフェースを提供してないだけで、(少なくてもOracleとOpenJDKのJavaでは)複数の引数へ対応する準備が整っています。どういうことでしょうか?まず、BiFunctionのソースを見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

@FunctionalInterface
public interface BiFunction<T, U, R> {

/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);

/**
* Returns a composed function that first applies this function to
* its input, and then applies the {@code after} function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*
* @param <V> the type of output of the {@code after} function, and of the
* composed function
* @param after the function to apply after this function is applied
* @return a composed function that first applies this function and then
* applies the {@code after} function
* @throws NullPointerException if after is null
*/
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}

肝心なのはR apply(T t, U u);だけです。このapply関数がjava.lang.reflect.Proxy.KeyFactory#applyで実装されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final class KeyFactory
implements BiFunction<ClassLoader, Class<?>[], Object>
{
@Override
public Object apply(ClassLoader classLoader, Class<?>[] interfaces) {
switch (interfaces.length) {
case 1: return new Key1(interfaces[0]); // the most frequent
case 2: return new Key2(interfaces[0], interfaces[1]);
case 0: return key0;
default: return new KeyX(interfaces);
}
}
}

ご覧のように、KeyXが存在していて、その中でfor文を使って、interfaceごとに読み込み、レファレンスを作っています。なので、自作関数型インターフェース内でFunctionやBiFunctionと似ている感じでapplyを使えば良いでしょう。

1
2
3
4
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v); // KeyX should be called
}

そして、このTriFunctionを使って、3つ以上の引数が存在するケースに対応すると、

1
2
3
4
5
TriFunction<String, Integer, Integer, String> newSubString = (str, start, number) -> 
str.substring(start-1, str.length() <= start-1+number? str.length(): start-1+number);

String f = newSubString.apply("Hello World", 2, 7);
System.out.println(f); // ello Wo

うーん、悪くありません。しかし、もし単純な処理を行いたいだけであれば、わざわざインターフェースを作るなんか少し面倒臭いでしょう。他に方法ないでしょうか。

解決法2: カリー化

JavaScriptの経験者であれば、既に思い付いたかもしれません。カリー化すればいいです。では、カリー化とは何でしょうか?

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

Wikipediaより

要するに、Java 8では、関数が変数のように扱うことができますので、戻り値で関数を返す手法が利用できます。

1
2
3
4
5
6
Function<String, Function<Integer, Function<Integer, String>>> newSubString = (str) -> (start) -> (number) 
-> str.substring(start-1, str.length() <= start-1+number? str.length(): start-1+number);


String f = newSubString.apply("Hello World").apply(2).apply(7);
System.out.println(f); // ello Wo

一回目、二回目では関数が返され、そして実行スコップから出ていない為、前の変数がクロージャーで持っていて、次の処理でも使用されます。
こんな感じで既存インターフェースを使うだけで、複数の引数へ対応できるようになりました。