キャプチャリング(capturing)は、英語の capture(捕らえる・取り込む)から来た言葉で、分野によって指すものが変わります。Web開発ではDOMイベントが外側の要素から内側へ伝わるフェーズ、ネットワークではパケットの記録、CGでは身体の動きの取り込みを指します。この記事の中心は、プログラミング言語のJavaにおけるキャプチャです。ラムダ式や匿名クラスの中から、外側で宣言されたローカル変数を参照する仕組みを指し、Javaはこの参照に「final または実質final であること」という条件を課しています。Java言語仕様(JLS)の規定を根拠に、条件の中身、ラムダ式と匿名クラスの違い、他の言語との違いを、JDK 25 LTS(Temurin 25.0.5+8)と JDK 27(2026年9月15日GA)での実測とあわせて整理します。
まとめ:Javaのキャプチャリングで押さえる5点
- 条件は2つあり、片方だけでは足りません。ラムダ式や内部クラスから参照するローカル変数は、final または実質final であることに加えて、その本体より前に definitely assigned(確実に代入済み)である必要があります。後者を外すと javac は別のエラーメッセージを出すため、キャプチャの問題だと気づきにくくなります。
- 制限の理由は性能ではなく安全性です。JLS は、変化し続けるローカル変数のキャプチャが並行性の問題を招きやすいことを根拠に挙げています。
- ローカル変数は値がコピーされ、フィールドはコピーされません。インスタンスフィールドに制限がないのは、外側のインスタンス(
this)がキャプチャされ、実行時にフィールドを読むためです。 - JavaScript・Python・C#は変数そのものを捕まえるので、関数の中から外側の変数へ代入できます(JavaScriptの
constなど、宣言で再代入を禁じた変数は除きます)。その代わり、ループで作った関数が全部同じ値を返す事故が起きます。Javaは値のコピーと再代入の禁止を組み合わせ、この事故をコンパイルエラーとして止めています。 - ラムダ式と匿名クラスは条件が同じでも、
this・名前の扱い・生成物が違います。ラムダ式は外側の文脈をそのまま引き継ぐため、同名の変数を宣言できず、インタフェースのdefaultメソッドも名前だけでは呼べません。
キャプチャリングとは|分野ごとの用法とJavaでの定義
「キャプチャリングとは」で検索すると、まったく別の技術の解説が並びます。どの分野の話かを先に確定させないと、読んでいる資料が目的に合いません。
分野ごとに指す対象の違い
| 分野 | 指している対象 | よく併記される語 |
|---|---|---|
| プログラミング言語 | 関数やクラスの内部から外側の変数を参照する仕組み | クロージャ、実質final |
| Webフロントエンド(DOM) | イベントが外側の要素からターゲットへ下りるフェーズ | バブリング、addEventListener |
| ネットワーク | 通信を流れるパケットを記録・保存する行為 | パケットキャプチャ、Wireshark |
| CG・映像 | 身体の動きや映像信号をデータとして取り込む行為 | モーションキャプチャ、キャプチャボード |
DOMイベントのキャプチャリングフェーズの発火順
JavaScriptの文脈で「キャプチャリング」と言う場合、多くはDOMイベントの伝播の話で、変数とは関係がありません。DOM Standard はイベントのフェーズを eventPhase の値で区別しており、ターゲットに届く前の CAPTURING_PHASE が1、ターゲット上の AT_TARGET が2、届いた後の BUBBLING_PHASE が3です。addEventListener の第3引数に { capture: true } を渡したリスナーだけがフェーズ1で呼ばれ、省略した祖先上のリスナーは、バブリングするイベントがその祖先まで伝播した場合にフェーズ3で呼ばれます。ターゲット上では、どちらの指定もフェーズ2の呼び出し対象になります。
// HTML: <div id="outer"><button id="btn">x</button></div>
const out = [];
const outer = document.getElementById("outer");
const button = document.getElementById("btn");
const add = (target, name, capture) =>
target.addEventListener("click",
(e) => out.push(name + (capture ? "(capture)" : "(bubble)") + " phase=" + e.eventPhase),
{ capture });
add(window, "window", true); add(document, "document", true);
add(outer, "outer", true); add(button, "button", true);
add(button, "button", false); add(outer, "outer", false);
add(document, "document", false); add(window, "window", false);
button.click();
console.log(out.join(" / "));
Chromium 156(headless)で実行した出力を1行ずつに分けると、次のとおりです。
window(capture) phase=1
document(capture) phase=1
outer(capture) phase=1
button(capture) phase=2
button(bubble) phase=2
outer(bubble) phase=3
document(bubble) phase=3
window(bubble) phase=3
外側の outer のキャプチャリスナーで stopPropagation() を呼ぶと、同じクリックでも出力は outer(capture) までで止まり、ボタン自身のリスナーは1つも呼ばれませんでした。子要素に届く前に外側で処理を差し込めるのがキャプチャフェーズの用途で、モーダルの外側クリック検知やログ収集に使われます。ここから先は、Javaの変数キャプチャを扱います。
Java言語仕様でのキャプチャの位置づけ
Javaでキャプチャが問題になるのは、ラムダ式・匿名クラス・ローカルクラスのように、宣言された場所とは別のタイミングで実行されうるコードを書いたときです。Java言語仕様 SE 27 の §15.27.2 は、ラムダ式についてこう定めています。
Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression must either be final or effectively final (§4.12.4), as specified in §6.5.6.1.(ラムダ式の中で使われているが宣言されていないローカル変数・仮引数・例外パラメータは、final または実質final でなければならない)
内部クラスにも同じ規定があり、§8.1.3 が「Any local variable, formal parameter, or exception parameter used but not declared in an inner class must either be final or effectively final」と定めています。条件そのものはラムダ式と匿名クラスで共通です。
「実質final(effectively final)」という用語は Java 7 の時点で既にありました。Java 7 の言語仕様 §4.12.4 は単一catch節の例外パラメータについて「may be effectively final instead of being explicitly declared final」と述べていますが、これはマルチcatchと再スロー解析のための概念でした。同じ版の §8.1.3 は内部クラスについて「must be declared final」と書いており、匿名クラスからローカル変数を参照するには final の明示が必要でした。Java 8 でラムダ式が入ったときに、実質finalがキャプチャの条件としてラムダ式と内部クラスの両方へ広げられています。
キャプチャできる変数とできない変数の一覧
「finalを付ければよい」で済ませると、拡張forが通って基本forが通らない理由や、初期化していない変数で別のエラーが出る理由を説明できません。条件を仕様の記述に沿って分解します。
実質final(effectively final)の3条件
JLS SE 27 §4.12.4 は、初期化子を持つローカル変数について、次の3つをすべて満たすものを実質finalと定義しています。
finalが宣言に付いていないこと(付いていればそもそも final 変数です)- 代入式の左辺に一度も現れないこと
- 前置・後置のインクリメント演算子・デクリメント演算子のオペランドにならないこと
初期化子を持たない変数にも増減演算の禁止が適用されます。加えて、代入が現れる時点でその変数が definitely unassigned であること、つまり「まだ代入されていないと確定している箇所での1回きりの代入」であることが条件になります。int y; if (cond) y = 1; else y = 2; のように分岐ごとに1回だけ代入する書き方は、この条件を満たします。
メソッド・コンストラクタ・ラムダ式の仮引数と例外パラメータも、初期化子を持つローカル変数と同じ基準で判定されます。メソッドの引数をメソッド内で再代入していると、その引数はキャプチャできなくなります。
変数の種類別のキャプチャ可否
JDK 25 と JDK 27 の javac で実際にコンパイルして確認した結果です。2つの版で結果に差はありませんでした。
| 変数の種類 | キャプチャ | 補足 |
|---|---|---|
| 再代入していないローカル変数 | 可 | 実質final。final の明示は不要です |
| 再代入しているローカル変数 | 不可 | ラムダ式より後ろで代入していてもエラーになります |
| 基本for文の増減させるループ変数 | 不可 | インクリメントされるため実質finalになりません |
| 拡張for文のループ変数 | 反復内で再代入しなければ可 | 反復ごとに別の変数として扱われます |
| メソッドの仮引数(再代入なし) | 可 | 再代入すると不可になります |
| マルチcatchの例外パラメータ | 可 | 暗黙にfinalで、再代入自体がコンパイルエラーです |
| 単一catchの例外パラメータ | 条件付きで可 | 暗黙finalではないため、再代入していなければ可です |
| try-with-resourcesのリソース変数 | 可 | 暗黙にfinalで、再代入自体がコンパイルエラーです |
| インスタンスフィールド・staticフィールド | 可(制限なし) | 値のコピーではなく実行時に読むため、変更も反映されます |
暗黙にfinalとなるのは、JLS §4.12.4 が挙げるインタフェースのフィールド、try-with-resources のリソース変数、マルチcatch節の例外パラメータの3種類です。単一catch節の例外パラメータは「never implicitly declared final, but may be effectively final」とされ、再代入しなければキャプチャできます。再代入してからラムダ式で参照すると、javac は次のエラーを出します。
B_UniCatch.java:3: error: local variables referenced from a lambda expression must be final or effectively final
void m(){ try { throw new IOException("x"); } catch (IOException e) { e = new IOException("y"); foo(() -> System.out.println(e.getMessage())); } } }
拡張forと基本forで結果が分かれる理由
判定しているのは構文の種類ではなく、あくまでその変数が実質finalかどうかですが、2種類のfor文では結論が分かれます。JLS §15.27.2 はこの差を明示しています。
The restriction to effectively final variables includes standard loop variables, but not enhanced-for loop variables, which are treated as distinct for each iteration of the loop (§14.14.2).(実質finalの制限は通常のループ変数には及ぶが、拡張forのループ変数には及ばない。後者は反復ごとに別個の変数として扱われるため)
基本for文の i はループ全体で1つの変数を使い回してインクリメントするため、実質finalになりません。拡張for文の変数は反復ごとに新しく作られるので、ループ本体で再代入しない限り実質finalです。拡張forの変数をキャプチャしたラムダ式を3つ作って順に実行すると、それぞれが自分の反復時の値を保持しています。
List<Runnable> rs = new ArrayList<>();
for (String s : List.of("a", "b", "c")) {
rs.add(() -> System.out.print(s + " "));
}
rs.forEach(Runnable::run);
// 実行結果: a b c
これを基本for文で書き換えると、javac は該当行を指してエラーを出します。
Bad.java:17: error: local variables referenced from a lambda expression must be final or effectively final
rs.add(() -> System.out.println(i));
^
実質finalでも通らない「初期化されていない変数」
見落とされやすいのが、条件がもう1つあることです。JLS §15.27.2 は実質finalの規定に続けて「Any local variable used but not declared in a lambda body must be definitely assigned (§16 (Definite Assignment)) before the lambda body, or a compile-time error occurs.」と定めています。実質finalであっても、ラムダ式の本体より前に確実に代入されていなければコンパイルエラーになります。
次のコードは y に代入が1回しか現れないため実質finalの条件を満たしますが、cond が偽のときに未代入のままラムダ式へ到達しうるため通りません。
void m(boolean cond) {
int y;
if (cond) y = 1; // else 節がない
foo(() -> System.out.println(y));
}
// A_DefAssign.java:3: error: variable y might not have been initialized
メッセージに final も lambda も出てこないため、キャプチャの条件の話だと結びつきにくくなります。直し方は、全経路でそれぞれ1回だけ代入する形にすることです。else 節を足すか、int y = cond ? 1 : 0; のように条件演算子で初期化すれば通ります。
int y = 0; と初期値を与えるだけでは解決しません。初期化した上で if (cond) y = 1; を残すと、今度は代入式の左辺に現れて実質finalでなくなり、最初のエラーに変わります。確実な代入と実質finalの両方を同時に満たす必要があります。
void initThenReassign(boolean cond) {
int y = 0;
if (cond) y = 1; // 初期値を足しただけでは再代入が残る
foo(() -> System.out.println(y));
}
// error: local variables referenced from a lambda expression must be final or effectively final
キャプチャの制限が設けられている理由
「なぜ再代入する変数を使えないのか」はパフォーマンス上の都合として説明されることがありますが、言語仕様が挙げている根拠は別のところにあります。
制約を設ける理由と、finalから実質finalへ緩めた理由
JLS §15.27.2 は、実質finalへの制限の意図をこう説明しています。前半が制約そのものを設ける理由、後半が final の明示という当初の条件を緩めた理由です。
The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems. Compared to the final restriction, it reduces the clerical burden on programmers.(実質finalへの制限は、動的に変化するローカル変数へのアクセスを禁じる。そうした変数のキャプチャは並行性の問題を招く可能性が高いためである。また final 限定と比べて、プログラマの事務的な負担を減らす)
挙げられているのは並行性の問題と記述負担の軽減で、メモリ配置の最適化ではありません。ラムダ式や匿名クラスは別スレッドや後のタイミングで実行されうるため、呼び出し元のローカル変数が書き換わり続ける前提を許すと、実行順序やスレッド間の同期を書き手が管理しなければならなくなります。
匿名クラスは合成フィールドへの値コピー
コンパイラが実際に何をしているかは、バイトコードで確認できます。引数 n をキャプチャする匿名クラスをコンパイルし、生成されたクラスを javap で見ると、キャプチャした値を保持する合成フィールドが作られています。
class Cap$1 implements java.lang.Runnable {
final int val$n;
public void run();
}
呼び出し側のバイトコードでは、外側のインスタンスと n の値をコンストラクタへ渡しています。
java.lang.Runnable anon(int);
0: new #11 // class Cap$1
4: aload_0 // 外側のインスタンス
5: iload_1 // n の値
6: invokespecial #13 // Method Cap$1."<init>":(LCap;I)V
ラムダ式の場合は、独立したクラスファイルを出力せず、invokedynamic の呼び出し記述子にキャプチャした値の型が現れます。
java.lang.Runnable lambda(int);
0: iload_1
1: invokedynamic #7, 0 // InvokeDynamic #0:run:(I)Ljava/lang/Runnable;
どちらも「その時点の値を渡している」点は同じです。参照型であれば参照そのものがコピーされるので、参照先のオブジェクトは呼び出し元と共有され、中身を書き換えれば双方から見えます。コピーされないのは「どのオブジェクトを指しているか」を後から差し替える権利のほうです。
フィールド参照はthisのキャプチャ
インスタンスフィールドに制限がない理由も、バイトコードで分かります。フィールドを参照するラムダ式と、何も参照しないラムダ式を比べると、呼び出し記述子が変わります。
java.lang.Runnable usesField();
1: invokedynamic #15, 0 // InvokeDynamic #0:run:(LCap2;)Ljava/lang/Runnable;
java.lang.Runnable usesNothing();
0: invokedynamic #19, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable;
private void lambda$usesField$0(); // インスタンスメソッド
private static void lambda$usesNothing$0(); // staticメソッド
フィールドを参照する側は Cap2 のインスタンス、つまり this をキャプチャしており、生成されたメソッドもインスタンスメソッドです。フィールドがコピーされているわけではないので、ラムダ式を作った後でフィールドを書き換えると、実行時には新しい値が読まれます。
int local = 10;
field = 10;
Runnable showLocal = () -> System.out.println("local captured = " + local);
Runnable showField = () -> System.out.println("field read = " + field);
field = 99;
showLocal.run(); // local captured = 10
showField.run(); // field read = 99
static フィールドはキャプチャを経由せず、クラスを介して直接読み書きされるため、ラムダ式の中からインクリメントできます。「キャプチャできないから値を変えられない」のではなく、ローカル変数だけが値コピーの対象になっている、と理解すると挙動が一貫します。
JavaScript・Python・C#・C++の変数キャプチャとの比較
Javaの制限が厳しく見えるのは、他の多くの言語が「変数そのもの」を捕まえるからです。同じ「ループで関数を3つ作って後で呼ぶ」コードを各言語で動かすと、違いがはっきり出ます。
ループで作った関数が返す値の言語別実測
| 言語(実測環境) | 書き方 | 3つの関数の戻り値 | 関数内から外側の変数へ代入 |
|---|---|---|---|
| Java(JDK 25・27) | 基本for | コンパイルエラー | 不可 |
| Java(JDK 25・27) | 拡張for | a b c | 不可 |
| JavaScript(Node.js v26.5.0) | for (var i …) |
3 3 3 | 可(constは不可) |
| JavaScript(Node.js v26.5.0) | for (let i …) |
0 1 2 | 可(constは不可) |
| Python 3.14.6 | [lambda: i for i in range(3)] |
2 2 2 | nonlocal 宣言で可 |
| C#(.NET SDK 10.0.401) | for |
3 3 3 | 可 |
| C#(.NET SDK 10.0.401) | foreach |
0 1 2 | 可(反復変数は読み取り専用) |
「3 3 3」や「2 2 2」になるのは、関数が作られた時点の値ではなく、呼ばれた時点の変数の中身を読むためです。Pythonの公式FAQはこれを「it is accessed when the lambda is called — not when it is defined」と説明しており、回避策として lambda i=i: i のように既定値で値を固定する書き方を挙げています(実測でも 0 1 2 になりました)。
JavaScriptの let とC#の foreach が正しく動くのは、言語仕様が反復ごとに新しい変数を作ると決めているからです。ECMAScript 2026 §14.7.4 では、let で宣言したfor文のループ変数について CreatePerIterationEnvironment が反復ごとに新しい束縛を作ります。C#はC# 5(Visual Studio 2012)で foreach を反復ごとの新しい変数に変えましたが、for のループ変数はC#の仕様で「considered to be declared outside of the loop」とされたままです。Javaの拡張forも同じ「反復ごとに別の変数」という扱いで、基本forで同じ事故が起きる場面はコンパイルエラーになります。
C++のコピーキャプチャと参照キャプチャ
C++はキャプチャの方法を書き手が選びます。[=] はラムダ式を作った時点の値をコピーし、[&] は変数への参照を保持します。Apple clang 21 で、ラムダ式を作った後に x を10から99へ書き換えると、出力は [=] 10 / [&] 99 でした。
コピーしたほうの値を本体で書き換えるには mutable 指定が必要で、付けずに ++n と書くと「cannot assign to a variable captured by copy in a non-mutable lambda」でコンパイルが止まります。mutable を付けても書き換わるのはラムダ式が持つコピーだけで、外側の n は0のままでした。Javaのローカル変数のキャプチャは、C++の [=] から mutable の選択肢を取り除いた形に近い動きです。値をコピーしたうえで書き換えを禁じているので、コピーと元の変数がずれることがありません。
ラムダ式と匿名クラスのキャプチャの違い
キャプチャの条件は共通でも、書き換えたときに動作が変わる箇所が4つあります。匿名クラスからラムダ式への置き換えを検討するときは、ここを確認してから進めます。
thisが指すオブジェクトの違い
最も実害が出やすいのが this です。匿名クラスの中の this は匿名クラス自身のインスタンスを指しますが、ラムダ式の中の this は外側のインスタンスを指します。外側のクラスを Good として実測すると、次のようになります。
lambda this = Good
anonymous this = Good$1
anonymous Good.this= Good
匿名クラスで外側のインスタンスを参照するには Good.this と書く必要がありましたが、ラムダ式では this がそのまま外側を指します。this を使っている匿名クラスをそのままラムダ式へ書き換えると、参照先が変わって挙動が壊れます。JavaScriptのアロー関数も this を外側から引き継ぐ側で、この点はJavaのラムダ式と共通しています。
同名変数の宣言とdefaultメソッドの呼び出し
ラムダ式の本体は、外側のメソッドと同じスコープの一部として扱われます。JLS §6.4 はラムダ式について「local variables outside the expression cannot be shadowed」と述べており、外側のローカル変数と同じ名前の変数をラムダ式の中で宣言するとコンパイルエラーになります。ラムダ式の引数名も同じ扱いです。
int x = 1;
Runnable r = () -> { int x = 2; System.out.println(x); };
// error: variable x is already defined in method m()
IntUnaryOperator f = x -> x + 1;
// error: variable x is already defined in method m()
匿名クラスは新しいクラス宣言なので、同じ int x = 2; を run() の中に書いても通り、内側は2・外側は1のまま動きました。使わない引数の名前がぶつかる場合は、Java 22 で正式化された無名変数 _ を使えます。(_, _) -> x は JDK 25 でコンパイル・実行できました。
もう1つの差は、関数型インタフェースの default メソッドです。匿名クラスの中では、実装しているインタフェースの hello() を名前だけで呼べます。ラムダ式の本体では名前が外側の文脈で解決されるため、同じ () -> hello() は「cannot find symbol」になります。JDK 25 と JDK 27 のどちらでも同じエラーでした。
匿名クラスとラムダ式で分かれるクラスファイルと実装クラス
今回使用したjavacでは、匿名クラスに対応する独立したクラスファイルを生成しますが、ラムダ式に対応する独立したクラスファイルは生成しません。ラムダ式1つと匿名クラス1つを含むクラスをコンパイルすると、出力されるクラスファイルは2つだけです。
$ javac Name.java
$ ls Name*.class
Name$1.class // 匿名クラスの分
Name.class // ラムダ式の分は生成されない
ラムダ式の実装クラスをどう用意するかは、言語仕様ではなく実行環境の実装に委ねられています。Temurin 25 と Zulu 27 では実行時に生成され、隠しクラス(hidden class)になりました。
lambda class = Name$$Lambda/0x0000000150080210
lambda isHidden = true
anon class = Name$1
anon isHidden = false
隠しクラスは通常のクラス検索の対象になりません。上の名前をそのまま Class.forName へ渡すと ClassNotFoundException になります。名前を前提にしたリフレクションや、クラスファイルの数を数えるビルド検証は、匿名クラスからラムダ式への置き換えで結果が変わります。
非キャプチャラムダのインスタンス再利用
何もキャプチャしないラムダ式は、評価のたびに新しいオブジェクトを作るとは限りません。ループ内で同じラムダ式を3回評価して System.identityHashCode を取ると、JDK 25 では非キャプチャのラムダ式だけ同じ値が返りました。
non-capturing #0 identity=1654589030
non-capturing #1 identity=1654589030
non-capturing #2 identity=1654589030
capturing #0 identity=257895351
capturing #1 identity=1929600551
capturing #2 identity=1690716179
ただしこれは仕様上の保証ではありません。LambdaMetafactory の javadoc は「Capture may involve allocation of a new function object, or may return a suitable existing function object. The identity of a function object produced by capture is unpredictable」と述べています。ラムダ式のインスタンスを == で比較したり、ロックの対象にしたりする設計は避けてください。
よくあるコンパイルエラーと回避策
キャプチャ関連のエラーはメッセージが数種類に分かれており、どれが出たかで原因を切り分けられます。
エラーメッセージ別の原因と対処
以下は英語ロケールでの表示で、日本語環境では「ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります」のように訳されます。英語の文字列で検索しても見つからないときは、javac -J-Duser.language=en で表示を揃えると原文と突き合わせられます。
| エラーメッセージ(冒頭) | 原因 | 対処 |
|---|---|---|
| local variables referenced from a lambda expression… | ラムダ式が参照する変数を再代入 | 再代入をやめるか、別の変数へ写す |
| local variables referenced from an inner class… | 匿名クラス等が参照する変数を再代入 | 同上(構文の違いだけ) |
| variable y might not have been initialized | 実質finalだが確実に代入済みでない | 全経路で1回だけ代入する |
| variable x is already defined in method … | ラムダ式内で外側と同名の変数を宣言 | 名前と参照箇所を変更する。未使用の変数・引数に限り _ も可 |
| multi-catch parameter e may not be assigned | マルチcatchの例外パラメータへ再代入 | 別の変数を用意する |
| auto-closeable resource ac may not be assigned | リソース変数へ再代入 | リソース変数は差し替えない |
最初の2つはメッセージの差が「lambda expression」と「inner class」だけなので、どちらの構文で書いているかを確認する目印になります。匿名クラスをラムダ式へ移す際は、同名変数によるコンパイルエラーに加え、this の参照先が変わることによる動作の違いも確認してください。JDKの更新とあわせた既存Javaシステムの改修は、フルスクラッチ開発の範囲で相談を受け付けています。
配列ホルダの競合リスクとAtomicInteger・集計APIの使い分け
ラムダ式の中から外側へ値を持ち出したい場合、要素数1の配列や AtomicInteger をホルダにする書き方が知られています。変数そのものは再代入されないため実質finalのままで、中身だけが変わります。
int[] holder = new int[1];
Runnable inc = () -> holder[0]++;
inc.run(); inc.run(); inc.run();
System.out.println(holder[0]); // 3
AtomicInteger counter = new AtomicInteger();
Runnable inc2 = counter::incrementAndGet;
inc2.run(); inc2.run();
System.out.println(counter.get()); // 2
これは、仕様が避けようとした並行性の問題を自分で引き受ける書き方です。配列ホルダには何の同期もないため、複数スレッドから更新すると結果が壊れます。スレッドをまたぐ可能性があるなら AtomicInteger のような並行処理向けのクラスを使い、単一スレッドで完結する集計なら Stream API の reduce や collect に寄せます。どこまでラムダ式に任せるかは、Javaラムダ式の使いどころで扱っている「副作用を持ち込まない」基準と同じ考え方です。
thisキャプチャによるメモリリーク
インスタンスフィールドやインスタンスメソッドを参照するラムダ式は this をキャプチャします。このラムダ式を長寿命のオブジェクト(イベントリスナーの登録先、キャッシュ、staticなコレクションなど)へ渡すと、外側のインスタンス全体が到達可能なまま残ります。
必要な値だけをローカル変数へ取り出してからキャプチャすれば、this は捕まりません。置き換えると、呼び出し記述子から外側の型が消え、生成されるメソッドも static になります。
Runnable viaField() { return () -> System.out.println(name); }
Runnable viaLocal() { String n = name; return () -> System.out.println(n); }
// javap -p -c の該当行
viaField(): invokedynamic // InvokeDynamic #0:run:(LVerify;)Ljava/lang/Runnable;
viaLocal(): invokedynamic // InvokeDynamic #1:run:(Ljava/lang/String;)Ljava/lang/Runnable;
private void lambda$viaField$0();
private static void lambda$viaLocal$0(java.lang.String);
取り出した値そのものが外側のインスタンスを参照しているオブジェクトであれば、this を直接キャプチャしなくても参照経路は残ります。効果があるのは、文字列や数値のように外側と切り離された値を取り出す場合です。リスナーの登録解除を含めた対処はメモリリークの原因の特定方法と直し方で整理しています。
よくある質問
capturing の意味と、キャプチャリングとキャプチャの違いは何ですか?
英語の capturing は capture(捕らえる・取り込む)の -ing 形で、「捕らえること」を意味します。Javaの文脈では「キャプチャリング」と「キャプチャ」は同じことを指し、言語仕様では variable capture のように capture 側の語が使われます。分野をまたぐと指す対象が変わり、DOMではイベントのキャプチャリングフェーズ、ネットワークではパケットキャプチャを指します。
なぜキャプチャする変数にfinalが必要なのですか?
Java 8 以降は final の明示は不要で、実質final であれば足ります。制限そのものが残っている理由として、Java言語仕様 §15.27.2 は、動的に変化するローカル変数のキャプチャが並行性の問題を招きやすいことを挙げています。
実質final(effectively final)とは何ですか?
final が付いていないのに、final が付いているのと同じ扱いを受ける変数のことです。初期化子を持つローカル変数なら、代入式の左辺にもインクリメント・デクリメントのオペランドにもならないものが該当します。初期化子を持たない変数では、各代入の直前に未代入と確定しており、インクリメント・デクリメントの対象にもならないことが必要です。実質finalの変数に後から final を付けてもコンパイルエラーにはなりません。
ラムダ式の中からローカル変数を書き換える方法はありますか?
変数そのものへ代入することはできません。要素数1の配列や AtomicInteger、可変オブジェクトのフィールドを経由すれば中身は変更できますが、並行性の問題を自分で管理する必要があります。集計が目的なら Stream API の reduce や collect を使うか、結果を戻り値で受け取る設計に変えるほうが安全です。
匿名クラスとラムダ式はどちらを使うべきですか?
関数型インタフェース1つを実装するだけならラムダ式が簡潔で、クラスファイルも増えません。複数のメソッドを実装する、自分自身の状態を持つ、this で自分自身を参照する、といった場合は匿名クラスが必要です。既存の匿名クラスを機械的にラムダ式へ置き換えると this の指す先と名前の解決が変わるため、個別に確認してください。
キャプチャリングはパフォーマンスに影響しますか?
キャプチャするラムダ式は評価のたびにオブジェクトが割り当てられる可能性があり、何もキャプチャしないラムダ式は既存のインスタンスが返されることがあります。ただし LambdaMetafactory の javadoc はこの同一性を保証していないため、書き方を変える前に計測してください。