2016-03-23 6 views
13

これはthis questionに基づいています。私たちが知っているように一つの理由は、ラムダということです、関数型プログラミングを行う際に、ラムダの内部で現在の状態を参照するために良い習慣ではありません最後の文字列フィールドを参照するときにラムダが囲むインスタンスをキャプチャする必要があるのはなぜですか?

public class TestClass { 
    public static void main(String[] args) { 
     MyClass m = new MyClass(); 
     Consumer<String> fn = m.getConsumer(); 

     System.out.println("Just to put a breakpoint"); 
    } 
} 

class MyClass { 
    final String foo = "foo"; 

    public Consumer<String> getConsumer() { 
     return bar -> System.out.println(bar + foo); 
    } 
} 

:メソッドは、ラムダ式に基づいてConsumerを返します。この例を考えてみましょうラムダ自体が範囲外になるまでガベージコレクションされない囲まれたインスタンスを取得します。

しかし、final文字列に関連し、この特定のシナリオでは、代わりに示すように、全体MyClassインスタンスを囲んで、コンパイラはちょうど戻っラムダに(定数プールからの)定数(final)文字列fooを同封している可能性がありそうですデバッグ時には(下にブレイクを入れてSystem.out.println)。ラムダが特別なinvokedynamicバイトコードにコンパイルされる方法と関係がありますか?

enter image description here

+0

あなたは明確にすることができますか?最初に、あなたが '' MyClass''インスタンスを囲むことを期待していると説明し、その後、あなたはこの '' args $ 1''を見て驚いています。 –

+1

@JeanLogaert質問は*最終的な変数を使っている間に*どうして*なぜですか? –

+0

@JeanLogeart質問定数として扱われる 'final String'に特に関連しています。 – manouti

答えて

3

ラムダがthisの代わりにfooをキャプチャしていた場合、別の結果が得られることがあります。次の例を考えてみましょう:

public class TestClass { 
    public static void main(String[] args) { 
     MyClass m = new MyClass(); 
     m.consumer.accept("bar2"); 
    } 
} 

class MyClass { 
    final String foo; 
    final Consumer<String> consumer; 

    public MyClass() { 
     consumer = getConsumer(); 
     // first call to illustrate the value that would have been captured 
     consumer.accept("bar1"); 
     foo = "foo"; 
    } 

    public Consumer<String> getConsumer() { 
     return bar -> System.out.println(bar + foo); 
    } 
} 

出力:ラムダで撮影した

bar1null 
bar2foo 

foo場合、それはnullとしてキャプチャされるだろうと2回目の呼び出しはbar2nullを印刷します。ただし、MyClassインスタンスが取得されているため、正しい値が出力されます。

もちろん、これは醜いコードであり、少し工夫されていますが、より複雑な現実のコードでは、がやや簡単にになる可能性があります。

唯一の真のuglyのことは、コンシューマを介してコンストラクタ内に割り当てられるfooの読み込みを強制していることに注意してください。当時消費者自身を構築することはfooを読むことが期待されていないので、すぐに使用しない限り、fooを割り当てる前に構築することはまだ合法です。

コンパイラはfooを割り当てる前に、コンストラクタで同じconsumerを初期化させませんが - おそらく最高:-)

+0

これは面白いですが、コード自体のバグのように見える傾向があります。 – manouti

+0

@ AR.3 'bar1null'の出力はおそらくコード内のバグと見なされますが、' bar2null'を出力するための2番目の呼び出しは期待できません.JVMのバグと見なされます。 –

+0

私はあなたに同意します。しかし、ある日、最終フィールドがラムダで異なって捕捉されれば、出力で2つの 'bar1null'を見るのは驚かないでしょう。 – manouti

3

あなたは正しいです、問題のフィールドがfinalですが、それがないので、それは技術的に、そうすることができます。それが返さラムダはMyClassのインスタンスへの参照を保持していることが問題である場合

しかし、あなたは簡単にそれを自分で修正することができます:

public Consumer<String> getConsumer() { 
    String f = this.foo; 
    return bar -> System.out.println(bar + f); 
} 

注意、フィールドはfinalされていなかった場合に、ラムダが実行された時点で元のコードは実際の値を使用しますが、ここに記載されているコードはgetConsumer()メソッドが実行された時点の値を使用します。

+0

厳密には正しいとは言えません。他の答えが指摘したように、セマンティクスはインスタンスメンバーの初期化順序のために異なるかもしれません。さらに、フィールドは実際には最終的ですが、ラムダは実際にはgetConsumerが呼び出された時点で実際の値を参照しています。最終的な不変性は付随的である。 –

14

bar + foobar + this.fooの略称です。私たちは暗黙のうちにインスタンスメンバーを暗黙的に取得していることを忘れてしまいました。だから、あなたのラムダはで、this.fooではありません。

あなたの質問が「この機能が異なって実装されている可能性がありますか」と答えた場合は、「おそらくはい」です。ラムダキャプチャの仕様/実装を、これを含むさまざまな特別なケースで段階的に優れたパフォーマンスを提供する目的で任意に複雑にすることができました。

thisの代わりにthis.fooをキャプチャするように仕様を変更しても、パフォーマンスはあまり変わりません。それはまだキャプチャラムダであり、余分なフィールド逆参照よりもはるかに大きなコストの考慮事項です。だから私はこれが実際のパフォーマンスを向上させるとは思わない。

+1

あなたの答えをありがとう!その根拠は、ラムダがインスタンスを囲むインスタンスへの参照を保持する潜在的な「リーク」の数を減らすことができるということです(Java 8がますます使用されるように)。 – manouti

+1

FYI数年前Nealはこの精神でC#ラムダキャプチャコードにいくつかの変更を加えることを検討していました。彼がこれまでにしたことは分かりません。 –

+0

これにも慎重なトレードオフが必要であることにも注意してください。ラムダが 'this.x'への参照を1つ持っているのは明らかですが、2つはどうでしょうか?三?九十六?ある時点で、追加の血管を捕捉する「治癒」は疾患よりも悪い。 –

1

注用というコンパイル時の定数が可変に任意の通常のJavaアクセスのために、主張されている一部の人々とは異なり、初期値の順序問題に影響されません。

私たちは、次の例によりこれを示すことができ

abstract class Base { 
    Base() { 
     // bad coding style don't do this in real code 
     printValues(); 
    } 
    void printValues() { 
     System.out.println("var1 read: "+getVar1()); 
     System.out.println("var2 read: "+getVar2()); 
     System.out.println("var1 via lambda: "+supplier1().get()); 
     System.out.println("var2 via lambda: "+supplier2().get()); 
    } 
    abstract String getVar1(); 
    abstract String getVar2(); 
    abstract Supplier<String> supplier1(); 
    abstract Supplier<String> supplier2(); 
} 
public class ConstantInitialization extends Base { 
    final String realConstant = "a constant"; 
    final String justFinalVar; { justFinalVar = "a final value"; } 

    ConstantInitialization() { 
     System.out.println("after initialization:"); 
     printValues(); 
    } 
    @Override String getVar1() { 
     return realConstant; 
    } 
    @Override String getVar2() { 
     return justFinalVar; 
    } 
    @Override Supplier<String> supplier1() { 
     return() -> realConstant; 
    } 
    @Override Supplier<String> supplier2() { 
     return() -> justFinalVar; 
    } 
    public static void main(String[] args) { 
     new ConstantInitialization(); 
    } 
} 

それは出力しますので、

var1 read: a constant 
var2 read: null 
var1 via lambda: a constant 
var2 via lambda: null 
after initialization: 
var1 read: a constant 
var2 read: a final value 
var1 via lambda: a constant 
var2 via lambda: a final value 

、あなたが見ることができるよう、realConstantフィールドへの書き込みはまだ実現しなかったという事実をスーパーコンストラクタが実行されたときに、たとえラムダ式を介してアクセスしたとしても、真のコンパイル時定数の初期化されていない値は見られません。技術的には、フィールドは実際には読み取られないためです。

また、不快なReflectionハックは、同じ理由で通常のJavaアクセス時にコンパイル時定数に影響を与えません。

  1. リフレクションもには影響しません、私たちは二つのことを示して

    lambda: foo 
    captured obj: true 
    via Reflection: bar 
    ordinary field access: foo 
    

    を:それは印刷し

    public class TestCapture { 
        static class MyClass { 
         final String foo = "foo"; 
         private Consumer<String> getFn() { 
          //final String localFoo = foo; 
          return bar -> System.out.println("lambda: " + bar + foo); 
         } 
        } 
        public static void main(String[] args) throws ReflectiveOperationException { 
         final MyClass obj = new MyClass(); 
         Consumer<String> fn = obj.getFn(); 
         // change the final field obj.foo 
         Field foo=obj.getClass().getDeclaredFields()[0]; 
         foo.setAccessible(true); 
         foo.set(obj, "bar"); 
         // prove that our lambda expression doesn't read the modified foo 
         fn.accept(""); 
         // show that it captured obj 
         Field capturedThis=fn.getClass().getDeclaredFields()[0]; 
         capturedThis.setAccessible(true); 
         System.out.println("captured obj: "+(obj==capturedThis.get(fn))); 
         // and obj.foo contains "bar" when actually read 
         System.out.println("via Reflection: "+foo.get(capturedThis.get(fn))); 
         // but no ordinary Java access will actually read it 
         System.out.println("ordinary field access: "+obj.foo); 
        } 
    } 
    

    :バックな修正値を読み込むための唯一の方法は、リフレクション経由でコンパイル時定数

  2. 周囲のオブジェクトは、使用されなくてもキャプチャされています。

インスタンスフィールドへのアクセスには、には、フィールドが実際には読み取られない場合でも、そのフィールドのインスタンスを取得するためのλ式がである必要がありますが、残念ながら私は少し恐ろしいです現在のJava言語仕様、内の値の取り込みやthisに関するいかなる文を見つけることができませんでした:

我々は、ラムダ式でないアクセスするインスタンスフィールドを作成するという事実に慣れthisへの参照を持たないインスタンスですが、それでも実際には保証されていませんb現在の仕様です。この省略はすぐに修正されることが重要です...

関連する問題