2015-09-08 12 views
5

Ich würde gerne ein seltsames Verhalten verstehen, das ich mit anonymen Klassen konfrontiert.Java: Initialisierung und Costruktor von anonymen Klassen

Ich habe eine Klasse, die eine geschützte Methode in seinem Konstruktor aufruft (ich weiß, schlechtes Design, aber das ist eine andere Geschichte ...)

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

dann habe ich eine andere Klasse, die A und überschreibt init() erstreckt.

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

Wenn ich Code

B b = new B(10); 

ich

> value=0 

und dass erwartet wird, weil der Konstruktor der Superklasse vor dem Ctor B aufgerufen wird und dann noch value ist.

Aber wenn eine anonyme Klasse wie dieser erwarten würde ich

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

mit value=0 zu bekommen, weil dies sollte B mehr oder weniger gleich Klasse sein: der Compiler erstellt automatisch eine neue Klasse C$1 die A erweitert und schafft Instanzvariablen in dem Verfahren der anonymen Klasse verwiesen lokale Variablen zu speichern, einen Verschluss zu simulieren etc ...

Aber wenn man diese laufen, ich habe

> java -cp . C 42 
> value=42 

Zuerst dachte ich, dass dies auf die Tatsache zurückzuführen war, dass ich Java 8 verwendete, und vielleicht, als ich lamdbas einführte, änderten sie die Art und Weise wie anonyme Klassen unter der Haube implementiert werden (du brauchst nicht mehr final), aber ich versuchte, mit Java 7 auch und bekam das gleiche Ergebnis ...

Eigentlich mit javap auf Byte-Code suchen, kann ich sehen, dass B ist

> 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 
... 

während für C$1:

> 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 
.... 

Kann mir jemand sagen, warum dieser Unterschied? Gibt es eine Möglichkeit, das Verhalten der anonymen Klasse mit "normalen" Klassen zu replizieren?

EDIT: , um die Frage zu klären: Warum bricht die Initialisierung der anonymen Klassen die Regeln für die Initialisierung einer anderen Klasse (wo Superkonstruktor aufgerufen wird, bevor Sie eine andere Variable setzen)? Oder gibt es eine Möglichkeit, Instanzvariable in B Klasse vor Inovking Superkonstruktor festlegen?

+0

Warum denken Sie, dass Ihr erster und zweiter Code gleich ist? Im zweiten Code greifen Sie auf die lokale Variable zu. Das wird initialisiert, bevor Ihre anonyme Klassenanweisung ausgeführt wird. –

+0

umh ... ok, du sagst: Die Tatsache, dass der Compiler eine Klasse zur Implementierung dieses Szenarios erstellt, sollte dem Entwickler verborgen bleiben, also ist die 'C $ 1' Klasse ein Sonderfall, und es ist in Ordnung, wenn sie nicht dem Standard entspricht Konstruktorregeln. Das ist ziemlich vernünftig, aber trotzdem, es ist ein bisschen peinlich. – ugo

Antwort

3

Diese Frage gilt für alle inneren Klassen, nicht nur für nicht Klassen. (Anon-Klassen sind innere Klassen)

JLS schreibt nicht vor, wie ein innerer Klassenkörper auf äußere lokale Variable zugreift; Es ist nur specifies, dass die lokalen Variablen effektiv endgültig sind und definitiv vor dem inneren Klassenkörper zugewiesen sind. Es liegt daher nahe, dass die innere Klasse den definitiv zugewiesenen Wert der lokalen Variablen sehen muss.

JLS spezifiziert nicht genau, wie die innere Klasse diesen Wert sieht; Es ist Aufgabe des Compilers, jeden Trick (der auf Bytecodeebene möglich ist) zu verwenden, um diesen Effekt zu erzielen. Insbesondere ist dieses Problem für Konstrukteure völlig unabhängig (was die Sprache betrifft).

Ein ähnliches Problem ist, wie eine innere Klasse auf die äußere Instanz zugreift. Dies ist ein wenig komplizierter, und es hat something mit Konstruktoren zu tun. Trotzdem diktiert JLS immer noch nicht, wie es vom Compiler erreicht wird; der Abschnitt einen Kommentar enthält, dass „... Compiler die unmittelbar umgebende Instanz darstellen kann, wie auch immer es will. Es besteht keine Notwendigkeit für die Programmiersprache Java ...“


Von JMM Sicht , diese Unterspezifikation könnte ein Problem sein; Es ist unklar, wie Schreibvorgänge in Bezug auf Lesevorgänge in der inneren Klasse durchgeführt wurden. Es ist vernünftig anzunehmen, dass ein Schreiben auf eine synthetische Variable erfolgt, die vor (in Programmierreihenfolge) der new InnerClass() Aktion ist; Die innere Klasse liest die synthetische Variable, um die äußere lokale Variable oder die umschließende Instanz zu sehen.


Gibt es eine Möglichkeit, das Verhalten der anonymen Klasse zu replizieren „normale“ Klassen?

Sie können die „normale“ Klasse als Außen innere Klasse ordnen

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

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

Es ist wie diese verwendet werden, die 10

new B0(10).new B(); 

Eine bequeme Factory-Methode hinzugefügt werden können, druckt um die Syntax Hässlichkeit zu verstecken

newB(10); 

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

Also teilen wir unsere Klasse in 2 Teile; der äußere Teil wird noch vor dem Superkonstruktor ausgeführt. Dies ist in einigen Fällen nützlich. (another example)


(inner anonymer Zugriff auf lokal Variable umschließenden Instanz wirksam letzter Super-Konstruktor)

+0

+1, weil die innere Klasse ein ziemlich guter Trick ist. Eine anonyme Klasse in einer Fabrikmethode wäre jedoch ausreichend gewesen. – Clashsoft

+0

@Clashsoft - Sie haben Recht; aber für den Fall, dass eine benannte Unterklasse aus irgendeinem Grund benötigt wird. – ZhongYu

2

Ihre anonyme Klasseninstanz verhält sich anders als Ihr erstes Code-Snippet, da Sie eine lokale Variable verwenden, deren Wert initialisiert wird, bevor die anonyme Klasseninstanz erstellt wird.

Sie können mit einer anonymen Klasseninstanz ein ähnliches Verhalten an den ersten Schnipsel, wenn man eine Instanz-Variable in der anonymen Klasse verwenden:

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

Diese

value=0 

seit init() ist gedruckt werden ausgeführt von A 's Konstruktor vor avalue ist initialisiert.

+0

Ich weiß, dass die Variable bereits initialisiert ist, meine Frage war, dass der Konstruktor der anonymen Klasse bei der Betrachtung des Byte-Codes nicht den Regeln der anderen "normalen" Klassen folgt. Sorry, vielleicht war ich nicht klar, ich habe die Frage bearbeitet ... – ugo

+0

Dies ist ein häufiges Problem in der Java * -Sprache *: Der Super-Konstruktor-Aufruf muss die erste Anweisung in einem Konstruktor sein. ** In der Sprache ** erzwingt der Compiler dies über einen Fehler. Der Bytecode (JVM) erlaubt dies jedoch, und der Compiler verwendet ihn in anonymen Klassen und wahrscheinlich auch an anderen Stellen. – Clashsoft

0

Die beiden Beispiele sind nicht verwandt.

Im Beispiel B:

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

der Wert gedruckt wird, ist das Feld value der Instanz von B.

im anonymen Beispiel:

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

der Wert gedruckt wird, ist die lokale Variable avalue der main() Methode.

2

Die variable Erfassung in anonymen Klassen erlaubt ist, die Regeln des normalen Konstrukteuren (Super Konstruktoraufruf zu brechen muss die erste sein, Anweisung), weil dieses Gesetz nur vom Compiler durchgesetzt wird. Die JVM ermöglicht das Ausführen eines beliebigen Bytecodes vor dem Aufruf des Superkonstruktors, der vom Compiler selbst verwendet wird (er bricht seine eigenen Regeln!) Für anonyme Klassen.

Sie können das Verhalten nachahmen entweder mit inneren Klassen wie in bayou.io Antwort gezeigt, oder Sie können eine anonyme in einer statischen B Factory-Methode verwenden:

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

Die Beschränkung eigentlich ziemlich sinnlos ist und kann in manchen Situationen lästig sein:

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 diesem Beispiel haben Sie zweimal die computeComplexString Berechnung zu tun, weil es keine wa y, um es beide an den Superkonstruktor übergeben und speichern Sie es in einer Instanzvariable.

+0

Wie wäre es mit add 'B (String) 'und' B (i, d) 'ruft' this (computeComplexString (i, d)) 'auf – ZhongYu

Verwandte Themen