ProjectLambda・第2版・2部(ラムダ式(実質的にfinal・スコープなど)〜メソッド・コンストラクタ参照)
ここで使用されているサンプルは、lambda-8-b84-03_apr_2013に基づいて作成されています。現在の仕様とは違っている点があるかもしれませんので注意してください。
- キーワードとしての"_"(アンダーバー)
- 交差型キャスト
- ラムダ式と実質的にfinal
- ラムダ式のスコープ
- メソッド・コンストラクタ参照
- メソッド・コンストラクタ参照の書き方
- メソッド・コンストラクタ参照と型引数
- メソッド・コンストラクタ参照とオーバーロード
前回はJavaのラムダ式がどういう物か、またその表記法を理解し、実際に使ってみました。
今回はラムダ式の導入に伴って変更される言語仕様やラムダ式と匿名クラスとの違いやメソッド・コンストラクタ参照を見ていきます。
キーワードとしての"_"(アンダーバー)
今まではJavaでは_を(変数名などの)識別子として使用することができ、_が特別な意味を持つことはありませんでした。
その一方でラムダ式を有している言語では_が特別な意味を持つことがあります。
例えばScalaのプレースホルダーなどです。
これらとの混乱を避けるために、また将来_に特別な意味を持たせるためにもJavaSE8では_が予約語となります。*1 *2
JavaSE8での識別子としての_の取り扱いですが、ラムダ式の引数としての_はコンパイルエラーとなります。互換性の観点からそれ以外の識別子として_が使用されている場合はコンパイル時に警告となります。
なお、_のみでなく_を含む識別子(例えば_1やunder_barなど)は問題ありません。
public class Main { public static void main(String[] args) { F1<Integer, Integer> f = _ -> _; // エラー } }
このような場合はコンパイルエラーになります。
public class _ // 警告 { public static void _() // 警告 { int _; // 警告 } }
このような場合は警告になります。
交差型キャスト
Javaには交差型、あるいは合成型と呼ばれるものがあります。
今まではジェネリクスの型境界にだけ使えていた「Type1 & Type2」のように型を&でつないだものです。
これがJavaSE8からはキャスト式で利用できるようになります。
例えば次のようなものです。
(Type1 & Type2) expr;
なぜこのような事をできるようにしたかというと複数のインターフェースを合成して別の関数型インターフェースを作るという事が必要になると考えられその際にインターフェースを定義するという冗長性を取り除きたかったためです。
例えばある関数型インターフェースFがありラムダ式を書く際にシリアライズ可能にする必要があるとしましょう。そうすると交差型キャストがないと次のように書かなければならないでしょう。
interface SerializeableF extends F, Serializable {} method((SerializableF) () -> {});
このようにインターフェースを新たに定義しなければならず冗長です。
そこでキャスト時に型を合成できるように交差型キャストが導入されました。
method((F & Serializable) () -> {});
コンパイラは合成された型をラムダ式へのターゲット型として扱うので合成された型は関数型インターフェースになっていなければなりません。
なので次のようなキャストと共にラムダ式を使うとコンパイラエラーになります。
Object o = (F0<Integer, Integer> & F1<Integer, Integer, Integer>) (i) -> i; o = (Object & F) () -> {};
ラムダ式と実質的にfinal
前回述べたとおり、ラムダ式やアクセスできる外のローカル変数はfinal修飾されているか実質的に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修飾してもコンパイルエラーにならないような変数の事です。
なのでループ変数について
for (int i = 0; i < 10; i++) { int n = i; //Functional f = () -> System.out.println(i); // エラー Functional f = () -> System.out.println(n); }
のようにnに代入してから参照するという事が出来ます。*3
また実質的に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
となってしまいました。*4
実はラムダ式内で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);
先ほどの例では「System.out::println」と書いていました。
Java(Javadoc)で何らかのメソッドを指定する際には#を使用していました。
当初はこれに合わせて「System.out#println」と言う#を使用した構文が採用されていたのですが、これは取り除かれ先ほどの::を使用した構文が採用されました。
なぜこのような決定を行ったのでしょうか。
それは#が一文字の記号でありJavaにおいてとても貴重な存在だからです。
Javaにおける一文字の記号でまだ意味を持っていないのは#と`だけなのです。*5
このような記号を将来の別の言語拡張で使用する可能性があるためメソッド参照にはC++でも似た意味*6を持つ::を使用するという決定が行われたわけです。
!: 否定 ": 文字列 $: 識別子に使用可 %: 除算 &: AND ': 文字 (, ): 式を囲むカッコなど -: 減算 =: 代入 ^: XOR ~: NOT \: \uXXXXなど |: OR @: アノテーション ;: 文末 +: 足し算など :: ラベルなど *: 乗算 ,: 引数の区切りなど .: メソッドセレクタやフィールドセレクタなど /: 除算 ?: 条件演算子 <: 比較 >: 比較 {, }: ブロックなど [, ]: 配列 _: 識別子
メソッド・コンストラクタ参照の書き方
それでは実際に細かく見ていきます。
まずはメソッド参照です。
staticメソッド参照
メソッド参照の中でもstaticなメソッドへのメソッド参照を最初に取り上げましょう。
staticなメソッドへのメソッド参照は、「Clazz::statcMethod」と書きます。
メソッド参照するためには、ターゲットになる関数型インターフェースのメソッドとメソッド参照されるクラスメソッドとの間に互換性がある必要があります。
具体的には、ターゲットになる関数型インターフェースのメソッドの引数をすべて順番通りに使ってメソッド参照されるメソッドを呼び出したときにコンパイルエラーにならず、またそのメソッドの戻り値を関数型インターフェースのメソッドの戻り値として返してもエラーにならなければ互換性があると言えます。
new FInterface() { 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) { F f = Main::sayHello; // F: void invoke() = Main::sayHello: void sayHello() F_int_int fii = Main::mul2; // F_int_int: int invoke(int) = Main::mul2: int mul2(int) f.invoke(); // HelloWorld System.out.println(f.invoke(100)); // 200 } }
Main#sayHelloはF#invokeと互換性があるのでメソッド参照を利用できます。
Main#mul2はF_int_int#invokeと互換性があるのでメソッド参照を利用できます。
イメージとしては次のような感じです。*7
F f = new F() { public void invoke() { Main.sayHello(); } }; F_int_int fii = new F() { public int invoke(int n) { return Main.mul2(n); } };
次にこれと同じことをジェネリクスを使った関数型インターフェースに対してやってみます。
F1<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(); F f = m::sayHello; F_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(); F f = m::sayHello; // OK f = () -> m.sayHello(); // Error: m が実質的にfinalでない
メソッド・コンストラクタ参照は基本的にラムダ式に置き換えることが出来ますが、実質的にfinalでないインスタンスへのメソッド参照は直接的には置き換えられません。
for (Integer i = 0; i < 10; i++) // iは実質的にfinalではない。 { // F1<String> f = () -> i.toString(); // iは実質的にfinalでないので参照できずエラー F1<String> f = i::toString; // iがなんだろうとメソッド参照は利用できOK System.out.println(f.invoke()); }
実はインスタンスメソッド参照はインスタンス化されていなくても適用できます。
その場合は、「Clazz::method」の形で参照できます。
そして、ターゲットとなる関数型インターフェースのメソッドは第一引数にClazzのインスタンスを受け取り、第二引数以降にClazz#methodの引数を受け取る必要があります。(当然戻り値についての互換性も求められる。)
例えば、「Comparable
int method(String s1, String s2);
また、このメソッド参照で作成されたインスタンスをfとすると、f.method("s1", "s2")は"s1".compareTo("s2")と同じになります。*8
public class Main { public int mul2(int n) { return n * 2; } public static void main(String[] args) { F2<Integer, Main, Integer> f1 = Main::mul2; Main m = new Main(); 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) { F0<Main> f = Main::new; F1<Main, Integer> fi = Main::new; F1<Main, String> fs = Main::new; Main m = f.invoke(); m = fi.invoke(10); m = fs.invoke("HelloWorld"); } }
配列コンストラクタ参照
F1<String[], Integer> f1 = String[]::new; F1<int[], Integer> f2 = int[]::new;
この場合int[]::newは次と似たような意味になります。
new F1<int[], Integer>() { public int[] invoke(Integer i) // 引数はintと互換性がある必要がある。 { return new int[i]; } }
多次元配列の場合はどうでしょうか?多次元配列の場合も同様に書けますが、一次元配列の時と同様にターゲット型となる関数型インターフェースのメソッドはintと互換性のある引数を1つだけ持っている必要があります。例えば次のようになります。
F1<int[][], Integer> f = int[][]::new;
int::newは最外の要素数を指定したコンストラクタ参照です。
匿名クラスで書くと次のようになります。
new F1<int[][], Integer>() { public int[][] invoke(Integer i) { return new int[i][]; } }
なおnew int[y][x]のような2引数の配列のコンストラクタ参照はできません。
配列のコンストラクタ参照は常に1引数で最外の要素数を指定する実装になります。
メソッド・コンストラクタ参照と型引数
クラスの型引数は::の前に書き、メソッド・コンストラクタの型引数は::の後に書きます。(大概の場合は型推論が働いて省略できます。)
Clazz<String>::<Integer>method instance::<List<Clazz>>method Clazz<String>::<Integer>new Clazz<String>[]::new
メソッド・コンストラクタ参照とオーバーロード
最後に、オーバーロードされているものに対してメソッド参照やコンストラクタ参照を行うと、ターゲット型が要求する引数の型に適用可能な物のうち最も近いものが選ばれます。
public static String method(Object o) {return "Object";} public static String method(String s) {return "String";} F1<String, Object> fo = Main::method; // Object F1<String, CharSequence> fcs = Main::method; // Object F1<String, String> fs = Main::method; // String
また、メソッド参照が一意に定められないような場合はエラーになります。
例えば、
F1<String, Integer> f = Integer::toString;
がエラーになってしまします。
というのも、この時にInteger::toStringとした場合、次の二つの解釈が発生してしまいます。
public String toString()というインスタンスメソッドへのメソッド参照(Integer -> String) public static String toString(int n)というクラスメソッドへのメソッド参照(int -> String)
この場合はObject::toStringで代用できます。