Javaのラムダ式で注意したい変数キャプチャの落とし穴とは?代入と変数名のベストプラクティス解説
生徒
「ラムダ式の中で外部の変数を使おうとしたらエラーが出たんですが、これってバグですか?」
先生
「それはバグというより、Javaのラムダ式の『変数キャプチャ』の仕様によるものですね。」
生徒
「変数キャプチャ?変数を使ってるだけなのに…」
先生
「Javaのラムダ式では外部変数を使うときに注意点がいくつかあるんです。代入や変数名の使い方にも落とし穴があるので、一緒に確認してみましょう!」
1. Javaのラムダ式と変数キャプチャとは?
Javaのラムダ式では、外部スコープの変数を参照することができます。これを変数キャプチャ(Variable Capture)と呼びます。たとえば、メソッド内のローカル変数をラムダ式で使用する場合、それがキャプチャの対象となります。
ただし、このときに使える変数には条件があります。それがfinalまたはeffectively finalであることです。
2. なぜ代入できないのか?
Javaでは、ラムダ式が参照する変数にあとから再代入することはできません。これは、ラムダ式が変数のスナップショット的な状態を扱うためです。
public class LambdaCapture {
public static void main(String[] args) {
int count = 0;
Runnable r = () -> System.out.println("カウント: " + count);
count++; // ← コンパイルエラー
r.run();
}
}
このように、ラムダ式で使用した変数は、その後変更できないというルールがあります。再代入するとコンパイルエラーになります。
3. 変数名のシャドーイングに注意
Javaのラムダ式では、外部スコープの変数と同じ名前のローカル変数をラムダ式内で再定義することはできません。これを変数のシャドーイングと呼び、ラムダ式では禁止されています。
public class LambdaShadowing {
public static void main(String[] args) {
String message = "こんにちは";
Runnable r = () -> {
// String message = "別のメッセージ"; // ← エラー
System.out.println(message);
};
r.run();
}
}
ラムダ式は同じスコープを共有しているとみなされるため、変数名の上書きはできません。
4. 変数の使い回しでハマるケース
ラムダ式を使ってループ処理を書くとき、変数が思った通りに動作しないことがあります。これも変数キャプチャの落とし穴のひとつです。
import java.util.*;
public class LambdaLoopTrap {
public static void main(String[] args) {
List<Runnable> runners = new ArrayList<>();
for (int i = 0; i < 3; i++) {
runners.add(() -> System.out.println("i = " + i));
}
runners.forEach(Runnable::run);
}
}
このコードは一見問題なさそうですが、すべてのラムダ式が同じ変数iを参照しているため、実行結果は意図と異なる可能性があります。
i = 3
i = 3
i = 3
5. 意図通りに動かすにはfinalな変数を使う
ループの中でラムダ式を使うときは、中間変数を使ってfinal化するのがベストプラクティスです。
import java.util.*;
public class LambdaLoopSafe {
public static void main(String[] args) {
List<Runnable> runners = new ArrayList<>();
for (int i = 0; i < 3; i++) {
final int index = i;
runners.add(() -> System.out.println("index = " + index));
}
runners.forEach(Runnable::run);
}
}
このようにすることで、各ラムダ式が自分専用の変数をキャプチャすることができ、期待通りの結果になります。
index = 0
index = 1
index = 2
6. ベストプラクティス:変数名と代入のコツ
Javaのラムダ式で変数を使うときは、以下の点に気をつけると安全です。
- ローカル変数は変更しない(effectively finalを保つ)
- ループ内では中間変数を用意する(finalをつけるとより明示的)
- 外部の変数名と同じ名前を使わない(シャドーイングエラーを避ける)
- できるだけラムダ式内ではローカルな名前を使う(混乱を防ぐ)
ラムダ式は便利ですが、こうした細かなルールを知らないと、思わぬエラーやバグにつながってしまいます。
7. 変数キャプチャと匿名クラスの違い
Javaのラムダ式と匿名クラスは似ているようで、実は変数の扱い方に差があります。匿名クラスではシャドーイングが許されるのに対し、ラムダ式では許されません。
public class CaptureComparison {
public static void main(String[] args) {
int number = 10;
Runnable anonymous = new Runnable() {
public void run() {
int number = 20; // OK(匿名クラス内では別スコープ)
System.out.println(number);
}
};
// ラムダ式では次のような書き方はNG
// Runnable lambda = () -> {
// int number = 20; // エラー:変数の重複
// System.out.println(number);
// };
}
}
このように、ラムダ式は外側のスコープと同じ文脈で評価されるという特徴があります。