Javaのラムダ式とGroovyのClosureの違いについて

2020.09.10

はじめに

GroovyにはClosureがあります。Javaにラムダ式が登場する以前から存在しており、Javaの関数オブジェクトと同じように利用されるほか、Groovy製のDSLなどにも登場します。

Groovy3からはJavaのラムダ式と同じ記法でClosureを表現できるようになり、よりJavaに近い感覚で書けるようになりました。一方でラムダ式とClosureはそれぞれ異なる性質を持つため、扱いには注意が必要です。

クロージャとは

クロージャとは、自身が変数環境を持つ関数オブジェクトのことを指します。内部に変数環境を持つため、データとメソッドを併せ持つオブジェクトに類似した性質を関数として持つことができます。

例として、Javascriptで呼び出されるたびにカウントアップして結果を返すカウンタをクロージャで実装します。

function makeCounter() {
	let i = 0;
	function counter() {
		return i++;
	}
	return counter;
}

let c = makeCounter();
c();  // 0
c();  // 1
c();  // 2

関数counterが変数iを持ち、呼び出される毎に値を更新していることがわかります。

Javaの場合

Javaのラムダ式はクロージャとしての性質を持ちません。そのため、先ほどのカウンタのような関数をラムダ式で表現することはできません。

試しに書いてみると、内部の変数を更新する箇所でコンパイルエラーが発生します。

public class Main {
    public static Supplier<Integer> makeCounter(){
        var i = 0;
        return () -> ++i; // compile error
    }

    public static void main(String[] args){
        var counter = makeCounter();
        counter.get();
        counter.get();
        counter.get();
    }
}

エラーメッセージをみると、ラムダ式の中の変数はすべて final として扱わなければいけないことがわかります。

Variable used in lambda expression should be final or effectively final

Groovyの場合

Groovyのラムダ式によって生成される関数オブジェクトは、冒頭で説明した通り Closure オブジェクトとなります。

Closure オブジェクトはJavaのラムダ式のような制限を持たないため、関数の中で宣言した変数は自由に更新することができます。

class Main2 {
    static makeCounter() {
        def i = 0
        return () -> i++
    }

    static void main(String[] args){
        def c = makeCounter()

        c() // 0
        c() // 1
        c() // 2
    }
}

おわりに

Javaのラムダ式とGroovyのClosureの違いについて、簡単にまとめました。

ここで紹介したクロージャは静的スコープに従って変数環境を保持していますが、Groovyの Closure には動的にスコープの対象を切り替える仕組みも持っています。

Groovyのクロージャは柔軟な実装が可能な反面、挙動を予想しづらい部分があります。一方でJavaのラムダ式は制約によって見通しやすい実装に誘導しているとも考えられます。

それぞれの言語の特性の違いをよく表していると思いました。

参考