ラムダ式について
疑問に思ったこと
- ラムダ式の外で作成した変数をラムダ式内で使えるのはどうして?
例えば下のソースコードを見てみる。
public class DemoApplication {
public static void main(String[] args) {
Dog dog = new Dog();
Runnable runnable = () -> {
dog.call();
};
runnable.run();
}
}
このソースコードは問題なく動くが少し納得いかないところがある。
納得いかないのはこの部分。
Runnable runnable = () -> {
dog.call();
};
ラムダ式はメソッドの中で関数を実装してインスタンスを作成している。
ラムダ式を使わずに書くと下のようになる。
※書き換えのバリエーションはほかにもあります
public class DemoApplication2 {
public static void main(String[] args) {
Dog dog = new Dog();
RunnableImple runnable = new RunnableImple();
runnable.run();
}
static class RunnableImple implements Runnable {
@Override
public void run() {
Dog dog = new Dog();
dog.call();
}
}
}
ラムダ式で書いたメソッドの実装部分と内部クラスで書いた実装部分を比較すると、Dog dog = new Dog();
の部分が省略できていることに気づくと思います。
直感的にはラムダ式実装の中で使用しているdog
変数は、ラムダ式を呼び出しているクラス(DemoApplication
クラス)で変数宣言していてそれを参照していると考えるかと思います。
しかしよく考えると冒頭に記述した疑問に行き当たります。
ラムダ式は内部クラスを簡単に記述できるようにしているだけで、実際のところはちゃんとインスタンスを作成しているはずです。
つまり、ラムダ式を定義している側のクラスインスタンスとラムダ式で定義されたクラスインスタンスは別インスタンスのはず...
どうして別インスタンスのはずのラムダ式で定義されたクラスインスタンスからラムダ式を定義している側のクラスインスタンスの変数を参照できるのでしょうか?
TL;DR
- ラムダ式はクロージャとも呼ばれる。
- ラムダ式は自身を定義しているメソッドの変数の参照を**「閉じ込める(close over)」**
- (当たり前だが)「閉じ込める」というのはイメージであり実際にはメモリ上でその値を固定している。
- 値が固定されているので別インスタンスでも参照することができる。
- 値は固定されていなければならないので、参照する変数はラムダ式の内外・前後にてその値を更新することはできない。(コンパイルエラーになる)
- finalもしくは事実上のfinal(=変数を代入してそれ以降値を変えていない)でなければならない。
調査
調査実施
ChatGPTへ質問して調査の目星をつける
まずはChatGPTに質問して回答をもらいます。
実行したプロンプトは『ラムダ式の外側で作成したインスタンスをラムダ式の中で利用できるのはなぜですか?』です。
Javaのラムダ式では、ラムダ式の外側で作成した変数をラムダ式の中で利用できる仕組みがあります。これは、ラムダ式がクロージャ(closure)として振る舞うためです。
クロージャとは、外部のスコープにある変数を自身のスコープ内で参照・操作できる関数のことです。具体的には、以下のような特性があります:
1. **変数のキャプチャ(Capture)**:
ラムダ式の中で外部スコープの変数を参照すると、その変数の値を保持する仕組みが自動的に生成されます。これにより、ラムダ式が作成された時点での外部変数の値が保持され、後でラムダ式が実行されてもその値が使われます。
2. **フレキシブルな利用**:
ラムダ式を使うことで、より動的で柔軟なコードを書くことができます。外部の状態をラムダ式内で利用することで、コードをよりコンパクトにしたり、より読みやすくしたりすることができます。
例えば、次のようなコードでは、ラムダ式が外部の変数 `threshold` をキャプチャして使用しています:
public class LambdaExample {
public static void main(String[] args) {
int threshold = 5;
// ラムダ式内で外部の変数 threshold を参照
Runnable runnable = () -> {
System.out.println("Threshold is: " + threshold);
};
runnable.run();
}
}
この例では、ラムダ式 `() -> { System.out.println("Threshold is: " + threshold); }` 内で `threshold` 変数を参照しています。`threshold` の値はラムダ式が実行される時点でのものが使われます。
このように、ラムダ式がクロージャとして機能することで、外部スコープの変数を効果的に利用することができます。
さらに質問します。実行したプロンプトは『それでは、ラムダ式の後などにその変数を再代入などするとコンパイルエラーになりますか?』です。
Javaのラムダ式でキャプチャした変数に対して再代入を行うと、コンパイルエラーが発生します。これは、ラムダ式がキャプチャした変数は事実上 **final** として扱われるためです。
具体的には、次のようなコードで説明します。
public class LambdaExample {
public static void main(String[] args) {
int threshold = 5;
// ラムダ式内で外部の変数 threshold を参照
Runnable runnable = () -> {
// threshold の値を参照するだけなので問題ない
System.out.println("Threshold is: " + threshold);
// コンパイルエラー:再代入はできない
// threshold = 10;
};
runnable.run();
}
}
上記の例では、ラムダ式内で `threshold` 変数を参照していますが、`threshold = 10;` のような再代入を試みると、コンパイルエラーが発生します。ラムダ式がキャプチャした変数は、実質的には final として扱われるため、再代入が許されません。
この制約は、ラムダ式の外部スコープの変数が安全に利用されることを保証するためにあります。もし、ラムダ式内で再代入が許可されると、複数のスレッドからアクセスされる場合に競合状態(race condition)が発生するリスクが高まります。
したがって、ラムダ式でキャプチャした変数に対しては、再代入を試みるとコンパイルエラーが発生し、その変数の値はラムダ式が作成された時点の値のまま固定されます。
おおむね聞きたいことは聞くことができました。
情報を探す
OracleのサイトでChatGPTの回答に近いページを見つけることはできました。
参考ページ:リンク
この資料の5ページ目、一番左上に【変数のキャプチャ】というセクションがあります。
ここには以下のように記載されています。
変数のキャプチャ:ラムダ式がクロージャと呼ばれる理由の1つは次のとおりです。
関数リテラル(前項までに紹介したような形式の関数)は、その関数リテラルを取り囲むスコープ(エンクロージング・スコープ)内にある
(かつ、その関数リテラルの外側にある)変数の参照を「閉じ込める(close over)」ことができます。
Javaの場合、エンクロージング・スコープは通常、ラムダ式が定義されているメソッドとなります。
インナー・クラスでもこのような構造は可能です。
しかし、インナー・クラスの場合、エンクロージング・スコープからは「final」の変数しか参照できないという制約がありました。
これは、これまでJava開発者の頭を悩ませてきたインナー・クラスに関する数々のテーマのうちでも、特に重要なものです。
ラムダ式によってこの制約が緩和されます。ただし、その程度は少しだけです。
変数の参照が「事実上final」である場合、つまりfinal修飾子がなくとも変数の内容が不変である場合、
ラムダ式はその変数を参照できます(リスト13 )。
messageは、ラムダ式が定義された場所を取り囲むmainメソッドのスコープ内において変更されないため、事実上finalです。
そのため、rに格納されるRunnableラムダ式の内部から参照できます。
上記に記載されている通り、
- ラムダ式はラムダ式を定義している関数内でfinal変数または事実上finalである変数を参照できる。
-
「参照」しているのであり、ラムダ式で定義されたクラスインスタンスに変数をコピーしているわけではない。
ということがわかりました。
まとめ
- ラムダ関数で定義されたインスタンスはラムダ関数を定義しているメソッドのfinal変数または事実上finalである変数を参照することができる。
- 上記を守らない変数参照をするとコンパイルエラーになる。