1. クロージャ入門
クロージャとは、引数(パラメータ)だけでなく、生成された場所の周囲のコンテキストから変数も「覚えて」利用できる関数(または関数オブジェクト)のことです。言い換えると、メソッド内のラムダ式や無名クラスがそのメソッドの変数を使っているなら、それはクロージャになります。
直感的な例
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "こんにちは、";
Runnable sayHello = () -> System.out.println(greeting + "世界!");
sayHello.run(); // 出力: こんにちは、世界!
}
}
ここでは、ラムダが外側のメソッドから変数 greeting を「キャプチャ」して内部で使っています。これがクロージャです。
2. 実質的に final な変数: 何で、なぜ必要?
Java のラムダ式(および無名クラス)は、外側のメソッドから、final として宣言されている、または初期化後に変更されない変数のみを使用できます。このような変数を 実質的に final と呼びます。
なぜ?
これは、ラムダ式が生成されたメソッドの実行が終わった後に呼び出される可能性があるための制約です。もし変数を変更できるとしたら、「どの時点の値を使うべきか」という混乱が起きます。サプライズを避けるために、Java は変数が不変(少なくとも見かけ上不変)であることを要求します。
例: 正しい使い方
public static void main(String[] args) {
int number = 42; // number — 実質的に final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
例: 使用後に変数を変更しようとする
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // エラー: 変数 number は final または実質的に final でなければなりません
r.run();
}
コンパイラは次のエラーを出します: Variable used in lambda expression should be final or effectively final.
実質的に final とは... 代入がちょうど一度だけ行われ、その後変更されない変数のことです。わざわざ final と書かなくても、コンパイラが判断してくれます。
3. ラムダ式はどのように変数をキャプチャする?
ラムダが外側の変数を使うように書くと、Java はその変数をラムダと一緒に「梱包」します。たとえそのラムダを作ったメソッドの実行がすでに終わっていても、変数は消えず、クロージャの内部で生き続けます。
図解: ラムダは変数を「覚える」
public static Runnable createGreeter(String name) {
// name — メソッドのパラメータ。ラムダにキャプチャされる
return () -> System.out.println("こんにちは、" + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Vasya");
greeter.run(); // こんにちは、Vasya!
}
ここでは、変数 name はすでに main のスタックには存在しませんが、greeter はその値を「覚えています」。
内部ではどう実装されている?
Java コンパイラは、キャプチャされた変数を保持する特別な補助オブジェクト(「capture/display class」とも呼ばれます)を作ります。ラムダ式は、その変数コンテナへの参照を持つオブジェクトになります。
4. クロージャの例: 変数を使う関数を返す
自分のコンテキストからの変数を使うラムダを返す関数を書いてみましょう:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x はラムダにキャプチャされる
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
ここでは、変数 x はメソッドのスタックからはすでに「消えています」が、ラムダは引き続きそれを使えます。
5. なぜキャプチャした変数を変更できないのか?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // エラー!
printer.run();
}
コンパイラはこれを許しません。もし base を変更できるなら、ラムダの中でどのバージョンの値を使うべきかが不明確になります。だからこそ、Java はラムダがキャプチャするローカル変数の変更を禁止しています。
ラムダで使えるものは?
- 初期化後に変更されないローカル変数(実質的に final)。
- クラスのフィールド(static とインスタンスの両方)— これらは変更可能ですが、これはローカル変数のキャプチャではなく(オブジェクト状態へのアクセスという)別の仕組みです。
6. 無名クラスとの比較
ラムダ式が導入される以前、Java では無名クラスを使ってクロージャ的なことができました:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
ルールは同じです: 変数 word は final または実質的に final でなければなりません。
違い: this のスコープ
- 無名クラスでは、this は無名クラスのインスタンスを指します。
- ラムダ式では、this は外側のオブジェクト(たとえば現在のクラスのインスタンス)を指します。
7. クロージャとクラスのフィールド
ラムダがクラスのフィールドを使う場合、厳密な意味での「ローカル変数のキャプチャ」ではありません。フィールドは常にアクセス可能で、変更もできます。
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("カウンタ: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // カウンタ: 1
r.run(); // カウンタ: 2
}
}
8. よくある誤りと Java におけるクロージャの特徴
よくある誤り1: ラムダにキャプチャされた変数を変更しようとする。 最も多いのは、ラムダで使ったローカル変数を後から変更しようとすることです。コンパイラは次のように知らせます: Variable used in lambda expression should be final or effectively final.
よくある誤り2: 変数が「凍結」されると期待する。 Java では、クラスのフィールドが関係する場合、キャプチャされるのは値のコピーではなくオリジナルへの参照です。フィールドが変われば、ラムダからも新しい値が見えます。一方で、ラムダ内で使うローカル変数は実質的に final でなければなりません。
よくある誤り3: ラムダが this の新しいスコープを作ると思う。 ラムダにおける this は外側のオブジェクト(外側のクラス)を指します。無名クラスでは、this は無名クラス自身を指します。
よくある誤り4: 変更可能なオブジェクトの扱い。 変更可能なオブジェクト(たとえば List)への参照をキャプチャすると、変数自体は実質的に final のままでも、ラムダの中でそのオブジェクトの中身を変更できます:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
ここでは、変数 list 自体は変わっていません(list = ... はしていません)。しかし、保持しているオブジェクトの中身は変更されています。
GO TO FULL VERSION