Java8にlambda構文が入りましたが、これはクロージャーではない、とされています。
(中略)
結論としては、「Java8のlambdaはクロージャーではないけど、クロージャーでやりたいことはできるし、やってはいけないことができないようになっているので、特に問題はない」と言えると思います。
クロージャ(クロージャー、英: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。
引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする(Wikipedia)
クロージャは関数閉包と訳されているように「関数の中に値を閉じ込めて保持する」という意味。
もうちょっとわかりやすく言うと定義時の変数を保持することができる、つまり「状態を保持することができる関数」を作ることができる仕組み。
たとえばこういう関数。呼び出すごとに値をインクリメントして返却する関数。
increment(); // 1
increment(); // 2
increment(); // 3
もちろん関数外のグローバル変数などを利用するのではなく、関数自体に値を保持させる。
private Integer num = 0;
increment(); // 1
increment(); // 2
increment(); // 3
//このコードでは関数自体が変数を保持していないのでクロージャではないよ
public increment(){
System.out.println(num++);
}
out("出力")
の時点で createMessage()
メソッドが処理される。
@Test
public void testNotClosure01() throws Exception {
out("開始");
out("定義");
final String name = "はなふさ";
out("出力");
System.out.println(createMessage(name));
System.out.println(createMessage(name));
out("終了");
// [処理結果]
// 開始: 12:55:55.699
// 定義: 12:55:56.753
// 出力: 12:55:57.758
// こんにちは、はなふさです(実行時変数:12:55:58.758)
// こんにちは、はなふさです(実行時変数:12:55:58.758)
// 終了: 12:55:58.758
}
private String createMessage(final String name) {
final String insideDatetime = now();
return String.format("こんにちは、%sです(実行時変数:%s)", name, insideDatetime);
}
private void out(final String comment) throws InterruptedException {
System.out.println(String.format("%s: %s", comment, now()));
Thread.sleep(1000);
}
private String now() {
return new DateTime().toString("HH:mm:ss.SSS");
}
out("定義")
時点にて createMessage()
メソッドの結果を変数に保持し、 out("出力")
にてその結果を出力するように変更。
しかし createMessage()
が返却するのは関数ではなく値なため、上のコード例と何も変わらない。
@Test
public void testNotClosure02() throws Exception {
out("開始");
out("定義");
String name = "はなふさ";
final String message01 = createMessage(name);
out("定義");
name = "ほげほげ";
final String message02 = createMessage(name);
out("出力");
System.out.println(message01);
System.out.println(message02);
out("終了");
// [処理結果]
// 開始: 12:56:18.202
// 定義: 12:56:19.277
// 定義: 12:56:20.280
// 出力: 12:56:21.283
// こんにちは、はなふさです(実行時変数:12:56:20.280)
// こんにちは、ほげほげです(実行時変数:12:56:21.283)
// 終了: 12:56:22.287
}
関数閉包という名の通り関数を使わなければならない。
out("定義")
時点にて createMessageClosure()
メソッドの結果(関数!)を変数に保持し、 out("出力")
にてその結果を出力するように変更。
- 上記のコードと異なり
createMessageClosure()
メソッドの戻り値は関数のため、実際に処理されるのはout("定義")
時ではなくout("出力")
時 createMessageClosure()
メソッド内、ラムダ式外で定義されたoutsideDatetime
変数値は、定義時out("定義")
時の時刻が出力される- ラムダ式内で定義された
insideDatetime
変数値は、実行時out("出力")
時の時刻が出力される createMessageClosure()
メソッドのname
引数値は、実行時の値値なし
ではなく定義時の値が出力される
上記から createMessageClosure
メソッドが返却する関数は、変数 name
および outsideDatetime
を関数定義時に関数閉包という形で保持していることが分かる。
@Test
public void testClosure() throws Exception {
out("開始");
out("定義");
String name = "はなふさ";
final Supplier<String> message01 = createMessengeClosure(name);
out("定義");
name = "ほげほげ";
final Supplier<String> message02 = createMessengeClosure(name);
out("出力");
name = "値なし";
System.out.println(message01.get());
System.out.println(message02.get());
out("終了");
// [処理結果]
// 開始: 12:56:49.833
// 定義: 12:56:50.893
// 定義: 12:56:51.921
// 出力: 12:56:52.926
// こんにちは、はなふさです(クロージャ変数:12:56:51.896、実行時変数:12:56:53.931)⇒ クロージャ変数は関数定義された時刻
// こんにちは、ほげほげです(クロージャ変数:12:56:52.925、実行時変数:12:56:53.932)⇒ 〃
// 終了: 12:56:53.932
}
private Supplier<String> createMessengeClosure(final String name) {
final String outsideDatetime = now();
return () -> {
final String insideDatetime = now();
return String.format("こんにちは、%sです(クロージャ変数:%s、実行時変数:%s)",
name,
outsideDatetime,
insideDatetime);
};
}
上記のように、クロージャは関数に関する仕組みのため、JavaにおいてはJava8ラムダ式から初めて利用できるようになった。
下記のJavaScriptコード場合、非同期呼び出しのコールバック処理にて、呼び出し時の変数 i
を参照しようとすると、呼び出し時の変数値ではなく max
値が取得されてしまう。
//非同期処理の呼び出しループ
for (i=0; i<max; i++) {
//指定URLを非同期取得する
$.get(api, function(){
//コールバック処理、ここで変数 i が使いたいが。。
});
}
クロージャを使えば、呼び出し時の変数値が取得できる。
for (i=0; i<max; i++) {
(function(){
var _i = i;
$.get(api, function(){
//コールバック処理、非同期呼び出し時点での変数 i 値が 変数 _i から取得できる
});
}());
}
紀平さん@tkihiraのニコ動画、オセロを1時間で作ってみたでの21:00あたり。
オセロのセルを押下された際の振る舞いを、盤面描画の際に各セルに登録しておく。クロージャで各セルの位置を無名関数内に保持しておく。
var cellsize=32; //セル高・幅
var piece; //セル配列
//オセロ盤面の描画処理
var showBoard = function(){
var board = document.getElementById("board");
while(var y=1; y<=8; y++){
while(var x=1; x<=8; x++){
var cell = piece[board[x][y]].cloneNode(true);
cell.style.left = ((x-1)) * cellsize) + "px"; //セルの表示位置
cell.style.top = ((y-1)) * cellsize) + "px"; //セルの表示位置
board.appendChild(cell);
//当該セルにオセロが配置されていなければ
if(board[x][y] == 0){
(function() {
var _x = x; _y = y;
//クリックされたらオセロを配置して再描画する
cell.onclick = function(){
board[_x][_y] = 1;
showBoard();
}
})();
}
}
}
};
うーん、これと言って思いつかない。。
ラムダ式内からクロージャ変数に対して変更を行うことはできない。
内部クラスやラムダ式において、その外側で定義されているローカル変数を使う場合は、そのローカル変数に暗黙にfinalが付いているものと見なされる。
変数に再代入を行うとコンパイルエラーになる。 「Local variable value defined in an enclosing scope must be final or effectively final」 (内部クラスから参照されるローカル変数は、finalまたは事実上のfinalである必要がある)
private Supplier<String> createMessengeClosure(String name) {
final String outsideDatetime = now();
return () -> {
name = "hoge"; ⇒ コンパイルエラー!
final String insideDatetime = now();
return String.format("こんにちは、%sです(クロージャ変数:%s、実行時変数:%s)",
name,
outsideDatetime,
insideDatetime);
};
}