ProjectLambda・第2版・1部(関数型インターフェース〜ラムダ式)
第2版の説明
以前からProjectLambdaについていろいろ書いてきました。
最初の投稿は2012/2/14、バレンタインの日でした。(投稿日が物語るように恋人居ないんです)
思えば一年以上前、志望校の一般入試が終わって精神的に解放されたころでした。(受験勉強中にProjectLambdaで遊んで現実逃避してたのは内緒w)
あの頃と今とではかなり仕様が変わってしまっています。なので、書き直してみたいと思いますのでお付き合いください。
なお、参考のために古い記事も消さずに残しておきます。
ProjectLambda対応JDKの導入
ProjectLambdaのアーリ・ドラフト・リビューはこちらにあります。
ラムダ式などのProjectLambdaの機能を使えるJDK(アーリビルド)はこちらにあります。
2013/04/09時点での最新版はb84です。
JDKはただのzipなどで配布されているので適当に解凍して適当にパスを通せば使えるようになります。
まだ完全に仕様が決まったわけではありませんので、最終的な仕様と異なることが書かれている可能性があります。
JDK8のアーリビルドはこちらに有りますがProjectLambdaのライブラリが2013/04/09現在まだマージされていないのでマージされるまでは上記したProjectLambdaのJDKを使用してください。
Javaのラムダ式の説明
ラムダ式がどのように表記されるのかの前にラムダ式がどのようなものなのかについて説明します。
Javaのラムダ式は完全なクロージャではありません。そして、関数型を導入したりもしません。
Javaのラムダ式はSAM Typeなインターフェース(関数型インターフェース[Functional Interface])と言う特殊なインターフェースを簡単に実装してインスタンス化できる構文です。
つまり、匿名クラスを簡単に書けるようにするのがJavaのラムダ式です。(完全なシンタックスシュガーでは無いですがイメージとしてはそのような感じです)
さて、SAM Typeや関数型インターフェースと言う聞きなれない言葉があります。これらの概念はProjectLambdaによって導入されるものなのでまずこの二つについて説明しましょう。
まず、SAM Typeと言うのはSingle-Abstract-Method Typeの略で抽象メソッドを一つだけ持っているクラスやインターフェースの事を指します。
例えば、次の様なものがSAM Typeに該当します。
abstract class ClassA { public abstract void method(); } abstract class ClassB { public abstract void method(); public void method(int n) {} // 抽象メソッドが一つだけあればabstractでないメソッドがあっても良い } interface InterfaceA { void method(); }
関数型インターフェースはSAM Typeなインターフェースの事を指します。上の例で言うとInterfaceAがそれに相当します。
そして、ラムダ式によって作成されるのは前途の通り関数型インターフェースを実装したクラスの「インスタンス」になります。
*1
なお、定義したインターフェースが関数型インターフェースであるかをコンパイル時に検査することを指示するアノテーション、@FunctionalInterfaceが導入されます。
これをインターフェースの定義時に注釈するとコンパイラが関数型インターフェースの要件を満たしているかを確認してくれるので関数型インターフェースを定義する際は使用すると良いでしょう。
このアノテーションが付いていて関数型インターフェースの要件を満たしていない場合(抽象メソッドが1つでない場合やクラスの場合など)はコンパイルエラーになります。
例えばComparatorも関数型インターフェースでこれにラムダ式を使うと次の様になります。
*2
List<Integer> list = ...; Collections.sort(list, new Comparator<Integer>() { public int compare(Integer n1, Integer n2) // 従来 { return n1 - n2; } }); Collections.sort(list, (n1, n2) -> n1 - n2); // ラムダ式使用
従来のものと見比べるとずいぶん短くなっているのが分ると思います。*3
そもそもなんでJavaにラムダ式を導入するの?というJavaにラムダ式を導入する利点については、「クロージャからProject Lambdaへ(3ページ目) | 日経 xTECH(クロステック)」や「Java in the Box Annex: Project Lambda」を参考にしてください。
また、初期の案では関数型を導入しようとしていました。
色々あって今の形に落ち着きましたが、その紆余曲折っぷりは「http://d.hatena.ne.jp/t_yano/20100729/1280409586」を参考にしてください。
ちなみに表記法も最初とは大きく違ったものになってしまいました。
個人的には関数型の頃の#を使った表記が好きだったのですが・・・
ラムダ式の書き方
ラムダ式がどのようなものを表すのかが分かったところでラムダ式の書き方を紹介します。
Javaのラムダ式はC#やScalaに似た構文を採用しました。
(引数リスト) -> 本体
参考までにScalaは次の様に書きます。
(引数リスト) => 本体
矢印の形が違うという差はありますが、ほとんど同じですね。
パートに分けて説明していきます。
まずは引数リストからです。
引数リストは従来のメソッドと同様に()で囲んでその中に引数を書いていきます。
引数が無い場合は()だけを書きます
() -> ・・・ // 引数が無いラムダ式 (int n) -> ・・・ // 引数がint型一つのラムダ式 (int n1, int n2) -> ・・・ // 引数がint型二つのラムダ式 ・ ・ ・ (int... n) -> ・・・ // 可変長引数を使用した場合(これは(int[] n) -> ・・・と書いても良い) (final int n1, @Annotated int n2) -> ・・・ // 修飾や注釈をした場合
final修飾したり注釈を付けたりも出来てメソッドの仮引数リストと特に変わりが無いことが分ると思います。
最初に書いたComparableのサンプルをもう一度書いてみます。
Collections.sort(list, (n1, n2) -> n1 - n2); // ラムダ式使用
なんと、これを見ると型が書かれていません。
先ほどの説明と反しますが、実は問題ありません。
というのも、ラムダ式が使用される場面では多くの場合引数や戻り値の型がコンパイル時に決まっていて、型推論できる場合が多いので省略して書いて良いことになっているのです。
例えば、今回の場合だとCollections#sortのシグネチャからComparable
型推論を使用せずに最初のサンプルを書くと次の様になります。
Collections.sort(list, (Integer n1, Integer n2) -> n1 - n2);
型推論を使用できるのであれば、先ほどの引数リストのサンプルは次の様に書くこともできます。
() -> ・・・ (n) -> ・・・ (n1, n2) -> ・・・ ・ ・ ・ (n) -> ・・・ // 可変長引数だと推論される場合
型推論を使用した場合はfinal修飾をしたり注釈を付けたりすることはできないので注意してください。
さらに、引数が一つで型推論を使用する場合は無駄な()を外して書いても良いことになっています。
n -> ・・・ // int n -> ・・・ // これはダメ
最後に、型を明記する書き方と型を省略する書き方とを混合することはできませんので注意してください。
(int n, m) -> ・・・ // エラー
引数リストの次は本体の書き方を説明します。
本体の書き方は2通りあります。
・・・ -> 式 ・・・ -> {複文}
上の書き方の時は、その式の評価結果がラムダ式の戻り値として使われます。
式の場合で戻り値がvoidなメソッドの呼び出しの場合は、戻り値が無い物として扱われます。
() -> 42 // 呼び出すと42が返される () -> null // 呼び出すとnullが返される n -> n * 2 // 呼び出すと引数nを二倍したものが返される () -> System.out.println("HelloWorld") // 戻り値がvoidなメソッド () -> new C() // 呼び出すとCのインスタンスが返される
式は()で囲っても問題ありませんので読みにくい場合などは()で囲むのもアリです。
() -> (42)
これらの書き方では本体の所に書いたものが強制的に戻り値として使用されるので、その戻り値の型が関数型インターフェースのメソッドの戻り値の型に変換出来ない場合はエラーになってしまいます。
例えば上の最後の例(戻り値の型がCと推論される場合)を"HelloWorld"と言った風に書くことはできません。
// 戻り値の型にCが予期される時 () -> "HelloWorld" // エラー(Cのオブジェクトを返さなければならないのにStringのインスタンスを返そうとしてしまっている) //ちなみにこの式は戻り値の型がObjectなどのStringと互換のある型と予期されている場合は問題ありません。
何もしない文を書くことはできません
() -> // ダメ () -> () // ダメ
また式以外も書けません。
n -> if (n > 10) System.out.println("HelloWorld") // エラー n -> for (int i = 0; i < n; i++) System.out.println("HelloWorld") // エラー
これらはブロック{}を使います。
下の書き方、{}の場合は、普通にメソッドを書くのと同じです。値を返すにはreturnを使わなければなりません。
() -> {return 42;} () -> {return null;} n -> {return n * 2;} () -> {return new String();} () -> {System.out.println("HelloWorld");} () -> {System.out.printf("HelloWorld");} () -> {} // OK n -> {if (n > 10) System.out.println("HelloWorld"); else System.out.println("WorldHello");} // OK n -> {for (int i = 0; i < n; i++) System.out.println("HelloWorld");} // OK n -> { int sum = 0; for (int i = 1; i <= n; i++) { sum += i; } return sum; }
メソッドジェネリクスの実型引数を指定してのラムダ式をどうするかという議論がありました。
結論から言うとメソッドジェネリクスの実型引数を指定するラムダ式の構文は導入されないことが決まりました。
実型引数を指定した場合のラムダ式の構文は恐らく次のようになったでしょう。
<実型引数> (引数リスト) -> 本体 <T> (T t) -> {}
この構文だとあるケースでは非常に構文解析が(コンパイラにとっても人間にとっても)複雑になる場合が出てきます。
foo((a) < b, c > d -> {});
複雑であることが特に理解しやすい例が上記です。
foo(...);のところからfooというメソッドを呼び出している事がわかります。
恐らく多くの人は,に目をつけて2つの実引数を指定しているのだと考えます。
そして,で分割をして1つ目の引数が「(a) < b」、2つ目の引数が「c > d -> {}」とするでしょう。
この時1つ目の引数については余計な()がついた比較であると説明できます。
ついで2つ目の引数ですが、「c > d」を見ると比較のように見えますが「->」があるせいで説明ができません。
このメソッド呼び出しは正しくはジェネリクス付きラムダ式「 d -> {}」(dは型推論されていて更に引数の()が取れている)をaでキャストしているとするのが正解です。恐らくコンパイラは正しく解析することはできるでしょうが人間が素早く正しく読み取ることは難しいのではないでしょうか。
ちなみに先程の例をJavaの一般的な命名規則に則り変なスペースを入れずに書くと次のようになります。
foo((FunctionalInterface)<T, S> a -> {});
こう書くと多少はわかりやすいですね。
ラムダ式を使ってみる
ラムダ式の構文を説明しましたので、実際にこれらを使ってみましょう。
ラムダ式を書くには関数型インターフェースが必要です。
ここでは汎用的な関数型インターフェースをいくつか用意しておきましょう。
@FunctionalInterface interface F // () -> void { void invoke(); } @FunctionalInterface interface F_int_int // int -> int { int invoke(int n); } @FunctionalInterface interface F0<R> // () -> R { R invoke(); } @FunctionalInterface interface F1<R, T1> // T1 -> R { R invoke(T1 arg1); } @FunctionalInterface interface F2<R, T1, T2> // (T1, T2) -> R { R invoke(T1 arg1, T2 arg2); }
F3以降の定義は省略します。
では実際に使ってみます。やはり、最初はHelloWorldを出力してみたいと思います。
public class Main { public static void main(String[] args) { F f = () -> System.out.println("HelloWorld"); f.invoke(); } }
いかがでしょうか?これでHelloWorldが出力されます。有用な例ではないですが、使ってみるという点では問題ないでしょう。
呼び出す際は普通にメソッド名を指定して呼び出さなければなりません。省略することはできません。
ちなみにFのインスタンスの生成の部分をラムダ式を書かずに書くと次の様なものになります。
F f = new F() { public void invoke() { System.out.println("HelloWorld"); } };
たったこれだけの処理のためにこれだけの行数を書けるのは煩わしいですね。
次にintの値を受け取ってそれを倍にしたものを返すラムダ式を書いてみたいと思います。
F_int_int fii = n -> n * 2; //F_int_int fii = (n) -> n * 2; // コメントアウトされた書き方でもOK //F_int_int fii = (int n) -> n * 2; // コメントアウトされた書き方でもOK //F_int_int fii = n -> {return n * 2;}; // コメントアウトされた書き方でもOK System.out.println(fii.invoke(100)) // 200
ラムダ式の本体に{}を使いたい場合はreturnを使用する必要があるのは前途の通りです、注意してください。
続けてジェネリクスなF1を使ってみます。
F1<Integer, Integer> fii = n -> n * 2; //F1<Integer, Integer> fii = (n) -> n * 2; //F1<Integer, Integer> fii = (Integer n) -> n * 2; System.out.println(fii.invoke(100)) // 200
ちなみに
F1
は不正です。
先ほどのHelloWorldの例もこの例もすべて型推論によってインスタンス化されます。
この様にラムダ式によって作成されるインスタンスの型は周辺のコンテキストに基づいて型推論され決定されます。このことをターゲット型付け[TargetTyping]と言い、その型をターゲット型[TargetType]と呼びます。
そして、次の様にターゲット型が曖昧であったり関数型インターフェースでない場合はコンパイルエラーになってしまいます。
public static void main(String[] args) { Object o = n -> n * 2; // ターゲット型:Object(ラムダ式適用不可!!) }
これを解決するには、キャストを用いて適切なターゲット型付けを促進してあげます。
Object o1 = (F_int_int)n -> n * 2; // F_int_int! Object o2 = (F1<Integer, Integer>)n -> n * 2; // F1<Integer, Integer>!
これらのまとめとしてforEachを行ってみましょう。
forEachと言うのは順にそれぞれの要素に処理を行うことです。
interface KnowHasNextBlock<T> // (T, boolean) -> void { void apply(T t, boolean hasNext); } public static <T> void forEach(Iterable<? extends T> iterable, Block<? super T> block) { for (Iterator<? extends T> iterator = iterable.iterator(); iterator.hasNext();) { block.apply(iterator.next(), iterator.hasNext()); } }
IterableとKnowHasNextBlockを引数に受け取り各要素について実行していきます。
KnowHasNextBlock#applyの第一引数には各要素が、第二引数には次の要素があるかが渡されます。
これを利用して次の要素があるかは関知しないforEachを作ってみます。
interface Block<T> { void apply(T t); } public static <T> void forEach(Iterable<? extends T> iterable, Block<? super T> block) { forEach(iterable, (t, hasNext) -> block.apply(t)); }
引数blockを使用して新しいKnowHasNextBlockのインスタンスをラムダ式を用いて作成しています。
ラムダ式は匿名クラスを作る糖衣構文なので、アクセスできる範囲も匿名クラスの物と同じ(スコープは違いますが・・・このことはまた後ほど)です。(匿名クラスからはfinal修飾されていないローカル変数にはアクセスできない)
ですが、final修飾されていないblockにアクセスしています。
実は、言語拡張によって実質的にfinalとして働くローカル変数にはfinal修飾されていなくてもアクセスできるようになりました。
これはラムダ式だけでなく匿名クラスやローカルクラスからアクセスする場合も同様です。
では実際にこれらを使ってみましょう。
import java.util.*; public class Main { public interface KnowHasNextBlock<T> { void apply(T t, boolean hasNext); } public static <T> void forEach(Iterable<? extends T> iterable, KnowHasNextBlock<? super T> block) { for (Iterator<? extends T> iterator = iterable.iterator(); iterator.hasNext();) { block.apply(iterator.next(), iterator.hasNext()); } } public interface Block<T> { void apply(T t); } public static <T> void forEach(Iterable<? extends T> iterable, Block<? super T> block) { forEach(iterable, (t, hasNext) -> block.apply(t)); } public static void main(String[] args) { Main.forEach(Arrays.asList(args), s -> System.out.println(s)); // Blockの方を使用して引数を出力する。 Integer[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9}; Main.forEach(Arrays.asList(array), (n, b) -> System.out.print(n * n + (b ? " " : "\n"))); // 1〜9までの二乗を出力する。 // 「1 4 9 16 25 36 49 64 81」 Main.forEach(Arrays.asList(array), i -> { Main.forEach(Arrays.asList(array), (j, b) -> { System.out.printf("%3d" + (b ? "" : "\n"), i * j); }); }); // 九九の表を出力する。 } }
ラムダ式の紹介はこれぐらいにしておきます。
Which is a FunctionalInterface
以下はオマケです。
どういったものが関数型インターフェースになって、どういったものがならないのか、ドラフトを参考にして説明しています。
さりげなくかなり長いです。
compareのほかにequalsなどのメソッドを持っているComparatorが関数型インターフェースになるのかの理由もここに書かれています。
@FunctionalInterface interface X { public void method(); }
これこそまさしく典型的な関数型インターフェースです。実に簡単ですね。
従来どうりジェネリクスを使っても関数型インターフェースになりえます。
@FunctionalInterface interface X<T> { public <S> void method(T t, S s); }
throwsを使用していても同じで問題ありません。
@FunctionalInterface interface X { public void method() throws Exception; }
次にダメな例を見てみましょう。
interface X { public boolean equals(Object obj); }
一見すると関数型インターフェースですが違います。
というのも、public boolean equals(Object obj)はObjectクラスで宣言されておりinterfaceでは黙示的に宣言されるメソッドなので(言語仕様9.2)これは次と同じです。
interface X {}
@FunctionalInterface interface X { public boolean equals(Object obj); public void method(); }
これは前途の通りpublic boolean equals(Object obj)は頭数に入らないので関数型インタフェースです。
この形式の代表的な関数型インターフェースはComparatorです。
ではこれはどうでしょう?
interface X { public Object clone(); public void method(); }
Objectを返すcloneメソッドはObject内で宣言されているので、前途同様関数型インターフェースでしょうか?
Objcetを返すcloneメソッドは確かにObject内で宣言されているのですが、残念ながらそれはprotectedです。
X内ではpublicなのでオーバーライドして定義されたものとなり、cloneとmethodの二つが宣言されたことになるので関数型インターフェースではありません。
@FunctionalInterface interface X { public Object clone(); }
これで関数型インタフェースです。
インターフェースで宣言可能で、暗黙的に宣言されるObjectクラスのメソッドは次の通りです。
boolean equals(Object) int hashCode() String toString()
これらはたとえ宣言されていても抽象メソッドの頭数としては数えられません。
ここまでは独立で宣言されたインターフェースについて見てきました。
次は複数のインターフェースを継承した場合どのようになるか見てみましょう。
まずは一番簡単な例です。
@FunctionalInterface interface X { public void method(Object obj); } @FunctionalInterface interface Y { public void method(Object obj); } @FunctionalInterface interface Z extends X, Y {}
ZのmethodとYのmethodは同じなのでZ内では一つになりZは関数型インタフェースです。(言語仕様9.4.1)
@FunctionalInterface interface X { public void method(Object obj); } @FunctionalInterface interface X { public void method(String str); } interface Z extends X, Y {}
オーバーロードされて抽象メソッドを二つ持つのでZは関数型インターフェースではありません。
@FunctionalInterface interface X { public int method(); } @FunctionalInterface interface Y { public long method(); } interface Z extends X, Y {}
これは関数型インターフェース以前の問題でコンパイルが通りません。(共変戻り値を使用できないため)
次にジェネリクスを使用した物です
@FunctionalInterface interface X { public Iterable method(Iterable<String> arg); } @FunctionalInterface interface Y { public Iterable<String> method(Iterable arg); } @FunctionalInterface interface Z extends X, Y {}
XのmethodとYのmethodとで同一のシグネチャになるのでZは関数型インターフェースになります。
@FunctionalInterface interface X { public int method(Iterable<String> arg); } @FunctionalInterface interface Y { public int method(Iterable<Integer> arg); } interface Z extends X, Y {}
さっきと似ていますが違います。
XのmethodとYのmethodとでは同一のシグネチャにならないのでZは関数型インターフェースになりません。
interface X<T, N extends Number> { public interface method(T arg); public interface method(N arg); } interface Y<String, Integer> {} @FunctionalInterface interface Z<Integer, Integer> {}
Xは型イレイジャされるとmethodが二つあることになり関数型インターフェースではありません。
Yも同様に関数型インターフェースではありません。
Zは型イレイジャされるとmethodが一つにまとめられるので関数型インターフェースになります。
他に、「抽象メソッドが一つだけであれば、デフォルト実装が使用されているメソッドがあっても関数型インターフェースになる」という物がありますが、これは「デフォルト実装」の説明後に書いておきます。