2016-03-23 12 views
13

Dies basiert auf this question. Betrachten Sie dieses Beispiel, in dem ein Verfahren zur Herstellung ein Consumer basierend auf einem Lambda-Ausdruck gibt:Warum muss ein Lambda die umschließende Instanz erfassen, wenn ein abschließendes String-Feld referenziert wird?

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); 
    } 
} 

Wie wir wissen, es ist keine gute Praxis einen aktuellen Zustand in einem Lambda zu verweisen, wenn die funktionale Programmierung zu tun, ist ein Grund, dass das Lambda würde die umschließende Instanz erfassen, die nicht als Garbage-Collection erfasst wird, bis das Lambda selbst nicht mehr im Gültigkeitsbereich ist.

jedoch in diesem speziellen Szenario final Saiten bezogen, so scheint es die Compiler nur die Konstante eingeschlossen haben könnten (final) string foo (aus dem Konstanten-Pool) in dem zurücken lambda, anstatt die ganze MyClass Instanz umschließt, wie gezeigt unten während des Debuggens (Platzierung der Unterbrechung bei System.out.println). Hat es damit zu tun, wie Lambdas zu einem speziellen invokedynamic Bytecode kompiliert werden?

enter image description here

+0

Können Sie klären?Sie erklären zuerst, dass Sie die umschließende '' MyClass''-Instanz erwarten und danach sind Sie überrascht, diese '' args $ 1'' zu sehen –

+1

@JeanLogaert Die Frage ist * warum * es passiert immer noch bei der Verwendung einer endgültigen Variablen –

+0

@JeanLogeart Die Frage bezieht sich speziell auf "final String", die als Konstante betrachtet wird. – manouti

Antwort

3

Wenn das Lambda foo anstelle von this erfasst, können Sie in einigen Fällen ein anderes Ergebnis erhalten. Betrachten Sie das folgende Beispiel:

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); 
    } 
} 

Ausgang:

bar1null 
bar2foo 

Wenn foo von dem Lambda gefangen genommen wurde, würde es als null und der zweite Anruf würde bar2null drucken erfaßt werden. Da jedoch die MyClass Instanz erfasst wird, wird der richtige Wert ausgegeben.

Natürlich ist dies ein hässlicher Code und ein bisschen künstlich, aber in komplexeren, realen Code könnte ein solches Problem etwas leicht auftreten.

Beachten Sie, dass die einzig wahre hässlich Sache ist, dass wir ein Lesen des zu-zugewiesen foo im Konstruktor durch den Verbraucher zwingen. Es wird nicht erwartet, dass der Verbraucher selbst zu dieser Zeit foo liest, also ist es immer noch legitim, ihn zu erstellen, bevor er foo zuweist - solange Sie ihn nicht sofort verwenden.

jedoch die Compiler lassen Sie nicht die gleichen consumer im Konstruktor initialisieren, bevor foo zuweisen - wahrscheinlich die Besten :-)

+0

Das ist interessant zu denken, aber es sieht eher wie ein Fehler im Code selbst aus. – manouti

+0

@ AR.3 würde die 'bar1null'-Ausgabe wahrscheinlich als ein Fehler im Code betrachtet werden, aber ich denke, Sie würden nicht erwarten, dass der zweite Aufruf 'bar2null' ausgibt: das wäre ein Fehler in der JVM. –

+0

Ich stimme dir zu. Wenn eines Tages jedoch "endgültige" Felder in einem Lambda anders erfasst werden, wäre ich nicht überrascht, zwei "bar1null" in der Ausgabe zu sehen. – manouti

3

Sie haben Recht, es technisch konnte tun so, weil das betreffende Feld final ist, aber es funktioniert nicht.

Wenn es jedoch ein Problem, dass das zurückgegebene Lambda behält den Verweis auf die MyClass Instanz ist, dann kann man es leicht selbst beheben:

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

Beachten Sie, dass, wenn das Feld final nicht gewesen wäre, dann würde der ursprüngliche Code den tatsächlichen Wert zum Zeitpunkt der Lambda-Ausführung verwenden, während der hier aufgelistete Code den Wert zum Zeitpunkt der Ausführung der getConsumer()-Methode verwenden würde.

+0

Das ist nicht genau richtig. Wie andere Antworten angemerkt haben, kann die Semantik aufgrund der Initialisierungsreihenfolge der Instanzmitglieder unterschiedlich sein. Obwohl das Feld tatsächlich endgültig ist, referenziert das Lambda tatsächlich immer noch den tatsächlichen Wert zum Zeitpunkt, zu dem getConsumer aufgerufen wurde. Die letzte Invarianz ist zufällig. –

14

In Ihrem Code ist bar + foo wirklich eine Abkürzung für bar + this.foo; Wir sind nur so an die Kurzschrift gewöhnt, dass wir vergessen, dass wir implizit ein Instanzmitglied holen. So fängt Ihr Lambda this ein, nicht this.foo.

Wenn Ihre Frage lautet "Könnte diese Funktion anders implementiert worden sein", lautet die Antwort "wahrscheinlich ja"; wir hätten die Spezifikation/Implementierung der Lambda-Erfassung willkürlich komplizierter machen können, um inkrementell bessere Leistungen für eine Vielzahl von speziellen Fällen, einschließlich dieser, zu liefern.

Ändern der Spezifikation, so dass wir this.foo statt this erfasst haben, würde sich nicht viel in der Art der Leistung ändern; es wäre immer noch ein Capturing-Lambda, was eine viel größere Kostenüberlegung als die Extra-Feld-Dereferenzierung ist. Daher sehe ich keinen wirklichen Leistungsschub.

+1

Danke für Ihre Antwort! Das Grundprinzip ist, dass es die Anzahl potentieller "Lecks" reduzieren könnte, wo Lambdas Verweise auf umschließende Instanzen behalten (da Java 8 mehr und mehr verwendet wird), indem nur die Instanzen eingeschlossen werden, wenn das Feld nicht "final" ist. – manouti

+1

FYI vor ein paar Jahren Neal erwog, in diesem Sinne einige Änderungen am C# Lambda Capture Code vorzunehmen; Ich weiß nicht, ob er es je getan hat. –

+0

Beachten Sie auch, dass dies auch sorgfältige Kompromisse erfordert. Es ist offensichtlich, wenn ein Lambda einen Bezug auf 'this.x' hat, aber was ist mit zwei? Drei? Sechsundneunzig? An einem gewissen Punkt ist die "Heilung" der Erfassung von zusätzlichen Vars schlimmer als die Krankheit. –

1

Beachten Sie, dass für jeden gewöhnlichen Java Zugriff auf eine Variable eines Kompilierung-Konstante ist, der konstante Wert findet statt, so dass er im Gegensatz zu einigen behaupteten Personen immun gegen Probleme mit der Initialisierungsreihenfolge ist.

Wir können dies durch das folgende Beispiel zeigen:

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(); 
    } 
} 

Er druckt:

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 

So, wie Sie, die Tatsache sehen, dass der Schreibvorgang in das realConstant Feld noch nicht geschehen Wenn der Superkonstruktor ausgeführt wird, wird kein nicht initialisierter Wert für die wahre Kompilierzeitkonstante angezeigt, selbst wenn über den Lambda-Ausdruck darauf zugegriffen wird. Technisch, weil das Feld nicht wirklich gelesen wird.

Außerdem haben aus dem gleichen Grund unangenehme Reflection-Hacks keine Auswirkungen auf den normalen Java-Zugriff auf Kompilierzeitkonstanten. Der einzige Weg, einen solchen modifizierten Wert wieder zu lesen, ist über Reflexion:

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); 
    } 
} 

Er druckt:

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

, die uns zwei Dinge zeigt,

  1. Reflexion hat auch keinen Einfluss auf Kompilierzeitkonstanten
  2. Das umgebende Objekt wurde erfasst, obwohl es nicht verwendet wird

Ich würde mich freuen, eine Erklärung wie zu finden, "jeder Zugriff auf ein Instanzfeld erfordert der Lambda-Ausdruck, um die Instanz dieses Feldes zu erfassen (auch wenn das Feld nicht tatsächlich gelesen wird)", aber leider ich konnte nicht jeden Aussage über Erfassung von Werten oder this in der aktuellen Java Language Specification finden, die ein bisschen erschreckend ist:

wir daran gewöhnt, dass nicht Instanzfelder in einem Lambda-Ausdruck Zugriff schaffen wird eine Instanz, die keinen Bezug auf this hat, aber selbst das ist nicht garantiert b y die aktuelle Spezifikation. Es ist wichtig, dass diese Unterlassung bald behoben wird ...

Verwandte Themen