新しい制御構文の定義例

Javaのクロージャに関するJSRのドラフトには、クロージャを導入することのメリットとして、新しい制御構文を独自に定義し、JavaのAPIを拡張できるということが挙げられている。前回は、Control invocation syntaxを利用して定義した新しい制御構文の例として、任意のロック機構で排他処理を行うwithLock文を紹介した。

同様の例としてよく挙げられるのが、ストリームの処理に利用するwith文だ。with文では引数にストリームオブジェクトを受け取り、処理が終了した際に必ずそのストリームを閉じるという制御構文である。

具体的には、リスト1のような定義になる(プロトタイプ仕様より引用)。ここでは、withメソッドはjava.io.Closeableオブジェクトを受け取り、それをクロージャのinvoke()に渡すように実装されている。そして処理が終了したらclose()メソッドを実行してストリームを閉じる。前回のwithLockの例と異なるのは、クロージャ自身も引数を受け取るという点だ。

リスト1 with文の定義例

    public static  R 
            with(T t, { T ==> R throws E } block) throws E {
        try {
            return block.invoke(t);
        } finally { 
            try { 
                t.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

このwith文を利用してストリームを処理している例がリスト2およびリスト3である。最初の例ではBufferedReaderを渡し、最初の1行を読み込む処理を行っている。クロージャ自身も引数としてCloseableオブジェクトを受け取るので、それを":"を用いた文法で指定している点に注意してほしい。次の例ではPrintWriterを渡し、文字を出力している。withの引数はClosableで定義されているので、一般的なストリームやネットワークチャネルはすべて同様に処理することができる。

リスト2 withを使ったストリームの処理

        try {
            String text = "";
            BufferedReader reader = 
                new BufferedReader(new FileReader("in.txt"));

            with(BufferedReader in : reader) {
                text = in.readLine();
            }
        } catch(IOException ex) {
            ex.printStackTrace();
        }

リスト3 withを使ったストリームの処理 その2

        try {
            PrintWriter writer = 
                new PrintWriter(new FileWriter("out.txt"));

            with(PrintWriter out : writer) {
                out.println("Hello!");
            }
         } catch(IOException ex) {
            ex.printStackTrace();
         }

ストリームを利用する処理では、最後にclose()メソッドを呼び出すケースが非常に多い。この例のように一度with文を定義しておけば、毎回close()を記述する必要がなくなるため、コードを簡潔にすることができるだろう。

"for"修飾子を用いた繰り返し構文の定義

BGGA版の仕様では、メソッドの修飾子として新たに"for"キーワードが使えるようになっている。"for"キーワードは、通常のメソッド修飾子と同様に戻り値の型の前に付けて使用する(※)。

※現行の仕様では識別子の直前に付けるように記述されているが、後に修飾子のように扱うよう変更された。プロトタイプ実装にはこれが反映されているが、仕様には次のアップデートで反映するとのこと。

もしクロージャを利用して繰り返し処理を行う制御構文を定義する場合には、メソッドにfor修飾子を付加する必要がある。前回の記事でUnrestricted closureではbreakやcontinueが使用できると書いたが、正確にはfor修飾子を付けたメソッドに対するUnrestricted closureがその対象となる。すなわち、forで修飾されたメソッドに渡すUnrestricted closureの処理においてcontinueが実行された場合には、処理は次の繰り返しへと映り、breakの場合には制御構文を抜ける。

たとえばリスト4のような例が考えられる。eachIndexは配列をひとつ受け取り、その要素それぞれに対して渡されたクロージャの処理を実行する。

リスト4 新しい繰り返し構文の定義例

    public static for  void 
            eachIndex(T[] array, { T ==> void } block) {
        for (T t : array) {
            block.invoke(t);
        }
    }

呼び出し側はリスト5のようになる。識別子の前に"dor"キーワードを付ける点が通常の呼び出しと異なる。処理は文字列配列の要素を表示するという単純なものだが、もしそれが"NOTHING"だった場合はcontinueで無視するようになっている。eachIndexはforで修飾されているため、このcontuinueによって処理は次の要素へと移されることになる。もしbreakならばeachIndexの処理を終了し、returnならばエンクロージャの処理そのものを終了することになる。

リスト5 eachIndex文の利用例

       String[] fruits = {"apple", "orange", "NOTHING", "blueberry"};

       for eachIndex(String item : fruits) {
           if(item.equals("NOTHING")) continue;
           System.out.println(item);
       }

上記は配列に対する例だが、同様の構文をjava.util.Mapに対して定義する場合、リスト6のように記述できる(プロトタイプ仕様より引用)。これは渡されたMapオブジェクトの各エントリをそれぞれ1つずつ処理するというもの。クロージャには各エントリのキーと値が渡される。この際、getKey()およびgetValue()の実行時にIlligalStateExceptionがスローされる可能性があるため、eachEntryの定義にはthrows宣言が含まれる。

リスト6 Mapに対する繰り返し構文の定義例

    public static for  void 
            eachEntry(Map map, { K, V ==> void throws X } block)
                throws X {
        for (Map.Entry entry : map.entrySet()) {
            block.invoke(entry.getKey(), entry.getValue());
        }
    }

呼び出し側はリスト7のようなコードになる。クロージャに渡されるのはMapオブジェクトのキーと値なので、eachEntryの呼び出し時にそれぞれの型を指定している。

リスト7 eachEntry文の利用例

       Map map = new HashMap();
       map.put("java.lang", 43);
       map.put("java.io", 63);
       map.put("org.xml.sax", 13);
       map.put("javax.sql", 17);

       for eachEntry(String name, Integer value : map) {
           if (name.startsWith("org.")) continue;
           System.out.println(name + ":" + value);
       }

J2SE 5.0のリリース時に、for-each風に利用できるfor文のための新しい構文が導入された。for修飾子を用いることで、繰り返し処理のための同様のシンタックスシュガーが独自に定義できるというわけだ。

まとめ

Java SE 7ではクロージャも含めていくつかの画期的な機能の導入が予定されている。それらを使いこなせるようになれば開発者にとって非常に強力な武器になるだろう。しかし残念ながら、すぐ使えと言われて使いこなせるほど単純なものでもない。J2SE 5.0で導入されたジェネリクスにしても、未だに戸惑ってしまうユーザーは少なくないだろう。

そのように新機能で挫折しないためには、リリース前から情報を集めてある程度の予備知識を蓄えておくことが重要だ。幸いにして現在のJavaはオープンな開発体制が敷かれており、リリース前でも大半の情報は手に入れることができる。参照実装も早い段階で公開される。実際に使ってみるまでいかなくても、その概要を知っておくだけでリリース後のモチベーションが大きく違ってくるはず。本連載で6回に渡って行ってきた"クロージャ特集"もその手助けになれば幸いである。