java 8 では様々な機能が追加され、その中でも「ラムダ式」はかなり注目され、話題にもなりました。
同じバージョンで「メソッド参照」という機能も追加されましたが、注目度はイマイチのようです。
ラムダ式に比べると、できることも限られるし、そもそもラムダ式で代用できるため、致し方ないかもしれません。
ただ、メソッド参照で記述するとコード量が減り、かなりスッキリします。
そして、自分が好むか好まざるかに関わらず、既存のコードに含まれていれば、嫌でも理解しなければならないでしょう。
ということで、今回はメソッド参照についての記事となります。
メソッド参照を使うとスマートに記述できますが、結構わかりにくいところや落とし穴があるように感じました。
そのあたりを見ていこうと思います。
まずはテストで使用するコードです。
package test01;
public class Greeter {
private String greet;
public Greeter(String greet) {
this.greet = greet;
}
public void setGreet(String greet) {
this.greet = greet;
}
public void hello() {
System.out.println(greet);
}
}
まあ、あまり解説は不要かと思います。
次に、ラムダ式やメソッド参照で使うインターフェースの定義です。
そのインターフェースは「関数型インターフェース」といい、メソッドは1つしか持てません。
package test01;
@FunctionalInterface
public interface FuncInter0 {
public void func();
}
@FunctionalInterface
はなくてもいいですが、指定すると、例えばメソッドを複数定義するとエラーとなるなど、チェックをしてくれるようです。
これらを使うコードを書いていきます。
まず準備のコードです。
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;
fi0
変数は後で複数回インスタンスを代入するため、ここでは初期化のみを行っています。
さて、まずは匿名クラスの復習です。
匿名クラス
// 匿名クラス
fi0 = new FuncInter0() {
@Override
public void func() {
greeter.hello();
}
};
fi0.func();
上のコードを実行すると「今日は!」が出力されます。
特に問題ないですね。
ラムダ式
上と同じことをラムダ式を使って書いてみます。
// ラムダ式
fi0 = () -> greeter.hello();
fi0.func();
劇的に短くなりました。
なるほど便利ですね。
次に、同じことをメソッド参照を使って書いてみます。
メソッド参照
// メソッド参照
fi0 = greeter::hello;
fi0.func();
メソッド参照は「(インスタンスを格納した)変数名::メソッド」という形をしており、それを代入したり引数として指定したりすることができます。
他に「クラス名::メソッド」という形式もありますが、ここでは触れません。
匿名クラスがラムダ式になったのに比べると、地味ですね。
連結した実行コード
上記の「準備」、「匿名クラス」、「ラムダ式」、「メソッド参照」のコードを単純に連結したコードを一応載せておきます。
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;
// 匿名クラス
fi0 = new FuncInter0() {
@Override
public void func() {
greeter.hello();
}
};
fi0.func();
// ラムダ式
fi0 = () -> greeter.hello();
fi0.func();
// メソッド参照
fi0 = greeter::hello;
fi0.func();
上のコードを実行すると、「今日は!」が3回出力されます。
同じことを3回やっているので当然ですね。
コンパイルエラー?
さて、問題はここからです。
例えば次のようなコードを実行コードの最後にでも追加すると、コンパイルが通らなくなります。
greeter = null;
エラー内容は以下のとおり。
"Local variable greeter defined in an enclosing scope must be final or effectively final"
これがどこで出るかというと、匿名クラスとラムダ式の中でgreeter
変数を使っているところです。
greeter
をfinal
にしろ、と言っているようです。
確かに匿名クラスの文法では、メソッド外の変数にアクセスする場合、その変数はfinal
でないとダメでした。
でも、うまくいっていたコードでもgreeter
変数にfinal
はつけていません。
greeter
に代入するコードを追加するまでは、暗黙的にfinal
とみなしていたとしか思えないですね。
このコンパイルエラーを深掘りするつもりはないですが、メソッド参照でgreeter
を使っているところでは、エラーになっていません。
このことが、メソッド参照を使うときの注意点のヒントになります。
メソッド参照は使われた時点のインスタンスを保持する
メソッド参照はラムダ式(ひいては匿名クラス)と同等のはずです。
なので、ラムダ式でコンパイルエラーになるのであれば、メソッド参照でもエラーになるべきだと個人的には思います。
でも実際にはコンパイルエラーにならないため、逆にちょっとした混乱の元となる可能性があります。
ちょっと実験をしてみます。
上のメソッド参照のコードを次のように変えます。
// メソッド参照その2
fi0 = greeter::hello;
fi0.func();
greeter = new Greeter("今晩は!");
fi0.func();
これを準備コードの後に続けます。
要するに次のようなコードにします。
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;
// メソッド参照その2
fi0 = greeter::hello; // ※1
fi0.func();
greeter = new Greeter("今晩は!"); // ※2
fi0.func();
新たにGreeter
のインスタンスを生成し、greeter
変数に代入しています(上記コードの※2)。
fi0
変数に代入されたメソッド参照が、greeter
変数が参照するインスタンスのhello
メソッドを呼び出すのであれば、2回目の出力は「今晩は!」になりそうです。
でも実際にはそうはならず、「今日は!」が出力されます。
これから分かることは、メソッド参照はそれが使用された(上記コードの※1)時点のインスタンスを保持しており、それを使用しているということです。
greeter
変数がその後上書きされても、参照するインスタンスが変わるわけではないようです。
greeter
がfinal
であれば、それを上書きして代入する(※2のような)コードは書けないため、混乱することもないんですけどね。
メソッド参照はディープコピーしない
もう1つ実験をしてみます。
上のメソッド参照のコードをさらに次のように変えます。
// メソッド参照その3
fi0 = greeter::hello;
fi0.func();
greeter.setGreet("今晩は!");
fi0.func();
これを準備コードの後に続けます。
要するに次のようなコードにします。
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;
// メソッド参照その3
fi0 = greeter::hello; // ※1
fi0.func();
greeter.setGreet("今晩は!"); // ※2
fi0.func();
今度は、greeter
変数に新たなインスタンスを代入して上書きしているわけではなく、setter
を使って、インスタンスの中身を変えているだけです(上記コードの※2)。
もしメソッド参照が、それが使用された(上記コードの※1)時点でインスタンスをディープコピーし、それを保持しているのであれば、2回目の出力は「今日は!」と出力されるはずです。
でも実際にはそうはならず、「今晩は!」が出力されます。
よって、ディープコピーしたインスタンスではなく、インスタンスそのものを保持し、それを使用していることがわかります。
今回はJavaのメソッド参照についての記事でした。
かなりとんがった話題でしたが、実際にハマると、かなり苦戦するのではないかと思いました。