ラムダでは、ローカル変数はfinalである必要がありますが、インスタンス変数はそうではありません。なんでそうなの?
フィールドとローカル変数の基本的な違いは、JVMがラムダインスタンスを作成するときに、ローカル変数がcopiedであることです。一方、フィールドへの変更は外部クラスインスタンスにも伝播されるため、フィールドは自由に変更できます(それらのscopeは外部全体ですボリスが下で指摘したように、クラス)。
匿名クラス、クロージャー、labmdasについて考える最も簡単な方法は、variable scopeの観点からです。クロージャーに渡すすべてのローカル変数にコピーコンストラクターが追加されることを想像してください。
プロジェクトlambdaのドキュメント: State of the Lambda v4
セクションの下7。変数capture、それが言及されている....
可変ローカル変数のキャプチャを禁止するのが目的です。その理由は、次のようなイディオムがあるからです。
int sum = 0; list.forEach(e -> { sum += e.size(); });
基本的にシリアルです。競合状態のないこのようなラムダ本体を記述することは非常に困難です。そのような関数がキャプチャスレッドをエスケープできないように(できればコンパイル時に)強制しない限り、この機能は解決するよりも多くの問題を引き起こす可能性があります。
編集:
ここで注意すべきもう1つのことは、内部クラス内でローカル変数にアクセスすると、ローカル変数が内部クラスのコンストラクターに渡され、非最終変数の値は構築後に変更できるため、非最終変数では機能しません。
インスタンス変数の場合、コンパイラはクラスの参照を渡し、クラスの参照はインスタンス変数へのアクセスに使用されます。したがって、インスタンス変数の場合は必要ありません。
PS:匿名クラスは最終ローカル変数(Java SE 7のみ)にアクセスできますが、Java SE 8ではラムダ内部でも最終変数に効果的にアクセスできます。内部クラスも同様です。
Java 8 in Action 本では、この状況は次のように説明されています。
ローカル変数にこれらの制限がある理由を自問するかもしれません。まず、インスタンス変数とローカル変数が舞台裏で実装される方法に大きな違いがあります。インスタンス変数はヒープに保存されますが、ローカル変数はスタックに保存されます。ラムダがローカル変数に直接アクセスでき、ラムダがスレッドで使用されている場合、ラムダを使用しているスレッドは、変数を割り当てたスレッドが割り当てを解除した後に変数にアクセスしようとする可能性があります。したがって、Javaは、元の変数へのアクセスではなく、そのコピーへのアクセスとして、無料のローカル変数へのアクセスを実装します。これは、ローカル変数が1回だけ割り当てられている場合、違いはありません。したがって、制限があります。第二に、この制限は、外部変数を変化させる典型的な命令型プログラミングパターン(後の章で説明するように、簡単な並列化を妨げる)も妨げます。
インスタンス変数は常に、オブジェクトへの参照に対するフィールドアクセス操作、つまりsome_expression.instance_variable
を介してアクセスされるためです。 instance_variable
のようにドット表記で明示的にアクセスしない場合でも、暗黙的にthis.instance_variable
として扱われます(または、外部クラスのインスタンス変数にアクセスする内部クラスの場合は、OuterClass.this.instance_variable
、これは内部this.<hidden reference to outer this>.instance_variable
)です。
したがって、インスタンス変数に直接アクセスすることはなく、直接アクセスしている実際の「変数」はthis
(割り当て可能でないため「実質的に最終」です)、または他の式の先頭にある変数です。 。
ラムダ本体から参照できる変数について尋ねているようです。
JLS§15.27.2 から
使用されているがラムダ式で宣言されていないローカル変数、仮パラメーター、または例外パラメーターは、最終宣言するか、実質的に最終宣言する必要があります(§4.12.4)。
したがって、変数をfinal
として宣言する必要はありません。単に「効果的に最終」であることを確認する必要があります。これは、匿名クラスに適用されるルールと同じです。
Lambda式内では、周囲のスコープから最終変数を効果的に使用できます。事実上、変数finalを宣言することは必須ではありませんが、ラムダ式内でその状態を変更しないようにしてください。
クロージャ内でこれを使用することもできます。クロージャは無名関数であり、クラスが関連付けられていないため、「this」を使用するとエンクロージングオブジェクトがラムダ自体ではありません。
そのため、finalで宣言されておらず、実質的にfinalでもない囲みクラスのフィールド(private Integer i;など)を使用すると、コンパイラが代わりにトリックを作成し、「this」(this.i)を挿入するので、引き続き機能します。
private Integer i = 0;
public void process(){
Consumer<Integer> c = (i)-> System.out.println(++this.i);
c.accept(i);
}
以下はコード例です。これも予想していなかったので、ラムダ以外は変更できないと予想していました。
public class LambdaNonFinalExample {
static boolean odd = false;
public static void main(String[] args) throws Exception {
//boolean odd = false; - If declared inside the method then I get the expected "Effectively Final" compile error
runLambda(() -> odd = true);
System.out.println("Odd=" + odd);
}
public static void runLambda(Callable c) throws Exception {
c.call();
}
}
出力:Odd = true
はい、インスタンスのメンバー変数を変更できますが、変数を処理するときと同じように、インスタンスCANNOTを変更できます。
前述のようなもの:
class Car {
public String name;
}
public void testLocal() {
int theLocal = 6;
Car bmw = new Car();
bmw.name = "BMW";
Stream.iterate(0, i -> i + 2).limit(2)
.forEach(i -> {
// bmw = new Car(); // LINE - 1;
bmw.name = "BMW NEW"; // LINE - 2;
System.out.println("Testing local variables: " + (theLocal + i));
});
// have to comment this to ensure it's `effectively final`;
// theLocal = 2;
}
ローカル変数を制限する基本原則は、 データと計算の妥当性
2番目のスレッドによって評価されたラムダに、ローカル変数を変更する機能が与えられた場合。異なるスレッドから可変ローカル変数の値を読み取る機能でさえ、古いデータの読み取りを避けるために同期またはvolatileの使用が必要になります。
しかし、わかっているように、ラムダの 主な目的は
これにはさまざまな理由がありますが、Javaプラットフォームの最も差し迫った理由は、コレクションの処理を複数のスレッドに分散しやすくすることです。
ローカル変数とはまったく異なり、ローカルinstanceは、グローバルにsharedであるため、変更できます。これは ヒープとスタックの差 :を介してよりよく理解できます。
オブジェクトが作成されると、常にそのオブジェクトはヒープスペースに保存され、スタックメモリにはそのオブジェクトへの参照が含まれます。スタックメモリには、ヒープスペース内のオブジェクトへのローカルプリミティブ変数と参照変数のみが含まれます。
まとめると、本当に重要だと思う2つのポイントがあります。
instanceを効果的にfinalにするのは本当に難しい。これは多くの無意味な負担を引き起こす可能性があります(ネストされたクラスを想像してください)。
インスタンス自体はすでにグローバルに共有されており、ラムダもスレッド間で共有可能です。したがって、mutationを処理し、このmutationを渡したいため、スレッドは適切に連携できます。
ここでのバランスは明確です。あなたが何をしているのかを知っているなら、あなたはそれを簡単にできますが、そうでなければデフォルトの制限は陰湿なバグを避けるのに役立ちます。
追伸同期がインスタンスの突然変異で必要な場合は、直接 ストリーム削減メソッド を使用できます。またはインスタンスの突然変異に依存関係の問題がある場合は、 thenApply
またはthenCompose
in Function while mapping
または同様のメソッド。