関数型インターフェースの中には、戻り値が特定のクラスやインターフェースのインスタンスを返すものを作成することも可能です。もし、その戻り値も関数型インターフェースの場合、呼び出し元の関数型インターフェースおよびその戻り値の関数型インターフェースも一緒に実装可能です。
例えば、次のような何らかのインターフェースを返すGetInterfaceがあったとします。
@FunctionalInterface public interface GetInterface<T> { T get(); }
このGetInterfaceが関数型インターフェースであるDoSomethingInterfaceを返し、そのDoSomethingInterfaceが「Hello」と標準出力するように実装する場合、次のようになります。
public static void main(String[] args) { GetInterface<DoSomethingInterface> getInterface = ()->()->System.out.println("Hello"); DoSomethingInterface doSomethingInterface = getInterface.get(); doSomethingInterface.doSomething(); }
これを実行すると、次のようになります。
Hello
ここでは、2つの関数型インターフェースの処理が一緒に実装されています。3行目の「()->()->System.out.println("Hello")」がGetInterfaceの実装部分で、そこからさらに「()->System.out.println("Hello")」がDoSomethingInterfaceの実装部分です。
GetInterfaceはラムダ式のままの状態で、DoSomethingInterfaceの実装をラムダ式から匿名クラスに変えると、次のようになります。
GetInterface<DoSomethingInterface> getInterface = () -> { return new DoSomethingInterface() { @Override public void doSomething() { System.out.println("Hello"); } }; }; DoSomethingInterface doSomethingInterface = getInterface.get(); doSomethingInterface.doSomething();
ラムダ式では文法的に正しければ処理の実装で再帰的に自分自身のメソッドを呼び出すことも可能です。
例えば、次のような引数のintを文字列に変換する関数型インターフェースがあったとします。
@FunctionalInterface public interface IntToStringInterface { String convert(int value); }
ここで引数が10より小さい場合は引数を加算して再び自分自身にその値を渡し、10以上の場合は文字列に変換するような処理を実装した場合、次のようになります。8行目の「functionalInterface.convert(++value)」の部分が自分自身の呼び出しです。
public class SampleClass { private IntToStringInterface functionalInterface; private void process(int arg) { functionalInterface = value -> value < 10 ? functionalInterface.convert(++value) : String.valueOf(value); System.out.println("結果=" + functionalInterface.convert(arg)); } public static void main(String[] args) { SampleClass sample = new SampleClass(); // 10より小さい場合 sample.process(0); // 10以上の場合 sample.process(11); } }
これを実行すると下記の結果を得られます。ここでは与えられた引数が0(ゼロ)の場合、その引数が10になるまで加算して再帰的に呼び出され、10になった時点で文字列として返されています。与えられた引数が11の場合はそのまま文字列に変換して返されています。
結果=10 結果=11
ここで行われていることを細かく見ていきます。まず、引数の値「value」が10より小さい場合、valueを加算して再び自分自身を呼び出しています。そして再度「value < 10」の評価を行い、valueの値が10になるまで繰り返されます。
また、ここでfunctionalInterfaceの関数型インターフェースが実装されているので、それ以前にfunctionalInterfaceの関数型インターフェースが定義していても、その処理はここで書き換えられてしまいます。
例えば、次のようにクラス変数での宣言時に定義していた場合、SampleClassのprocessメソッドで関数型インターフェースのfunctionalInterfaceの処理を新しい実装で上書きしているため、クラス変数宣言時に定義していた処理は実行されることはありません。
今回はJavaのラムダ式の読み書きができるように基本的な記述方法を見てきました。しかし後半で紹介した応用した記述のように複雑になった記述が出てきた場合、何をしているのかすぐに分かりづらい実装が出てくることが想像できます。
そして、そのことはバグが見つけづらい実装や書いた本人しかすぐには解読できないような実装が今後出てくる原因になるかと思われます。そういうことを防ぐためにもラムダ式の実装ルールをうまく作っておかないと後々管理しづらくなることが予想できます。
Javaのラムダ式が正式にリリースされた今後は、ラムダ式のメリットを生かしつつ問題が起きにくい実装方法を考えていくのが、これからの課題になっていくのかもしれません。
また関数型プログラミングとは本来は引数の値が毎回同じなら結果も同じものを返し外部への影響を及ぼさないことを原則としています。関数型インターフェースをこの関数型プログラミングという点で見ると今回説明したようなクラスの変数やメソッドを扱うのは外部への影響を与えるため、この原則から外れてしまいます。
しかし、例えばGUIの実装の際に多くのListenerを関数型インターフェースとして扱うことができるので、ラムダ式を使ってソースを簡潔に実装することはあるかと思われます。そのため関数型というだけで、この原則に縛ることは実際には難しいかと思います。そこら辺を含めJavaの世界が今後どのようになるのか興味深い点でもあります。
次回からはラムダ式に関係がある、「Stream」などJava 8の新しいAPIを見ていきます。ご期待ください。
長谷川 智之(はせがわ ともゆき)
株式会社ビーブレイクシステムズ開発部所属。
社内サークル執筆チーム在籍。
主な執筆。
@IT連載『Javaの常識を変えるPlay framework入門』
日経ソフトウェア連載『コツコツ学ぶAndroidネイティブアプリ開発教室』
Copyright © ITmedia, Inc. All Rights Reserved.