ProjectLambda・1部(関数型インターフェース〜ラムダ式)
Java7がリリースされてから早や数か月がたちましたが、Java7に足りていない物があります。
それはラムダ式です。
今回のリリースにおいては大人の事情でラムダ式がハミゴにされてしまいました。恨めしき大人の事情。
ですが、Java8には導入される予定です。
そこで一歩早くJavaのラムダ式等々を理解し、使いこなせるようになっておきましょう。(リリースされるのは2013年ごろですが・・・)
またJavaのラムダ式のアーリ・ドラフト・リビューはこちらにあります。
ラムダ式などのProjectLambdaの機能を使えるJavaコンパイラ(アーリビルド)はこちらにあります。
コンパイラはただのzipなどで配布されているので適当に解凍して適当にパスを通せば使えるようになります。
まだ完全に仕様が決まったわけではありませんので、最終的な仕様と異なることが書かれている可能性があります。
Javaのラムダ式の説明
さてラムダ式がどのように表記されるのかの前にラムダ式がどのように実装されるのかについて話しておきましょう。
Javaのラムダ式は完全なクロージャではありません。関数型も導入されません。
Javaのラムダ式はSAM Typeなインターフェース(関数型インターフェース[Functional Interface])という特殊なインターフェースを簡単に実装してインスタンス化できる構文です。
つまり、匿名クラスを簡単に書けるようにするという事です。
なので、Javaのラムダ式は匿名クラス+αな程度のクロージャとなります。
匿名クラス+αな物をクロージャと呼ぶにはだいぶクロージャの定義のハードルを下げないといけないけど・・・
SAM Typeや関数型インターフェースについて説明すると
SAM TypeというのはSingle-Abstract-Method 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です。
また、関数型インターフェースというのはSAM Typeなインターフェースの事を指します。上記の場合はInterfaceAが関数型インターフェースになります。
そして、ラムダ式によって作成されるのは前途の通り関数型インターフェースを実装したクラス(匿名クラス)の「インスタンス」になります。
なので関数型インターフェースを満たすインターフェースを使用しているところにはすぐにでも適用可能です。
*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のラムダ式がどういったものか分かったところでラムダ式の書き方を確認しましょう。
Javaのラムダ式はC#やScalaと非常に似た記法を採用しました。
<メソッドジェネリクスのリスト> (引数リスト) -> 本体
Scalaだと
(引数リスト) => 本体
なので、矢印の形が違うなどの差は有れどかなり似てますね。
まず、引数リストについてみていきましょう。
引数リストは従来のメソッドと同様に()で囲んでその中に書いていきます。
引数が無い場合は()だけ書きます。
() -> ・・・ // 引数が無いラムダ式 (int n) -> ・・・ // 引数がint型一つのラムダ式 (int n1, int n2) -> ・・・ // 引数がint型二つのラムダ式 ・ ・ ・ (int... n) -> ・・・ // 可変長引数を使用した場合(これは(int[] n) -> ・・・と書いても良い)
従来のメソッドの引数リストと特に変わりませんね。
さて、最初の方に書いたComparableのサンプルをもう一度見てみましょう。
Collections.sort(list, (n1, n2) -> n1 - n2); // ラムダ式使用
なんと型が書かれていません。
先の説明と反しますが、問題ありません。
というのも、ラムダ式が使用される場面では多くの場合引数や戻り値の型がコンパイル時に決まっていて、型推論できる場合が多いので省略して書いて良いことになっているのです。
型推論を使用せずに最初のサンプルを書くと次のようになります。
Collections.sort(list, (Integer n1, Integer n2) -> n1 - n2);
よって上の引数リストのサンプルは型推論を利用して次のように書けます。
() -> ・・・ (n) -> ・・・ (n1, n2) -> ・・・ ・ ・ ・ (n) -> ・・・ // 可変長引数
さらに引数が一つで型推論を使用する場合は()を外して書いてしまっても良いことになっています。
n -> ・・・ // int n -> ・・・ // これはダメ
ちなみに、型を明記する書き方と型を省略する書き方とを混合することはできません。
(int n, m) -> ・・・ // エラー
引数リストの次は本体の書き方を見ていきましょう。
本体の書き方は3通りあります。
・・・ -> (式) ・・・ -> 式 // 上のカッコ省略した物 ・・・ -> {複文}
これらの違いについて言うと()で囲む書き方や()を省略する書き方をした場合は式一つでなくてはいけません。
また、その式の評価結果がメソッドの戻り値として使われます。
ちなみにreturnを書くとエラーになります。
() -> 42 // 呼び出すと42が返される () -> (42) // 同じ () -> null // 呼び出すとnullが返される () -> (null) // 同じ n -> n * 2 // 呼び出すと引数nを二倍したものが返される n -> (n * 2) () -> System.out.println("HelloWorld") // 戻り値がvoidなメソッド () -> (System.out.println("HelloWorld")) () -> new C() // 呼び出すとCのインスタンスが返される () -> (new C())
これらの書き方では本体の所に書いたものが強制的に戻り値として使用されるので、その戻り値の型が関数型インターフェースのメソッドの戻り値の型に変換出来ない場合はエラーになってしまいます。
例えば上の最後の例(戻り値の型がCと推論される場合)を"HelloWorld"と言った風に書くことはできません。
// 戻り値の型にCが予期される時 () -> "HelloWorld" // エラー(Cのオブジェクトを返さなければならないのにStringのインスタンスを返そうとしてしまっている) //ちなみにこの式は戻り値の型がObjectと予期されている場合は問題ありません。
何もしない文を書くことはできません
() -> // ダメ () -> () // ダメ
また式以外も書けません。
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");} // OK () -> {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; }
{}を使った場合は普通に文が来るので末尾の;を忘れないように注意しましょう。
最後に残ったメソッドジェネリクスですが、2012/02現在未実装な上、特に普通のメソッドジェネリクスと変わり無いように思えるので説明は避けさせてもらいます。
イメージとしては次のようになります。
<T, U extends Clazz> (t, u) -> new Pair<T, U>(t, u)
ラムダ式を使ってみる
さて、ラムダ式の表記法を説明しましたが、実際に使ってみなければ分からないこともあると思うので、使ってみましょう。
まず、関数型インターフェースが無ければラムダ式を適用できないので汎用的な関数型インターフェースを定義しておきましょう。
interface Functional { void invoke(); } interface Functional_int_int { int invoke(int n); } interface Functional0<RET> { RET invoke(); } interface Functional1<RET, ARG1> { RET invoke(ARG1 arg1); } interface Functional2<RET, ARG1, ARG2> { RET invoke(ARG1 arg1, ARG2 arg2); }
Functional3以降の定義は省略します。
では実際に使ってみましょう。やっぱり最初はHelloWorldを出力してみます。
public class Main { public static void main(String[] args) { Functional f = () -> {System.out.println("HelloWorld");}; f.invoke(); } }
どうですか?これでHelloWorldが出力されます。あんまりラムダ式が有用な例では無いですが、まぁ良いでしょう。
呼び出すときは普通にメソッド名を指定して呼び出します。省略は出来ません。
ちなみにこれをラムダ式を使わずに書くと次のようになります。
Functional f = new Functional() { public void invoke() { System.out.println("HelloWorld"); } };
無駄が多くて長いです・・・
次はFunctional0
値を返さないときは戻り値に当たる型引数にVoidを指定します。
Functional0<Void> f = () -> System.out.println("HelloWorld"); f.invoke();
先ほどと同じように書けます。ちなみに、f.invoke()はnullを返します。
さて、他にも色んな書き方が出来ますがこれぐらいにしておいて、メソッドの引数にラムダ式を使ってHelloWorldを出力してみましょう。
public static void method(Functional f) { f.invoke(); } public static void method0(Functional0<Void> f) { f.invoke(); } public static void main(String[] args) { method(() -> System.out.println("HelloWorld")); method0(() -> System.out.println("HelloWorld")); }
全て型推論によってインスタンス化されます。
[追記:ここから〜]
この様にラムダ式によって作成されるインスタンスの型は周辺のコンテキストに基づいて型推論され決定されます。このことをターゲット型付け[TargetTyping]と言い、その型をターゲット型[TargetType]と呼びます。
そして、次の様にターゲット型が曖昧な場合はコンパイルエラーになってしまいます。
public static void method(Functional f) { f.invoke(); } public static void method(Functional0<Void> f) { f.invoke(); } public static void main(String[] args) { method(() -> System.out.println("HelloWorld")); // Functional?Functional0<Void>? }
これを解決するには、キャストを用いて適切なターゲット型付けを促進してあげます。
method((Functional)() -> System.out.println("HelloWorld")); // Functional! method((Functional0<Void>)() -> System.out.println("HelloWorld")); // Functional0<Void>!
[〜ここまで2012/02/17]
では次はintの値を受け取ってそれを倍したものを返してみます。
Functional_int_int fii = n -> n * 2; //Functional_int_int fii = (n) -> n * 2; //Functional_int_int fii = (int n) -> n * 2; // コメントアウトされた書き方でもOK //Functional_int_int fii = n -> {return n * 2;}; // コメントアウトされた書き方でもOK System.out.println(fii.invoke(100)) // 200
ラムダ式の本体に{}を使いたい場合はreturnを使用する必要があるのは前途の通りです、注意してください。
次はジェネリクスなFunctional1を使ってみます。
Functional1<Integer, Integer> fii = n -> n * 2; //Functional1<Integer, Integer> fii = (n) -> n * 2; //Functional1<Integer, Integer> fii = (Integer n) -> n * 2; System.out.println(fii.invoke(100)) // 200
ちなみに
Functional1
は不正です。
そろそろ実用的なことをしたくなってきました。
ラムダ式と言えばforEach(?)、forEachと言えばラムダ式(?)ということで、forEachを実装からやってみましょう。
public <T> void forEach(Iterable<T> iterable, Functional2<Void, ? super T, Boolean> f) { for (Iterator<T> i = iterable.iterator(); i.hasNext();) { f.invoke(i.next(), i.hasNext()); } }
IterableとFunctional2を引数に受け取り各要素について実行していきます。
Functional2#invokeの第一引数には各要素を、第二引数には次の要素がある場合はtrueが渡されます。
この辺りは簡単ですね。
次にこれをラップする形でFunctional1を引数に受け取るforEachを書いてみたいと思います。
Functional1#invokeは各要素だけを受け取り、次の要素があるかどうかは把握しません。
public static <T> void forEach(Iterable<T> iterable, Functional1<Void, ? super T> f) { forEach(iterable, (t, b) -> f.invoke(t)); }
引数fを使用して新しいFunctional2のインスタンスをラムダ式を用いて作成しました。
ラムダ式は匿名クラスを作る糖衣構文なので、アクセスできる範囲も匿名クラスの物と同じ(スコープは違いますが・・・このことはまた後ほど)です。(匿名クラスからはfinal修飾されていないローカル変数にはアクセスできない)
ですが、final修飾されていないfにアクセスしています。
実は、言語拡張によって実質的にfinalとして働くローカル変数にはfinal修飾されていなくてもアクセスできるようになりました。
これはラムダ式に対してだけなので、匿名クラス内からローカル変数にアクセスする場合は依然としてfinal修飾されていないといけません。[削除:2012/02/18]
これはラムダ式だけでなく匿名クラスやローカルクラスからアクセスする場合も同様です。
では実際にこれらを使ってみましょう。
import java.util.*; public class Main { public static <T> void forEach(Iterable<T> iterable, Functional2<Void, ? super T, Boolean> f) { for (Iterator<T> i = iterable.iterator(); i.hasNext;) { f.invoke(i.next(), i.hasNext()); } } public static <T> void forEach(Iterable<T> iterable, Functional1<Void, ? super T> f) { Main.forEach(iterable, (t, b) -> f.invoke(t)); } public static void main(String[] args) { Main.forEach(Arrays.asList(args), s -> System.out.println(s)); // Functional1の方を使用して引数を出力する。 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); }); }); // 九九の表を出力する。 } }
さて、余りに長くなってしまいました。
今回のラムダ式の紹介はこれぐらいにしておきましょう。
関数型インターフェース
以下はオマケです。
どういったものが関数型インターフェースになって、どういったものがならないのか、ドラフトを参考にして説明しています。
さりげなくかなり長いです。
compareのほかにequalsなどのメソッドを持っているComparatorが関数型インターフェースになるのかの理由もここに書かれています。
interface X { public void method(); }
これこそまさしく典型的な関数型インターフェースです。実に簡単ですね。
従来どうりジェネリクスを使っても関数型インターフェースになりえます。
interface X<T> { public <S> void method(T t, S s); }
throwsを使用していても同じで問題ありません。
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 {}
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の二つが宣言されたことになるので関数型インターフェースではありません。
interface X { public Object clone(); }
これで関数型インタフェースです。
インターフェースで宣言可能で、暗黙的に宣言されるObjectクラスのメソッドは次の通りです。
boolean equals(Object) int hashCode() String toString()
これらはたとえ宣言されていても抽象メソッドの頭数としては数えられません。
ここまでは独立で宣言されたインターフェースについて見てきました。
次は複数のインターフェースを継承した場合どのようになるか見てみましょう。
まずは一番簡単な例です。
interface X { public void method(Object obj); } interface Y { public void method(Object obj); } interface Z extends X, Y {}
ZのmethodとYのmethodは同じなのでZ内では一つになりZは関数型インタフェースです。(言語仕様9.4.1)
interface X { public void method(Object obj); } interface X { public void method(String str); } interface Z extends X, Y {}
オーバーロードされて抽象メソッドを二つ持つのでZは関数型インターフェースではありません。
interface X { public int method(); } interface Y { public long method(); } interface Z extends X, Y {}
これは関数型インターフェース以前の問題でコンパイルが通りません。(共変戻り値を使用できないため)
次にジェネリクスを使用した物です
interface X { public Iterable method(Iterable<String> arg); } interface Y { public Iterable<String> method(Iterable arg); } interface Z extends X, Y {}
XのmethodとYのmethodとで同一のシグネチャになるのでZは関数型インターフェースになります。
interface X { public int method(Iterable<String> arg); } 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> {} interface Z<Integer, Integer> {}
Xは型イレイジャされるとmethodが二つあることになり関数型インターフェースではありません。
Yも同様に関数型インターフェースではありません。
Zは型イレイジャされるとmethodが一つにまとめられるので関数型インターフェースになります。
他に、「抽象メソッドが一つだけであれば、デフォルト実装が使用されているメソッドがあっても関数型インターフェースになる」という物がありますが、これは「デフォルト実装」の説明後に書いておきます。
2012/02/17:追記:ターゲット型付け、ターゲット型に言及
2012/02/18:修正:匿名クラスなどからの実質的にfinalな値へのアクセスについて修正
2012/03/03:修正:関数型インターフェースの説明内の頭数に数えられないObjectのメソッドの一覧からインターフェース内で定義できない物(final修飾されたもの)の除去