2017-07-12 2 views
3

Ich verstehe dieses Verhalten nicht.Verwenden Sie ein Klassenfeld in einem Lambda

Dieses Stück Code entspricht:

public class A { 

    private String s; 

    private Function<String, String> f = e -> s; 

    public A(String s) { 
     this.s = s; 
    } 
} 

Aber wenn ich s endgültig zu machen, dann bekomme ich einen Compiler-Fehler:

public class A { 

    private final String s; 

    private Function<String, String> f = e -> s; // Variable 's' might not have been initialized 

    public A(String s) { 
     this.s = s; 
    } 
} 

Warum ist das so? Wenn es umgekehrt wäre, würde ich das verstehen, aber wie beschwert sich der Compiler, wenn ich ein Feld final deklariere (was mich zwingt, seinen Wert im Konstruktor zu initialisieren), und es ist OK, wenn es nicht final ist?

Antwort

11

Es hat nichts mit dem Lambda zu tun, dieses Beispiel hat den gleichen Fehler:

public class Test { 
    private final String a; 
    private String b = a; // // Variable 'a' might not have been initialized 

    public Test(String a) { 
     this.a = a; 
    } 
} 

Es ist, weil die Initialisierung am Ort der Erklärung vor dem Konstruktor ausgeführt wird . Daher wird an der Stelle der Deklaration ba immer noch nicht initialisiert.

Es ist klar, wenn Sie dieses Beispiel verwenden:

public class Test { 
    private String a = "init"; 
    private String b = a; 

    public Test(String a) { 
     this.a = a; 
    } 

    public static void main(String[] args) { 
     System.out.println(new Test("constructor").b); 
    } 
} 

Wenn Sie es ausführen, druckt es "init" (der Wert auf dem Feld a ursprünglich zugewiesen wurde) und nicht "constructor", weil die Initialisierung von b stattfand vor den Konstruktor ausführen.

Das Lambda von Ihrem Beispiel macht das es ein wenig komplizierter, weil wir erwarten konnten, da der Zugriff auf a aufgeschoben ist, wäre es in Ordnung mit dem Compiler, aber anscheinend folgt der Compiler nur der allgemeinen Regel von "don Vor dem Initialisieren auf die Variable nicht zugreifen. "

Sie können es umgehen einen Accessor-Methode:

public class Test { 
    private final String a; 
    private String b = getA(); // allowed now, but not very useful 
    private Function<String, String> f = e -> getA(); // allowed now and evaluated at the time of execution of the function 

    public Test(String a) { 
     this.a = a; 
    } 

    public static void main(String[] args) { 
     System.out.println(new Test("constructor").b); // prints "null" 
     System.out.println(new Test("constructor").f.apply("")); // prints "constructor" 
    } 

    public String getA() { 
     return a; 
    } 
} 
+3

Es ist eine fundamentale Eigenschaft der Lambda-Ausdrücke, wie Ausdrücke im umgebenden Kontext zu arbeiten, so dass die Regeln bezüglich des Zugriffs auf leere finale Variablen gleich sind. Dies ist anders als innere Klassen. Beachten Sie, dass Sie anstelle von 'getA() '' auch umgehen können, indem Sie 'Test.this.a' verwenden, um auf' a' zuzugreifen. – Holger

+1

@Holger Danke, die Antworten, die Sie verlinkt haben, sind sehr interessant und klar. Interessant, dass der Zugriff auf "a" über "a" und "this.a" Fehler sind, aber "Test.this.a" ist kein Fehler im Falle des Lambda, aber immer noch ein Fehler im Falle der Zuweisung zu "b". –

+2

Ich habe 'b = Test.this.a;' mit fast jedem JDK 8 & 9 versucht und es hat funktioniert. [Die Spezifikation] (https://docs.oracle.com/javase/specs/jls/se8/html/jls-16.html) bezieht sich eindeutig auf "* den einfachen Namen der Variablen (oder, für ein Feld, die einfacher Name des Felds, das durch 'this' qualifiziert wurde) * "wenn der Zugriff auf eine leere letzte Variable verboten wird. Hast du Eclipse benutzt? – Holger

5

Eine nicht abschließende Elementvariable wird immer initialisiert (da sie im Fall Ihrer String Variable einen Standardwert hat - null), es besteht also keine Möglichkeit, dass sie nicht initialisiert wird.

Auf der anderen Seite kann eine letzte Variable nur einmal initialisiert werden, also gehe ich davon aus, dass sie nicht auf ihren Standardwert initialisiert wird.

Die am engsten verwandten, was ich gefunden ist in JLS 4.12.4.:

4.12.4. final Variables

A variable can be declared final. A final variable may only be assigned to once. It is a compile-time error if a final variable is assigned to unless it is definitely unassigned immediately prior to the assignment

Ich gehe davon aus wir diesen letzten Satz zu verstehen, verstehen, dass eine endgültige Variable keinen Standardwert zugewiesen wird, da sonst finden Sie eine Kompilierung erhalten Zeitfehler bei this.s = s;.

Eine bessere JLS Referenz (dank Holger) ist JLS 16:

Chapter 16. Definite Assignment

For every access of a local variable or blank final field x, x must be definitely assigned before the access, or a compile-time error occurs.

Die rationale hinter dieser Forderung ist, dass (in Ihrem Beispiel) der Lambda-Ausdruck vor s aufgerufen werden kann initialisiert:

public A(String s) { 
    String v = f.apply("x"); // this.s is not initialized at this point 
          // so it can't be accessed 
    this.s = s; 
} 

Beachten Sie, dass Sie den Lambda-Ausdruck im Konstruktor initialisieren können, nachdem Sie die letzte Variable initialisiert haben (ich änderte den Namen des Arguments anders als die Elementvariable, so dass der Lambda-Ausdruck nicht gr ab, dass die lokale Variable):

public A(String so) { 
    // f = e -> s; // Error: The blank final field s may not have been initialized 
    this.s = so; 
    f = e -> s; // works fine 
} 
+3

[JLS §16] zu tun (https://docs.oracle.com/javase/ specs/jls/se8/html/jls-16.html) ist der richtige Ort: "** Für jeden Zugriff einer lokalen Variable oder eines leeren' final' Feldes 'x' muss' x' vor dem Zugriff eindeutig zugewiesen werden, oder ein Kompilierungsfehler tritt auf. ** " – Holger

+0

@Holger thanks! – Eran

0

Dies auch mögliche Weise

public class A { 

    private final String s; 
    private Function<String, String> f; 

    public A(String s) { 
    this.s = s; 
    this.f = e -> s; 
    } 
} 
Verwandte Themen