2016-03-23 21 views
13

Si basa su this question. Considerate questo esempio in cui un metodo restituisce un Consumer basata su un'espressione lambda:Perché un lambda deve catturare l'istanza che racchiude quando fa riferimento a un campo di stringa finale?

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

Come sappiamo, non è una buona pratica per fare riferimento a uno stato di corrente all'interno di una lambda quando si fa programmazione funzionale, uno dei motivi è che la lambda catturerebbe l'istanza allegata, che non sarà raccolta dei rifiuti fino a quando lo stesso lambda non rientra nello scope.

Tuttavia, in questo specifico scenario correlate a final stringhe, sembra che il compilatore potrebbe avere appena chiuso il (final) costante stringa foo (dal pool costante) nel lambda restituito, invece di racchiudere tutto MyClass istanza come mostrato di seguito durante il debug (ponendo l'interruzione allo System.out.println). Ha a che fare con il modo in cui i lambda sono compilati con un codice bytecode speciale invokedynamic?

enter image description here

+0

Puoi chiarire?Per prima cosa spiega che ci si aspetta l'istanza di '' MyClass'' e dopo si è sorpresi di vedere questo '' args $ 1'' –

+1

@JeanLogaert La domanda è * perché * si verifica ancora mentre si utilizza una variabile finale –

+0

@JeanLogeart La domanda si riferisce specificamente a 'final String', che è considerato una costante. – manouti

risposta

3

Se lambda stava acquisendo foo anziché this, in alcuni casi è possibile ottenere un risultato diverso. Si consideri il seguente esempio:

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

uscita:

bar1null 
bar2foo 

Se foo è stato catturato dal lambda, sarebbe catturato come null e la seconda chiamata sarebbe stampare bar2null. Tuttavia, poiché l'istanza MyClass viene acquisita, stampa il valore corretto.

Ovviamente questo è un codice brutto e un po 'forzato, ma in un codice di vita più complesso, un tale problema potrebbe un po' facilmente verificarsi.

Nota che l'unica vera brutta cosa, è che stiamo forzando una lettura del to-be-assegnato foo nel costruttore, attraverso il consumatore. Costruire il consumatore stesso non è previsto per leggere foo in quel momento, quindi è ancora legittimo costruirlo prima di assegnare foo - a patto di non utilizzarlo immediatamente.

Tuttavia il compilatore non ti consente di inizializzare la stessa consumer nel costruttore prima di assegnare foo - probabilmente per il meglio :-)

+0

Questo è interessante da pensare, ma tende ad assomigliare più ad un errore nel codice stesso. – manouti

+0

@ AR.3 l'output 'bar1null' sarebbe probabilmente considerato un bug nel codice, ma penso che non ci si aspetterebbe che la seconda chiamata stampi' bar2null': sarebbe considerato un bug nella JVM. –

+0

Sono d'accordo con te. Se un giorno, comunque, i campi 'finali' vengono catturati in modo diverso in una lambda, non mi sorprenderebbe affatto vedere due' bar1null' nell'output. – manouti

3

Hai ragione, tecnicamente potrebbe farlo, perché il campo in questione è final, ma non è così.

Tuttavia, se si tratta di un problema che la lambda restituita mantiene il riferimento all'istanza MyClass, allora si può facilmente risolvere da soli:

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

nota, che se il campo non fosse stato final, quindi il codice originale utilizzerà il valore attuale nel momento in cui viene eseguito il lambda, mentre il codice elencato qui utilizzerà il valore al momento dell'esecuzione del metodo getConsumer().

+0

Questo non è esattamente corretto. Come hanno osservato altre risposte, la semantica potrebbe essere diversa a causa dell'ordine di inizializzazione dei membri di istanza. Inoltre, sebbene il campo sia effettivamente definitivo, il lambda fa ancora riferimento al valore effettivo al momento in cui è stato chiamato getConsumer. L'invarianza finale è incidentale. –

14

Nel codice, bar + foo è in realtà una scorciatoia per bar + this.foo; siamo così abituati alla stenografia da dimenticare che stiamo recuperando implicitamente un membro di istanza. Quindi il tuo lambda sta catturando this, non this.foo.

Se la tua domanda è "questa funzionalità potrebbe essere stata implementata in modo diverso", la risposta è "probabilmente sì"; avremmo potuto rendere arbitrariamente più complicata la specifica/implementazione della cattura lambda allo scopo di fornire prestazioni incrementalmente migliori per una varietà di casi speciali, incluso questo.

Cambiare le specifiche in modo da catturare this.foo anziché this non cambierebbe molto in termini di prestazioni; sarebbe ancora una cattura lambda, che è una considerazione dei costi molto più grande della dereferenziazione di campo extra. Quindi non vedo questo come fornire un reale aumento delle prestazioni.

+1

Grazie per la tua risposta! La logica è che potrebbe ridurre il numero di potenziali "perdite" in cui lambdas conserva i riferimenti alle istanze in allegato (come Java 8 è sempre più utilizzato), includendo solo le istanze se il campo non è 'finale'. – manouti

+1

FYI alcuni anni fa Neal stava considerando di fare alcune modifiche al codice di cattura di C# lambda in questo spirito; Non so se l'abbia mai fatto. –

+0

Si noti inoltre che ciò richiede anche un attento compromesso. È ovvio quando un lambda ha un riferimento a 'this.x', ma che dire di due? Tre? Novantasei? Ad un certo punto, la "cura" di catturare ulteriori vars è peggiore della malattia. –

1

Nota che per ogni accesso Java ordinaria ad una variabile essendo una fase di compilazione costante, il valore costante ha luogo, quindi, a differenza di quanto affermato da alcune persone, è immune ai problemi di ordine di inizializzazione.

Possiamo dimostrare questo dal seguente esempio:

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

Esso stampa:

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 

Quindi, come si può vedere, il fatto che la scrittura al campo realConstant non aveva ancora succedere quando viene eseguito il super costruttore, non viene visualizzato alcun valore non inizializzato per la costante costante in fase di compilazione, anche quando si accede tramite espressione lambda. Tecnicamente, perché il campo non è effettivamente letto.

Inoltre, i brutti attacchi di Reflection non hanno alcun effetto sull'ordinario accesso Java alle costanti di compilazione, per lo stesso motivo. L'unico modo per leggere un tale valore modificato di nuovo, è via Riflessione:

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

Esso stampa:

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

che ci mostra due cose,

  1. riflessione ha anche alcun effetto sulla costanti di tempo di compilazione
  2. L'oggetto circostante è stato catturato, nonostante non verrà utilizzato

Sarei felice di trovare una spiegazione del tipo "qualsiasi accesso a un campo di istanza richiede l'espressione lambda per acquisire l'istanza di quel campo (anche se il campo non viene effettivamente letto)", ma purtroppo io non riusciva a trovare qualsiasi dichiarazione per quanto riguarda l'acquisizione di valori o di this nel corrente linguaggio Java Specification, che è un po 'spaventoso:

Ci siamo abituati al fatto che non accesso ai campi di istanza in un'espressione lambda creerà un'istanza che non ha un riferimento a this, ma anche questo non è effettivamente garantito b y la specifica attuale. È importante che questa omissione venga risolta presto ...