ProjectLambda・2部(ラムダ式(実質的にfinal・スコープ)〜メソッド・コンストラクタ参照)
- ラムダ式と実質的にfinal
- ラムダ式のスコープ
- メソッド・コンストラクタ参照
- メソッド・コンストラクタ参照の書き方
- メソッド・コンストラクタ参照と型引数
- メソッド・コンストラクタ参照とオーバーロード
前回はJavaのラムダ式がどういう物か、またその表記法を理解し、実際に使ってみました。
今回はラムダ式のスコープやメソッド・コンストラクタ参照を見ていきます。
ラムダ式と実質的にfinal
前回に説明したとおりラムダ式は匿名クラスを作成する糖衣構文です。
また、ラムダ式内から参照できるローカル変数はfinal修飾されているか実質的にfinalとして働く物に限ります。
final int n = 0; int m = 0; int l = 0; l++; Functional f = () -> { System.out.println(n); System.out.println(m); //System.out.println(l); // エラー };
nはfinal修飾されているので当然参照できます。
mは修飾されていませんが、初期化後値は変わっていないので実質的にfinalで参照できます。
一方lはl++によって値が変わっているので実質的にfinalではありません。よって参照するとエラーになってしまいます。
これにあわせて、インナークラスから参照できるローカル変数も、final修飾されたものか実質的にfinalなものになります。
実質的にfinalというのは簡単に言うと、final修飾してもコンパイルエラーにならないような変数の事です。
なのでループ変数について
for (int i = 0; i < 10; i++) { int n = i; //Functional f = () -> System.out.println(i); // エラー Functional f = () -> System.out.println(n); }
のようにnに代入してから参照するという事が出来ます。*1
また実質的にfinalでならなければいけないので、ラムダ式内で値を変更するような動作は出来ません。
int n = 0; //Functional inc = () -> {n++;}; // コンパイルエラー
ProjectLambda単体でJavaのラムダ式が完全なクロージャにならないのはこの辺りを見れば理解できるかと思います。
ラムダ式のスコープ
匿名クラス内でthisと書くとそれは匿名クラスのインスタンスの事を指しますが、ラムダ式ではどうでしょう?
public class Main { public Main() { Functional f = () -> System.out.println(this.getClass().getName()); f.invoke(); } public static void main(String[] args) { new Main(); } }
これをコンパイルして実行してみると
Main
となってしまいました。*2
実はラムダ式内でthisを参照するとその外でthisを参照したのと同じ意味を持ちます。(superも同様です。)
さらにラムダ式内ではどのようなスコープが適用されるのでしょうか?
int n = 100; Functional f = new Functional() { public void invoke() { int n = 500; System.out.println(n); } }
よくある書き方ですね。invoke内で外のnと同じ名前の変数を宣言しています。
これは問題なくコンパイルでき、f#invokeを呼び出せば500と出力されます。
同じことをラムダ式でやってみます。
int n = 100; Functional f = () -> { int n = 500; System.out.println(n); }
これをコンパイルするとなんとコンパイルエラーになってしまいます。
Main.java:15: エラー: 変数はnで定義されています int n = 500;
実は、ラムダ式内では完全に外と同じスコープが適用されます。
いわゆるレキシカルスコープという奴です。
この辺りは微妙に匿名クラスとは違うので注意する必要があります。
ちなみにスコープが外と同じだからと言って、次の様にbreakやcontinueで制御構造を操ることはできません。
while (true) { int n = 100; Functional f = () -> { System.out.println(n); break; // エラー } f.invoke(); }
さて、実質的にfinalとして働く物への参照について確認しましたし、ラムダ式と匿名クラスの微妙な違いについても確認しましたので、話を変えてメソッド参照とコンストラクタ参照についてです。
メソッド・コンストラクタ参照
ラムダ式で、受け取った引数をそのまま使用して一つのメソッドをただ呼び出すだけのゲートウェイ的な処理を書くことがままあります。
例えば、1部の最後で出てきたforEachを呼び出す部分。
Main.forEach(Arrays.asList(args), s -> System.out.println(s));
これのs -> System.out.println(s)がそうです。
こういったシチュエーションでゲートウェイ的なラムダ式の書き方をせずに、もっとダイレクトに書けるようにしましょうというのがメソッド・コンストラクタ参照です。
このケースではメソッド参照を使って次の様に書けます。
Main.forEach(Arrays.asList(args), System.out::println);
メソッド・コンストラクタ参照の書き方
それでは実際に細かく見ていきます。
まずはメソッド参照です。
staticメソッド参照
メソッド参照の中でもstaticなメソッドへのメソッド参照を最初に取り上げましょう。
staticなメソッドへのメソッド参照は、「Clazz::statcMethod」と書きます。*3
メソッド参照するためには、ターゲットになる関数型インターフェースのメソッドとメソッド参照されるクラスメソッドとの間に互換性がある必要があります。
具体的には、ターゲットになる関数型インターフェースのメソッドの引数をすべて順番通りに使ってメソッド参照されるメソッドを呼び出したときにコンパイルエラーにならず、またそのメソッドの戻り値を返してもエラーにならなければ互換性があると言えます。
new FunctionalInterface() { public ReturnType method(A1 a1, A2 a2, A3 a3, ・・・, An an) { return Clazz.staticMethod(a1, a2, a3, ・・・, an); } }; // こういったことが出来る必要がある。
では実際に使ってみます。
public class Main { public static void sayHello() { System.out.println("HelloWorld"); } public static int mul2(int n) { return n * 2; } public static void main(String[] args) { Functional f = Main::sayHello; // Functional: void invoke() = Main::sayHello: void sayHello() Functional_int_int fii = Main::mul2; // Functional_int_int: int invoke(int) = Main::mul2: int mul2(int) f.invoke(); // HelloWorld System.out.println(f.invoke(100)); // 200 } }
Main#sayHelloはFunctional#invokeと互換性があるのでメソッド参照を利用できます。
Main#mul2はFunctional_int_int#invokeと互換性があるのでメソッド参照を利用できます。
イメージとしては次のような感じです。
Functional f = new Functional() { public void invoke() { Main.sayHello(); } }; Functional_int_int fii = new Functional() { public int invoke(int n) { return Main.mul2(n); } };
次にこれと同じことをジェネリクスを使ったFunctionalに対してやってみます。
Functional0<Void> f0 = Main::sayHello; // Void invoke() Functional1<Integer, Integer> f1 = Main::mul2; // Integer invoke(Integer)
インスタンスメソッド参照
次はインスタンスメソッドです。
インスタンスメソッドへのメソッド参照も先ほど同様互換性がある必要があります。
メソッド参照の仕方は「instance#method」です。
public class Main { public void sayHello() { System.out.println("HelloWorld"); } public int mul2(int n) { return n * 2; } public static void main(String[] args) { Main m = new Main(); Functional f = m::sayHello; Functional_int_int fii = m::mul2; f.invoke(); System.out.println(f.invoke(100)); } }
ジェネリクスを使用するものに代入するときも上記と同様です。
先ほど出てきた
Main.forEach(Arrays.asList(args), System.out::println);
もインスタンスメソッドへのメソッド参照です。
ちなみに、メソッド参照に使用するインスタンスがfinal修飾されていなくても実質的にfinalでなくてもメソッド参照を行う事が出来ます。
Main m = new Main(); m = createMain(); Functional0<Void> f = m::sayHello; // OK
メソッド・コンストラクタ参照は基本的にラムダ式に置き換えることが出来ますが、実質的にfinalでないインスタンスへのメソッド参照は直接的には置き換えられません。
for (Integer i = 0; i < 10; i++) // iは実質的にfinalではない。 { // Functional1<String> f = () -> i.toString(); // iは実質的にfinalでないので参照できずエラー Functional1<String> f = i::toString; // iがなんだろうとメソッド参照は利用できOK System.out.println(f.invoke()); }
実はインスタンスメソッド参照はインスタンス化されていなくても適用できます。
その場合は、「Clazz::method」の形で参照できます。
そして、ターゲットとなる関数型インターフェースのメソッドは第一引数にClazzのインスタンスを受け取り、第二引数以降にClazz#methodの引数を受け取る必要があります。(当然戻り値についての互換性も求められる。)
例えば、「Comparable
int method(String s1, String s2);
また、このメソッド参照で作成されたインスタンスをfとすると、"s1".compareTo("s2")はf.method("s1", "s2")と同じになります。*4
public class Main { public void sayHello() { System.out.println("HelloWorld"); } public void mul2(int n) { return n * 2; } public static void main(String[] args) { Functional1<Void, Main> f0 = Main::sayHello; Functional2<Integer, Main, Integer> f1 = Main::mul2; Main m = new Main(); f0.invoke(m); // HelloWorld // 内部的にはm.sayHello()と同じになる System.out.println(f1.invoke(m, 100)); // 200 // 内部的にはm.mul2(100)と同じになる } }
これの使用例としては、文字列を大文字小文字を無視して並び替えるときに
List<String> list = ...
Collections.sort(list, String::compareToIgnoreCase);
と言った風に書けます。
コンストラクタ参照
これでメソッド参照については一通り見ました、次はコンストラクタ参照です。
コンストラクタ参照はメソッド参照が理解できれば簡単です。
コンストラクタ参照は「Clazz::new」と書かれます。
そして、ターゲットとなる関数型インターフェースは戻り値の型がClazzと互換性があり、引数がClazzのコンストラクタと互換性のある必要があります。
戻り値の型がClazzと互換性がないといけないのは、作成されるインスタンスはその関数型インターフェースのメソッドの戻り値で返されるためです。
public class Main { public Main() {} public Main(int n) {} public Main(String s) {} public static void main(String[] args) { Functional0<Main> f = Main::new; Functional1<Main, Integer> fi = Main::new; Functional1<Main, String> fs = Main::new; Main m = f.invoke(); m = fi.invoke(10); m = fs.invoke("HelloWorld"); } }
メソッド・コンストラクタ参照と型引数
クラスの型引数は::の前に書き、メソッド・コンストラクタの型引数は::の後に書きます。(大概の場合は型推論が働いて省略できます。)
Clazz<String>::<Integer>method instance::<List<Clazz>>method Clazz<String>::<Integer>new
メソッド・コンストラクタ参照とオーバーロード
最後に、オーバーロードされているものに対してメソッド参照やコンストラクタ参照を行うと、ターゲット型が要求する引数の型に適用可能な物のうち最も近いものが選ばれます。
public static void method(Object o) {System.out.println("Object");} public static void method(String s) {System.out.println("String");} Functional1<Void, Object> fo = Main::method; // Object Functional1<Void, CharSequence> fcs = Main::method; // Object Functional1<Void, String> fs = Main::method; // String
また、メソッド参照が一意に定められないような場合はエラーになります。
例えば、
Functional1<String, Integer> f = Integer::toString;
がエラーになってしまします。
というのも、この時にInteger::toStringとした場合、次の二つの解釈が発生してしまいます。
public String toString()というインスタンスメソッドへのメソッド参照。 public static String toString(int n)というクラスメソッドへのメソッド参照。
この場合はObject::toStringで代用できます。
Class::method(Type1, Type2, ...)
と言ったメソッドの引数の型を指定できる表記法が導入されていましたが最新のバイナリでは取り除かれています。
この書き方があると便利なんだけど復活するかな?
次回はインターフェースのデフォルト実装についてです。
[修正:2012/03/03:最新のリポジトリで#の構文が削除されたので::に変更]
[追記:2012/03/14:「メソッド・コンストラクタ参照と型引数」]