Skip to content

Instantly share code, notes, and snippets.

@asufana
Last active January 31, 2020 04:57
Show Gist options
  • Save asufana/94378314d8d621cafc21 to your computer and use it in GitHub Desktop.
Save asufana/94378314d8d621cafc21 to your computer and use it in GitHub Desktop.
Java8 クロージャ

Java8 クロージャ

Javaにクロージャが提供されたのか?

Java8にlambda構文が入りましたが、これはクロージャーではない、とされています。

(中略)

結論としては、「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++);
}

Java8ラムダ式でのクロージャ利用

まずクロージャではないコード

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)

非同期処理の呼び出し時パラメータの保持

下記の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();
				}	
			})();
		}
	}
	}
};

クロージャの使い道(Java)

うーん、これと言って思いつかない。。

Javaクロージャの制限(実質的final)

ラムダ式内からクロージャ変数に対して変更を行うことはできない。

内部クラスやラムダ式において、その外側で定義されているローカル変数を使う場合は、そのローカル変数に暗黙に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);
        };
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment