2015-09-08 33 views
5

Mi piacerebbe capire uno strano comportamento che ho dovuto affrontare con le classi anonime.Java: inizializzazione e costruttore di classi anonime

Ho una classe che chiama un metodo protetto all'interno del suo costruttore (lo so, il design povero, ma questa è un'altra storia ...)

public class A { 
    public A() { 
    init(); 
    } 
    protected void init() {} 
} 

poi ho un'altra classe che estende A e sovrascrive init().

public class B extends A { 
    int value; 
    public B(int i) { 
    value = i; 
    } 
    protected void init() { 
    System.out.println("value="+value); 
    } 
} 

Se il codice

B b = new B(10); 

ottengo

> value=0 

e che ci si aspetta, perché il costruttore della classe Super viene invocato prima del B ctor e poi value è ancora.

Ma quando si utilizza una classe anonima come questo

class C { 
    public static void main (String[] args) { 
    final int avalue = Integer.parsetInt(args[0]); 
    A a = new A() { 
     void init() { System.out.println("value="+avalue); } 
    } 
    } 
} 

mi aspetto di ottenere value=0 perché questo dovrebbe essere più o meno uguale alla classe B: il compilatore crea automaticamente una nuova classe C$1 che si estende A e crea variabili di istanza per memorizzare le variabili locali fanno riferimento i metodi della classe anonima, simulando una chiusura ecc ...

Ma quando si esegue questo, ho avuto

> java -cp . C 42 
> value=42 

Inizialmente pensavo che questo fosse dovuto al fatto che stavo usando java 8, e forse, introducendo lamdbas, hanno cambiato il modo in cui le classi anonime sono implementate sotto il cofano (non hai più bisogno di final), ma ho provato con Java 7 anche ed ho ottenuto lo stesso risultato ...

in realtà, guardando il codice byte con javap, vedo che è B

> javap -c B 
Compiled from "B.java" 
public class B extends A { 
    int value; 

    public B(int); 
    Code: 
     0: aload_0 
     1: invokespecial #1     // Method A."<init>":()V 
     4: aload_0 
     5: iload_1 
     6: putfield  #2     // Field value:I 
     9: return 
... 

mentre per C$1:

0.123.516,41 mila
> javap -c C\$1 
Compiled from "C.java" 
final class C$1 extends A { 
    final int val$v; 

    C$1(int); 
    Code: 
     0: aload_0 
     1: iload_1 
     2: putfield  #1     // Field val$v:I 
     5: aload_0 
     6: invokespecial #2     // Method A."<init>":()V 
     9: return 
.... 

Qualcuno potrebbe dirmi perché questa differenza? Esiste un modo per replicare il comportamento della classe anonima usando classi "normali"?

EDIT: per chiarire la questione: perché l'inizializzazione delle classi anonime rompere le regole per l'inizializzazione di qualsiasi altra classe (dove costruttore di eccellente viene invocato prima di impostare qualsiasi altra variabile)? Oppure, c'è un modo per impostare la variabile di istanza nella classe B prima di inovking super-costruttore?

+0

Perché pensi che il tuo primo e il secondo codice siano gli stessi? Nel secondo codice, stai accedendo alla variabile locale. Questo verrà inizializzato prima dell'esecuzione dell'istruzione della classe anonima. –

+0

umh ... ok, tu dici: il fatto che il compilatore crei una classe per implementare questo scenario dovrebbe essere nascosto allo sviluppatore, quindi la classe 'C $ 1' è un caso speciale, e va bene se non segue lo standard regole del costruttore. Questo è abbastanza ragionevole, ma comunque, imho è un po 'imbarazzante. – ugo

risposta

3

Questa domanda si applica a tutte le classi interne, non solo alle classi anon. (Le classi Anon sono classi interne)

JLS non stabilisce il modo in cui un corpo di classe interna accede a una variabile locale esterna; è solo il specifies che le variabili locali sono effettivamente definitive e definitivamente assegnate al corpo della classe interiore. Pertanto, è ovvio che la classe interna deve vedere il valore assegnato della variabile locale.

JLS non specifica esattamente come la classe interna vede quel valore; spetta al compilatore utilizzare qualsiasi trucco (che sia possibile a livello di bytecode) per ottenere quell'effetto. In particolare, questo problema è completamente estraneo ai costruttori (per quanto riguarda la lingua).

Un problema simile è come una classe interna accede all'istanza esterna. Questo è un po 'più complicato, e ha something da fare con i costruttori. Tuttavia, JLS continua a non dettare come viene realizzato dal compilatore; la sezione contiene un commento che "... compilatore può rappresentare l'istanza immediatamente racchiude come mai si desidera. Non v'è alcuna necessità per il linguaggio di programmazione Java per ..."


Dal punto di vista JMM , questa sotto-specifica potrebbe essere un problema; non è chiaro come siano state fatte le scritture in relazione alle letture nella classe interiore. È ragionevole supporre che, una scrittura sia fatta su una variabile sintetica, che è prima (in ordine di programmazione) l'azione new InnerClass(); la classe interna legge la variabile sintetica per vedere la variabile locale esterna o l'istanza allegata.


Esiste un modo per replicare il comportamento della classe anonima con "normali" le classi?

È possibile organizzare la classe "normale", come classe esterna-interna

public class B0 
{ 
    int value; 
    public B0(int i){ value=i; } 

    public class B extends A 
    { 
     protected void init() 
     { 
      System.out.println("value="+value); 
     } 
    } 
} 

Sarà usato in questo modo, che stampa metodo factory 10

new B0(10).new B(); 

vantaggio può essere aggiunto per nascondere la sintassi ugliness

newB(10); 

public static B0.B newB(int arg){ return new B0(arg).new B(); } 

Quindi abbiamo diviso la nostra classe in 2 parti; la parte esterna viene eseguita anche prima del super costruttore. Questo è utile in alcuni casi. (another example)


(interno l'accesso anonimo variabile locale che racchiude esempio efficace costruttore super finale)

+0

+1 perché la classe interiore è un bel trucco. Tuttavia, una classe anonima in un metodo di produzione sarebbe stata sufficiente. – Clashsoft

+0

@Clashsoft - hai ragione; ma nel caso in cui una sottoclasse nominata sia richiesta per qualche motivo. – ZhongYu

2

L'istanza della classe anonima si comporta in modo diverso rispetto al primo frammento di codice poiché si utilizza una variabile locale il cui valore è inizializzato prima che venga creata l'istanza della classe anonima.

È possibile ottenere un comportamento simile al primo frammento con un'istanza classe anonima se si utilizza una variabile di istanza della classe anonima:

class C { 
    public static void main (String[] args) { 
    A a = new A() { 
     int avalue = 10; 
     void init() { System.out.println("value="+avalue); } 
    } 
    } 
} 

questo stampa

value=0 

dal init() è eseguito dal costruttore A prima di inizializzare avalue.

+0

So che la variabile è già inizializzata, la mia domanda riguardava il fatto che guardando il codice byte, il costruttore della classe anonima non segue le regole delle altre classi "normali". Scusate, forse non ero chiaro, ho modificato la domanda ... – ugo

+0

Questo è un problema comune nel linguaggio Java *: la chiamata super-costruttore deve essere la prima dichiarazione in un costruttore. ** Nella lingua **, il compilatore impone questo tramite un errore. Il bytecode (JVM), tuttavia, consente questo, e il compilatore ne fa uso in classi anonime e probabilmente anche in altri posti. – Clashsoft

0

I due esempi non sono correlati.

Nell'esempio B:

protected void init() { 
    System.out.println("value="+value); 
} 

il valore in fase di stampa è il campo value dell'istanza di B.

Nell'esempio anonymous:

final int avalue = Integer.parsetInt(args[0]); 
A a = new A() { 
    void init() { System.out.println("value="+avalue); } 
} 

il valore corso di stampa è la variabile locale avalue del metodo main().

2

La cattura variabile nella classi anonime è permesso di rompere le regole dei costruttori normali (chiamata eccellente costruttore deve essere il primo dichiarazione) perché questa legge è applicata solo dal compilatore. La JVM consente di eseguire qualsiasi bytecode prima di richiamare il super costruttore, che viene utilizzato dal compilatore stesso (rompe le sue stesse regole!) Per le classi anonime.

È possibile simulare il comportamento sia con le classi interne come mostrato nella risposta di bayou.io, oppure è possibile utilizzare un anonimo in un metodo B factory statica:

public class B extends A 
{ 
    public static B create(int value) 
    { 
     return new B() { 
      void init() { System.out.println("value="+value); 
     }; 
    } 
} 

La limitazione è in realtà piuttosto inutile e può essere fastidioso in alcune situazioni:

class A 
{ 
    private int len; 

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

class B extends A 
{ 
    private String complexString; 

    public B(int i, double d) 
    { 
     super(computeComplexString(i, d)); 
     this.complexString = computeComplexString(i, d); 
    } 

    private static String computeComplexString(int i, double d) 
    { 
     // some code that takes a long time 
    } 
} 

in questo esempio, si deve fare il calcolo computeComplexString due volte, perché non c'è wa Per passare entrambi al super costruttore e memorizzarlo in una variabile di istanza.

+0

che ne dici di aggiungere un 'B (String)', e 'B (i, d)' chiama 'questo (computeComplexString (i, d))' – ZhongYu