Java Magazine2013年9月号からの転載。講読申込み受付中。 |
Ted Neward著
Java 8のラムダ式について知る
利用中のプログラミング言語やプラットフォームの新リリースほど、ソフトウェア開発者コミュニティが待ち望むものはそうありません。Java開発者も例外ではありません。むしろ、Java開発者のほうが新リリースに期待しているかもしれません。なにしろ、Javaを生み出したSunと同様にJavaも衰退していくように思われた時代も、そう遠くない昔にあったのですから。一度終わりかけて再起したのですから、いっそう大事に思えるものです。しかしJavaの場合、新リリースへの熱狂の理由はそれだけではありません。前リリースでは実現されず、Java開発者の多くが(数十年とは言わないまでも)長年求め続けてきた「最新型の」言語機能が、Java 8でようやく新たに実現されるのです。
言うまでもなく、この熱狂を呼ぶJava 8の言語機能こそ、この全2回シリーズのテーマでもある、ラムダ式(別名:クロージャ)です。言語機能というものは、ある程度のサポートが背後になければ、その便利さや面白さが分からないこともよくあります。Java 7のいくつかの機能はまさにこの表現に当てはまります。たとえば、数値リテラルの機能拡張は、実際にはほとんどの開発者にとって魅力的に映りませんでした。
しかし、Java 8の関数リテラルでは、Java言語の中核が変更されるばかりでなく、関数リテラルを使いやすくするための言語機能もいくつか追加されます。さらに、関数リテラルの機能を直接利用するために、ライブラリも一部改良されます。これらの変更によって、Javaでの開発が容易になります。
Java Magazineでは以前にもラムダ式に関する記事を掲載しています。しかし、執筆以降に構文やセマンティクスが変更された可能性があり、また、過去の記事に目を通す時間のない方のためにも、本記事では読者がラムダ式の構文についてまったく知らないと仮定しています。
注:本記事は、Java SE 8プレリリース版に基づき説明します。そのため、本記事には、最終リリースの公開時点には正確ではなくなる内容が含まれる可能性があります。構文およびセマンティクスは、最終リリースまでに変更されることがあります。
ラムダ式について、公式の詳細な説明が必要な場合は、Brian Goetzの論文(State of the Lambda:Libraries Edition)や、その他のProject Lambdaホームページで公開されている文献が貴重な参考資料となります。
Javaでの関数オブジェクト(ファンクタとも呼ばれます)の使用については、Javaコミュニティでは有用でも必要でもないと強く主張されていましたが、実際には必要とされる場面が常にありました。初期のJavaでは、GUIの構築時に、ウィンドウを開く/閉じる、ボタンを押す、スクロールバーを動かすといったユーザー・イベントに応答するために、何ブロックものコードを記述する必要がありました。
Java 1.0のAbstract Window Toolkit(AWT)アプリケーションは、C++にすでにあった機能と同様に、ウィンドウ関連のクラスを拡張して、選択したイベント・メソッドをオーバーライドするという方式でした。しかし、この方式は扱いづらく実用性に欠けると判断されました。そのため、SunはJava 1.1で、一連の「リスナー」と呼ばれるインタフェースを導入しました。各リスナーには1つ以上のメソッドがあり、メソッドのそれぞれがGUI内部の1つのイベントに対応します。
CODE = OBJECT
Javaが進化し成熟するにつれ、一連のコード・ブロックをオブジェクト(実際はデータ)として扱うことが、単に便利なだけではなく必要にもなる分野が増えました。
また、リスナー・インタフェースやそれらに対応するメソッドを実装するクラスを記述しやすくするため、Sunはインナー・クラスを導入しました。インナー・クラスでは、クラス名を指定しなくても、既存クラスのボディ内部に実装クラスを記述できます。至る所で見かけるこのようなインナー・クラスは、匿名インナー・クラスと呼ばれます(なお、リスナーはJavaの歴史で登場する数々の要素のほんの一例に過ぎません。後ほど確認しますが、リスナーと同じく「コア」インタフェースには、Runnable
やComparator
などもあります)。
インナー・クラスは、構文とセマンティクスの両面において、少し奇妙なところがありました。たとえば、インナー・クラスが静的インナー・クラスであるかインスタンス・インナー・クラスであるかは、特定のキーワードの有無ではなく、インスタンス作成時のレキシカル・コンテキストによって判断されました(ただし、静的インナー・クラスは、staticキーワードを使用して明示的に指定されることもあります)。実際の問題として、採用試験などのプログラミング課題でリスト1のようなコードが取り上げられた際に多くの誤答が見られました。
リスト1
class InstanceOuter { public InstanceOuter(int xx) { x = xx; } private int x; class InstanceInner { public void printSomething() { System.out.println("The value of x in my outer is " + x); } } } class StaticOuter { private static int x = 24; static class StaticInner { public void printSomething() { System.out.println("The value of x in my outer is " + x); } } } public class InnerClassExamples { public static void main(String... args) { InstanceOuter io = new InstanceOuter(12); // これはコンパイル・エラーになりますか? InstanceOuter.InstanceInner ii = io.new InstanceInner(); // 次の文で何が出力されますか? ii.printSomething(); // 12と出力されます // 次の文ではどうですか? StaticOuter.StaticInner si = new StaticOuter.StaticInner(); si.printSomething(); // 24と出力されます } }
Java開発者はもっぱら、インナー・クラスのような「機能」はせいぜいプログラミング課題で扱うくらいで、一般的な状況では使用しない言語機能なのだと確信していました。必要になるときもありますが、その場合でもたいていは、イベント処理の目的でのみ使用しました。
構文やセマンティクスは不格好でも、システムは機能しました。Javaが進化し成熟するにつれ、一連のコード・ブロックをオブジェクト(実際はデータ)として扱うことが、単に便利なだけではなく必要にもなる分野が増えました。Java SE 1.2で改良されたセキュリティ・システムでは、異なるセキュリティ条件下で実行させるコード・ブロックを渡すという方法が便利だと判断されました。同じくJava SE 1.2に搭載された改良版のCollectionクラスは、ソート済みコレクションに対してソート順を指定する方法を把握するために、特定のコード・ブロックを渡す方法が便利だと判断されました。また、Swingでも、「ファイルを開く」ダイアログ・ボックスや「ファイルを保存」ダイアログ・ボックスで表示するファイルを指定するために、特定のコード・ブロックを渡す方法が便利だと判断されました。同様の事例は多数見られました。記述した本人しか好まないような分かりづらい構文がしばしば使用されたものの、こうしたコード自体は機能していました。
しかし、関数型プログラミングの考え方がプログラミングの主流になり始めたときには、ほとんどのJava開発者が諦めました。関数型プログラミングをJavaで実現すること自体は可能でしたが(こちらに格好の事例があります)、Javaで記述する関数型プログラミングは、何といっても冴えない扱いにくいものでした。多数ある主流のプログラミング言語では、コード・ブロックの定義や受渡しの機能、および後で実行できるようにコード・ブロックを保管できる機能が言語側で十分にサポートされています。Javaもさらなる進化を遂げ、そのような主流の言語に加わる必要がありました。
Java 8では、前述のようなコード・ブロックを記述しやすくするための複数の言語機能が導入されます。その中でも重要な機能はラムダ式です。ラムダ式は、くだけた表現としてクロージャや匿名メソッドと呼ばれることもあります(クロージャと呼ばれる理由については後ほど説明します)。それでは、これらの点について1つずつ説明していきます。
ラムダ式:ラムダ式は基本的には、後で実行するメソッドの実装を簡潔に記述するための手法に過ぎません。たとえば、以前はリスト2に示すようにRunnableを定義しました。リスト2では匿名インナー・クラス構文を使用していますが、基本的な概念を表現するコード行が過度に多くなるという「縦の問題(vertical problem)」がはっきりと現れています。一方、Java 8のラムダ式構文では、同じ内容のコードをリスト3のように記述できます。
リスト2
public class Lambdas { public static void main(String... args) { Runnable r = new Runnable() { public void run() { System.out.println("Howdy, world!"); } }; r.run(); } }
リスト3
public static void main(String... args) { Runnable r2 = () -> System.out.println("Howdy, world!"); r2.run(); }
いずれのコードも結果は同じで、Runnable実装オブジェクトのrun()メソッドが呼び出され、コンソールに何らかの文字列が出力されます。しかし、内部的には、Java 8で記述したリスト3のコードではRunnableインタフェースを実装する匿名クラスを生成する以外にも、少し処理が加えられます。その処理の一部は、Java 7で導入されたinvoke dynamicバイトコードに関係します。本記事では詳細には説明しませんが、ラムダ式構文の結果、単なる匿名クラスのインスタンス以上のものができると覚えておきましょう。
関数型インタフェース:Runnableインタフェースは、Callable<T>インタフェース、Comparator<T>インタフェース、およびJava内部ですでに定義されているその他の多数のインタフェースと同様に、Java 8で関数型インタフェースと呼ばれるものです。関数型インタフェースの条件として、実装されるメソッドが1つだけ存在する必要があります。関数型インタフェースにより、構文が簡潔になります。インタフェースのどのメソッドをラムダ式で定義しようとしているかが曖昧ではなくなるためです。
Java 8の設計者は、@FunctionalInterfaceというアノテーションを提供することを決定しています。このアノテーションは、インタフェースがラムダ式で使用されることを単に明示するためのものであり、コンパイラでは必要とされません。つまり、コンパイル時には、アノテーションではなくインタフェースの構造から、そのインタフェースが「関数型インタフェースであること」が判断されます。
本記事ではこれ以降も、RunnableインタフェースとComparator<T>インタフェースを実際の例として利用しますが、これらのインタフェースには、「メソッドが1つのみである」という関数型インタフェースの制約に従っている点を除き、特別なところはありません。開発者は、以下のインタフェースのように、ラムダ式のインタフェース・ターゲット型となる新しい関数型インタフェースをいつでも定義できます。
interface Something { public String doit(Integer i); }
このSomethingインタフェースは、RunnableやComparator<T>とまったく同様に正当かつ適切な関数型インタフェースです。このインタフェースについては、ラムダ式のいくつかの構文を本記事で習得した後に、再度取り上げます。
構文:Javaにおけるラムダ式は、基本的には3つの部分で成り立っています。1つ目は括弧で囲まれた一連のパラメータ、2つ目は矢印、3つ目はボディです。ボディには、1つの式か1つのJavaコード・ブロックのいずれかを記述できます。リスト2の例では、runにパラメータは指定されておらず、戻り型はvoidです。したがって、パラメータも戻り値もありません。一方、Comparator<T>を使用した例では、ラムダ式の構文がやや明確になります(リスト4)。Comparatorが2つの文字列を引数として受け取り、整数を返すこと、およびその戻り値は負数(「~未満」の場合)、正数(「~より大きい」場合)、0(「~と等しい」場合)のいずれかとなることを覚えておいてください。
リスト4
public static void main(String... args) { Comparator<String> c = (String lhs, String rhs) -> lhs.compareTo(rhs); int result = c.compare("Hello", "World"); }
ラムダ式のボディに複数の式を指定する必要がある場合は、通常のJavaコード・ブロックと同様に、returnキーワードで戻り値として式を返すことができます(リスト5)。
リスト5
public static void main(String... args) { Comparator<String> c = (String lhs, String rhs) -> { System.out.println("I am comparing" + lhs + " to " + rhs); return lhs.compareTo(rhs); }; int result = c.compare("Hello", "World"); }
なお、リスト5にもある波括弧をコード内のどの場所に置くかは、Javaの各種掲示板やブログで今後数年にわたって議論されるでしょう。ラムダ式のボディ内で実行可能な処理についてはいくつかの制約がありますが、そのほとんどは非常に直感的なものです。ラムダ式のボディにはラムダ式の外側に抜ける「break」や「continue」は記述できない、あるいは、ラムダ式が値を返す場合はすべてのコード・パスで値を返すか例外をスローする必要がある、などの制約があります。これらの制約は標準的なJavaメソッドとほぼ同じルールであり、それほど意外なものはありません。
型推論:Java以外の一部の言語でこれまでに評価の高かった機能の1つが、型推論です。これは、開発者がパラメータの型付けを何度も行うのではなく、コンパイラがパラメータの型を推論できるほどスマートであるべきだという考え方です。
たとえば、リスト5のComparatorを考えてみます。ターゲットの型がComparator<String>の場合、このラムダ式に渡されるオブジェクトは必ず文字列(または文字列の何らかのサブタイプ)になります。文字列でない場合、このコードはそもそもコンパイルされません(なお、この考え方自体は新しいものではなく、「継承の基礎」と言えます)。
この場合、lhsとrhsの各パラメータの前にあるString宣言は完全に冗長です。また、Java 8では型推論機能が強化されているため、型を宣言するかどうかはまったくの任意になります(リスト6)。
リスト6
public static void main(String... args) { Comparator<String> c = (lhs, rhs) -> { System.out.println("I am comparing" + lhs + " to " + rhs); return lhs.compareTo(rhs); }; int result = c.compare("Hello", "World"); }
言語仕様には、ラムダ式の正式な型を明示的に宣言することが必要になるケースについて、明確なルールが定められる予定です。ただし、ほとんどの場合に、例外的なケースではなくデフォルトとして、ラムダ式に対するパラメータの型の宣言を完全に省略できるようになります。
Javaのラムダ式構文による興味深い副作用の1つとして、補助情報が何もない場合はObject型の参照に代入できないというケースが、Javaの歴史の中で初めて登場します(リスト7)。
リスト7
public static void main4(String... args) { Object o = () -> System.out.println("Howdy, world!"); // コンパイルできません }
リスト7では、コンパイラにより、Objectが関数型インタフェースではないというエラーが示されます。ただし、実際の問題は、このラムダ式で実装すべき関数型インタフェースがどれであるのか(Runnableなのか別のものなのか)をコンパイラでは完全には推論できないことです。この場合は、リスト8のように、使い慣れたキャストを使用することで、補助情報をコンパイラに示すことができます。
リスト8
public static void main4(String... args) { Object o = (Runnable) () -> System.out.println("Howdy, world!"); // 今度はコンパイルできます }
前述のとおり、ラムダ式はあらゆるインタフェースで使用できるため、カスタム・インタフェースへと推論されるラムダ式も、同じように容易に推論されます(リスト9)。ところで、ラムダ式の型シグネチャにおいて、プリミティブ型は、それぞれに対応するラッパー型と同様に有効です。
リスト9
Something s = (Integer i) -> { return i.toString(); }; System.out.println(s.doit(4));
この型シグネチャでプリミティブ型を使用できる点についても新しい考え方ではありません。Java 8では、Javaで長期間維持されてきた原則、パターン、構文が新機能にもそのまま適用されます。この点についてまだご存じでなければ、少しの時間をかけてコード内の型インタフェースについて調べることで、理解が深まります。
レキシカル・スコープ:一方で、新しい考え方も1つあります。ラムダ式のボディ内でコンパイラが名前(識別子)を取り扱う方法が、インナー・クラスでの取り扱い方法とは異なります。ここで、リスト10のインナー・クラスの例を考えます。
リスト10
class Hello { public Runnable r = new Runnable() { public void run() { System.out.println(this); System.out.println(toString()); } }; public String toString() { return "Hello's custom toString()"; } } public class InnerClassExamples { public static void main(String... args) { Hello h = new Hello(); h.r.run(); } }
リスト10のコードを実行すると、筆者のマシンでは「Hello$1@f7ce53」という直感的ではない文字列が出力されます。この理由は単純で、匿名のRunnableの実装内では、thisキーワードとtoStringの呼出しの両方が、式を満たすもっとも内側のスコープである匿名インナー・クラスの実装にバインドされるためです。
この例では、HelloクラスのtoStringの出力が得られるように見えますが、そうするためには、Java仕様のインナー・クラスの説明にある「アウター・クラスのthis」構文を使用して、リスト11のように明示的に修飾する必要があります。それでは、「アウター・クラスのthis」のような構文は直感的でしょうか。
リスト11
class Hello { public Runnable r = new Runnable() { public void run() { System.out.println(Hello.this); System.out.println(Hello.this.toString()); } }; public String toString() { return "Hello's custom toString()"; } }
率直に言って、この分かりにくさはインナー・クラスがその有用性を発揮できず、混乱を招くだけとなった部分の1つです。確かに、thisキーワードがこのようにむしろ直感的ではない構文となっている背景には、ある程度は筋の通った理由があります。しかしそれは、政治家が特権を持つことは筋が通っていると言うようなものです。
一方、ラムダ式にはレキシカル・スコープが適用されます。つまり、ラムダ式の定義の周囲にある環境が、自身の1つ外側のスコープであると認識されます。そのため、リスト12のラムダ式の例は、2つ目のHelloネスト・クラスの例(リスト11)と同じ結果になりますが、はるかに直感的な構文になっています。
リスト12
class Hello { public Runnable r = () -> { System.out.println(this); System.out.println(toString()); }; public String toString() { return "Hello's custom toString()"; } }
なお、この結果から分かるように、thisはラムダ式そのものを参照するわけではありません。このことが重要になる場合もありますが、そのような状況はごくまれです。さらに、そのような状況(ラムダ式でそのラムダ式自身を返したい場合など)になっても、比較的簡単に使用できる方法が別にあります。この方法については、後ほど説明します。
変数のキャプチャ:ラムダ式がクロージャと呼ばれる理由の1つは次のとおりです。関数リテラル(前項までに紹介したような形式の関数)は、その関数リテラルを取り囲むスコープ(エンクロージング・スコープ)内にある(かつ、その関数リテラルの外側にある)変数の参照を「閉じ込める(close over)」ことができます。Javaの場合、エンクロージング・スコープは通常、ラムダ式が定義されているメソッドとなります。インナー・クラスでもこのような構造は可能です。しかし、インナー・クラスの場合、エンクロージング・スコープからは「final」の変数しか参照できないという制約がありました。これは、これまでJava開発者の頭を悩ませてきたインナー・クラスに関する数々のテーマのうちでも、特に重要なものです。
ラムダ式によってこの制約が緩和されます。ただし、その程度は少しだけです。変数の参照が「事実上final」である場合、つまりfinal修飾子がなくとも変数の内容が不変である場合、ラムダ式はその変数を参照できます(リスト13)。messageは、ラムダ式が定義された場所を取り囲むmainメソッドのスコープ内において変更されないため、事実上finalです。そのため、rに格納されるRunnableラムダ式の内部から参照できます。
リスト13
public static void main(String... args) { String message = "Howdy, world!"; Runnable r = () -> System.out.println(message); r.run(); }
表面的には何でもないように思えることですが、ラムダ式のセマンティクスのルールは、Javaの全体的な性質を変えるものではないことを覚えておいてください。すなわち、ラムダ式を定義した場所と参照先のオブジェクトが離れていても、オブジェクトにアクセスでき、変更できます(リスト14)。
リスト14
public static void main(String... args) { StringBuilder message = new StringBuilder(); Runnable r = () -> System.out.println(message); message.append("Howdy, "); message.append("world!"); r.run(); }
従来のインナー・クラスの構文やセマンティクスに精通している鋭い開発者は、インナー・クラス内で参照され、「final」宣言された参照にも同じことが当てはまることを思い出すでしょう(final修飾子が、参照先のオブジェクトではなく、参照のみに適用されている場合)。Javaコミュニティの視点から判断して、このことがバグであるか機能の1つであるかは今後に委ねられますが、現状はこれまで説明したとおりです。予想外のバグが発生しないように、開発者はラムダ式の変数のキャプチャ方法について理解しておくのが賢明でしょう(実は、この振る舞いも新しいものではなく、コード量を少なくしコンパイラでの処理を増やすことで、既存のJava機能を作り直しているだけです)。
メソッド参照:前項までに説明したラムダ式はすべて匿名リテラルであり、ラムダ式の定義は使用する時点で行いました。1回限りの動作においてはこの方法で十分良いのですが、複数の場所で同じ振る舞いを必要とする場合は、あまり上等な手段ではありません。たとえば、次のPersonクラスを考えます(今のところ、カプセル化が不適切である点については気にしないでください)。
class Person { public String firstName; public String lastName; public int age; });
PersonをSortedSetに格納する場合や、何らかの形式のリスト内でソートする必要がある場合、Personインスタンスをソートするための異なる複数のメカニズムが必要になります。たとえば姓でソートする場合、名でソートする場合があります。この目的でComparator<T>を使用します。Comparator<T>インスタンスを渡すことで、適用するソート方法を定義できるのです。
スコープを調べる
ラムダ式にはレキシカル・スコープが適用されます。つまり、ラムダ式の定義の周囲にある環境が、自身の1つ外側のスコープであると認識されます。
ラムダ式を使用すると、確かにソート用のコードが記述しやすくなります(リスト15)。しかし、名を基準としたPersonインスタンスのソートは、コードベース内で何度も実行する可能性があります。この種のアルゴリズムを何度も記述することは、明らかにDRY(Don't Repeat Yourself、「重複の排除」)原則に違反します。
リスト15
public static void main(String... args) { Person[] people = new Person[] { new Person("Ted", "Neward", 41), new Person("Charlotte", "Neward", 41), new Person("Michael", "Neward", 19), new Person("Matthew", "Neward", 13) }; // 名でソート Arrays.sort(people, (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName)); for (Person p : people) System.out.println(p); }
ComparatorをPerson自体のメンバーとして取り込むことは確かに可能です(リスト16)。そうすれば、Comparator<T>も他の静的フィールドと同じように参照できるようになります(リスト17)。正直に言えば、関数型プログラミングの熱心な支持者であれば、このようなスタイルを好むでしょう。関数をさまざまな方法で組み合わせることができるからです。
リスト16
class Person { public String firstName; public String lastName; public int age; public final static Comparator<Person> compareFirstName = (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName); public final static Comparator<Person> compareLastName = (lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName); public Person(String f, String l, int a) { firstName = f; lastName = l; age = a; } public String toString() { return "[Person: firstName:"+ firstName + " " + "lastName:"+ lastName + " " + "age:"+ age + "]"; } }
リスト17
public static void main(String... args) { Person[] people = ...; // 名でソート Arrays.sort(people, Person.compareFirstName); for (Person p : people) System.out.println(p); }
しかし、このようなスタイルは、従来のJava開発者にとっては違和感があります。従来は、Comparator<T>のシグネチャに合うメソッドを作成し、そのメソッドを直接使用するだけでした。この方法こそ、メソッド参照で実現できることです(リスト18)。コロン2つを使用してメソッド名を指定するスタイルに注目してください。このスタイルでは、コンパイラに対して、メソッド・リテラルではなく、Personに定義されたcompareFirstNamesメソッドをこの場所で使用することを指示しています。
リスト18
class Person { public String firstName; public String lastName; public int age; public static int compareFirstNames(Person lhs, Person rhs) { return lhs.firstName.compareTo(rhs.firstName); } // ... } public static void main(String... args) { Person[] people = ...; // 名でソート Arrays.sort(people, Person::compareFirstNames); for (Person p : people) System.out.println(p); }
好奇心の強い読者のために、別の方法も紹介しておきます。それは、次のようにcompareFirstNamesメソッドを使用してComparator<Person>インスタンスを作成する方法です。
Comparatorcf = Person::compareFirstNames;
さらに、もっと簡潔に記述するために、新しいライブラリ機能の一部を利用して次のように記述し、構文上のオーバーヘッドの一部を完全になくすこともできます。この方法は、高次の関数(大まかに言えば、関数を順に渡す関数)を利用して、前のコード全体を事実上なくし、次のように1行で記述してメモリをほぼ消費しない形にしたものです。
Arrays.sort(people, comparing( Person::getFirstName));
この例は、ラムダ式や、ラムダ式に伴う関数型プログラミングの各種技術が非常に強力であることを示す好例です。
仮想拡張メソッド:しかし、インタフェースには、よく言われる欠点もあります。その1つは、その実装が疑いようもなく明白な場合でも、デフォルトの実装を記述できないことです。たとえば、架空のRelationalインタフェースを考えます。このインタフェースは、比較メソッド(「~より大きい」、「~未満」、「~以上」など)に似た一連のメソッドを定義するものです。これらのメソッドのいずれか1つを定義すれば、その1つ目を参考にして他のメソッドの定義方法も簡単に把握できます。実際、すべてのメソッドはComparable<T>のcompareメソッドを参考にして定義できます(compareメソッドの定義内容が前もって分かっている場合)。しかし、インタフェースにはデフォルトの振る舞いを記述できません。また、抽象クラスはクラスの一種で、潜在的なサブクラスの実装継承スロットを1つ占有します。
しかし、Java 8では、前述の関数リテラルが広範に使用されるようになったため、インタフェースの「インタフェースらしさ」を損なうことなくデフォルトの振る舞いを指定できることの重要性が高まっています。そのため、Java 8では仮想拡張メソッドが導入されます(以前の草案ではdefenderメソッドと呼ばれていたものです)。仮想拡張メソッドは基本的に、実装クラスでメソッドの実装が記述されない場合に、インタフェース側でそのメソッドのデフォルトの振る舞いを指定できるようにするものです。
ここで少し、Iteratorインタフェースを考えてみます。現在、3つのメソッド(hasNext、next、remove)があり、各メソッドには定義が必要です。しかし、一連の反復処理において次(next)のオブジェクトを「スキップ」できる機能があれば便利でしょう。さらに、Iteratorの実装は、上記3つのメソッドを参考にして容易に定義できるため、リスト19のように実装できます。
リスト19interface Iterator<T> { boolean hasNext(); T next(); void remove(); void skip(int i) default { for (; i > 0 && hasNext(); i--) next(); } }
Javaコミュニティの参加者の中には、仮想拡張メソッドは、インタフェースの宣言性を弱め、Javaで多重継承を許す図式を作り出してしまうメカニズムに過ぎないと嘆く人もいます。この懸念はある程度は的を射ています。特に、(クラスが複数のインタフェースを実装し、それらのインタフェースに同じ名前のメソッドがあり、それらのメソッドのデフォルト実装が異なっている場合における)デフォルト実装の優先順位に関するルールについて、今後念入りに調査する必要があるためです。
Learn More |
しかし、仮想拡張メソッドは名前のとおり、既存のインタフェースを拡張するための強力なメカニズムであり、拡張されたメソッドをある種の二流の機能に下げるものではありません。このメカニズムを使用すれば、たとえばオラクルが、開発者に異なる種類のクラスの追跡作業を求めることなく、既存のライブラリに対して強力な振る舞いを追加できます。開発者がSkippingIteratorクラスをサポートするコレクションを使用するために、ダウンキャストを指定する必要はありません。実際、変更しなければならないコードは存在せず、いかなる時期にコーディングされたIterator<T>にも、自動的にこのスキップの振る舞いが追加されます。
Collectionクラス内で行われる変更の大部分は、仮想拡張メソッドによってもたらされます。既存のCollectionクラスにも新しい振る舞いを追加でき、しかもそのためにコードをまったく変更する必要がないというのは朗報でしょう。ただし残念ですが、この話題の詳細については、このシリーズの次回の記事までお待ちください。
ラムダ式は、Javaコードの記述方法と設計方法の両方に多くの変化をもたらします。これらの変化の一部は、関数型プログラミング言語を参考としており、コーディングに対するJavaプログラマの考え方を変えるものです。この変化はチャンスでもあり、面倒な点でもあります。
このシリーズの次回の記事では、ラムダ式がもたらす変化がJavaライブラリに与える影響について詳細に説明します。また、新しいAPI、インタフェース、クラスの導入によって、新しい設計アプローチがどのように切り開かれるのかについても少し説明します。インナー・クラス構文が扱いにくいために、以前では実践できなかった設計アプローチです。
Java 8は本当に面白いリリースになりそうです。さあ、覚悟を決めて、新たな世界に飛び立ちましょう。
Ted Neward(@tedneward):Neudesicのアーキテクチャ・コンサルタント。複数の専門家グループに貢献。Effective Enterprise Java(Addison-Wesley Professional、2004年)、Professional F# 2.0(Wrox、2010年)などの著書多数。Java、Scalaをはじめとするテクノロジー記事の掲載やカンファレンスでの講演も多い。