クロージャからローカル変数へのアクセス

今回も第54回に引き続いてJava SE 7で導入される予定のクロージャについて紹介する。従来のJavaでも、無名インナークラスを使えばクロージャに近いコードを記述することはできた。その無名インナークラスとクロージャの非常に大きな違いが、クロージャではそれが定義された環境(エンクロージャ)のスコープ内のローカル変数にアクセスできる点だ。

リスト1で定義している{ int y => x += y }というクロージャは、エンクロージャ内のローカル変数xに、引数で渡された値yを加算して返すというものだ。クロージャ内で加算されたxの値は、クロージャの外側でも有効である。また、エンクロージャ側でxの値を変更した場合にも、次にクロージャが呼び出された際にはそれが反映される。したがってこのプログラムの実行結果はプロンプト1のようになる。

現在のところ仕様には記載されていないが、クロージャ側から参照されるローカル変数には@Sharedアノテーションを付加することになるようだ。@Sharedが指定されていない場合にはコンパイル時に警告が発せられる。

リスト1 AccessLocalVariable.java

class AccessLocalVariable {
    public static void main(String[] args) {
        @Shared int x = 0;

        { int => int } add = { int y => x += y };

        System.out.println(add.invoke(5));
        System.out.println(add.invoke(5));
        ++x;
        System.out.println(add.invoke(5));
  }
}

プロンプト1 AccessLocalVariable.javaの実行結果

>java AccessLocalVariable
5
10
16

クロージャのインタフェース型への変換

前回、クロージャはインタフェースの一種として扱われると書いたが、それに関連した機能としてクロージャリテラルはインタフェース型の変数に代入することができる。条件はそのインタフェースがただ1つのメソッドを持っており、その引数および戻り値の型/数がクロージャのそれと一致することだ。

たとえばリスト2のようなインタフェースがあるとする。IntFunctionはint型の値を2つ受け取り、int型の値を1つ返すようなメソッドadd()をただ1つもつ。

リスト2 IntFunction.java

interface IntFunction {
    int add(int x, int y);
}

通常であればこのIntFunctionをimplementsしたクラスを作成し、add()を実装して利用するという手順になるが、クロージャを利用した場合リスト3のように記述することができる。IntFunction型の変数plusには、IntFunctionのインスタンスの代わりにadd()と同じ引数/戻り値を持つクロージャリテラルを代入している。その上でplusに対してadd()メソッドを呼び出せば、代入したクロージャが実行される。

リスト3 ConversionSample.java

class ConversionSample {
    public static void main(String[] args) {
        IntFunction plus = { int x, int y => x + y };

        int res = plus.add(10, 20);
        System.out.println(res);
    }
}

もしIntFunctionがint型の値を2つ受け取りint型の値を1つ返すメソッド(たとえばint sub(int x, int y)のような)をもうひとつ持っている場合にはこの変換は使えない。

この{int x, int y => x + y}というクロージャは、実装上はIntFunctionの無名実装クラスとして扱われるとのこと。ConversionSample.classを逆コンパイルしてみるとリスト4のようになっており、IntFunctionインタフェースのadd()メソッドの代わってinvoke()メソッドの実装が用意されていることがわかる。

リスト4 ConversionSample.classを逆コンパイル

import java.io.PrintStream;

class ConversionSample {
    ConversionSample() { }

    public static void main(String args[]) {
        Object obj = new Object() {
        IntFunction plus$1;
        };
        obj._fld1 = _2B_INSTANCE0;
        int i = ((_cls1) (obj))._fld1.invoke(10, 20);
        System.out.println(i);
    }

    public static final _cls2 _2B_INSTANCE0 = new IntFunction() { 
        public final int _2B_invoke(int i, int j) {
            return i + j;
        }

        public final int invoke(int i, int j) {
            return _2B_invoke(i, j);
        }
    };
}

この変換が可能なのはインタフェースがただひとつのメソッドを持つときと書いたが、例外的に複数のメソッドを持つインタフェースでも適用できることがある。それは、複数のメソッドを持つインタフェースで、ひとつのメソッド以外のメソッドの引数がすべてObject型である場合。このときクロージャは、Object以外の型を持つメソッドに適用される。

この条件に当てはまるインタフェースの例としてはjava.util.Comparator<T>が挙げられている。Comparatorには「int compare(T o1, T o2)」と「boolean equals(Object obj)」という2つのメソッドが宣言されている。したがってcompare()メソッドの方にクロージャが適用できる。

たとえば次のような例が考えられる。java.util.Arraysクラスのsort()メソッドは第1引数にクラスTの配列を、第2引数にComparatorのインスタンスを取る。Comparatorインタフェースには順序付けの条件を定めるためのメソッドとして「int compare(T o1, T o2)」が宣言されている。ここで、Comparatorをimplementsする代わりに、クロージャを用いてcompare()メソッドの実装を定義することで順序付けの条件を指定しているのがだ。

このクロージャは2つの文字列を受け取り、最初の文字列の方が長ければ正の値、短ければ負の値、長さが等しい場合にはcompareTo()で比べた結果を返す。ここではこのクロージャをArrays.sort()の第2引数に渡しているので、ソート後のwordsは長さの短い文字列順に並ぶことになる ()。

リスト5 onversionSample2.java

import java.util.Arrays;

class ConversionSample2 {
    public static void main(String[] args) {
        String[] words = { "aaaa", "bbb", "cc", "ddddd" };

        // Comparatorの実装クラスの代わりにクロージャを渡す
        Arrays.sort(words,
            { String s1, String s2 =>
                 int r = s1.length() - s2.length();
                 (r == 0) ? s1.compareTo(s2) : r 
            });

        for (String word : words) {
            System.out.println(word);
        }
    }
}

プロンプト2 ConversionSample2.javaの実行結果

> java ConversionSample2
cc
bbb
aaaa
ddddd

この機能をうまく使えばインタフェースの実装をシンプルに記述できるようになるだろう。