Was macht `someObject.new` in Java?

100

In Java habe ich gerade herausgefunden, dass der folgende Code legal ist:

KnockKnockServer newServer = new KnockKnockServer();                    
KnockKnockServer.receiver receive = newServer.new receiver(clientSocket);

Zu Ihrer Information, der Empfänger ist nur eine Hilfsklasse mit der folgenden Signatur:

public class receiver extends Thread {  /* code_inside */  }

Ich habe die XYZ.newNotation noch nie gesehen . Wie funktioniert das? Gibt es eine Möglichkeit, dies konventioneller zu codieren?

Kaffee
quelle
7
Als Referenz innere Klasse .
Alvin Wong
1
Außerdem hatte ich geglaubt, dass dies newein Operator in vielen Sprachen ist. (Ich dachte, Sie könnten auch newin C ++ überladen ?) Javas innere Klasse ist für mich allerdings etwas seltsam.
Alvin Wong
5
Es gibt keine dummen Fragen zu StackOverflow!
Isaac Rabinovitch
2
@IsaacRabinovitch - Es gibt keine dummen Fragen. Es gibt jedoch viele dumme. (Und gelegentlich auch eine dumme Antwort.)
Hot Licks
2
@HotLicks Und was ist deine Definition einer dummen Frage? Eine, für die Sie zu schlau sind, um sie zu fragen, nehme ich an. Gut, dass Sie so viel Selbstwertgefühl haben.
Isaac Rabinovitch

Antworten:

120

Auf diese Weise können Sie eine nicht statische innere Klasse von außerhalb des enthaltenen Klassenkörpers instanziieren, wie in den Oracle-Dokumenten beschrieben .

Jede innere Klasseninstanz ist einer Instanz ihrer enthaltenden Klasse zugeordnet. Wenn Sie neweine innere Klasse von innerhalb seiner Klasse enthält , verwendet es die thisInstanz des Behälters durch Standard:

public class Foo {
  int val;
  public Foo(int v) { val = v; }

  class Bar {
    public void printVal() {
      // this is the val belonging to our containing instance
      System.out.println(val);
    }
  }

  public Bar createBar() {
    return new Bar(); // equivalent of this.new Bar()
  }
}

Wenn Sie jedoch eine Instanz von Bar außerhalb von Foo erstellen oder eine neue Instanz einer anderen enthaltenen Instanz zuordnen möchten, thismüssen Sie die Präfixnotation verwenden.

Foo f = new Foo(5);
Foo.Bar b = f.new Bar();
b.printVal(); // prints 5
Ian Roberts
quelle
18
Und wie Sie sehen, kann dies unglaublich verwirrend sein. Im Idealfall sollten innere Klassen Implementierungsdetails der äußeren Klasse sein und nicht der Außenwelt ausgesetzt sein.
Eric Jablow
10
@EricJablow in der Tat, es ist eines dieser Syntaxbits, die vorhanden sein müssen, um die Spezifikation konsistent zu halten, aber in 99,9999% der Fälle müssen Sie sie nicht wirklich verwenden. Wenn Außenstehende wirklich Bar-Instanzen erstellen müssen, würde ich eine Factory-Methode für Foo bereitstellen, anstatt sie verwenden zu lassen f.new.
Ian Roberts
Korrigieren Sie mich , wenn ich falsch liege , aber wenn die publicZugriffsebene auf KnockKnockServer.receivergemacht wurden , privatedann wäre es auf diese Weise zu instantiate unmöglich sein, nicht wahr? Um den Kommentar von @EricJablow zu erweitern, sollten innere Klassen im Allgemeinen immer standardmäßig eine privateZugriffsebene verwenden.
Andrew Bissell
1
@ AndrewBissell ja, aber es wäre auch unmöglich, receivervon außen überhaupt auf die Klasse zu verweisen . Wenn ich es entwerfen würde, hätte ich wahrscheinlich die Klasse public, aber ihren Konstruktor geschützt oder package-private und eine Methode KnockKnockServerzum Erstellen von Empfängerinstanzen.
Ian Roberts
1
@emory Ich sage das nicht, ich weiß, dass es durchaus triftige Gründe geben kann, eine innere Klasse öffentlich zu machen und Instanzen der inneren Klasse von Methoden der äußeren zurückzugeben, aber ich würde meinen Code eher so gestalten, dass " Außenstehende "müssen keine Instanzen der inneren Klasse direkt mit konstruieren x.new.
Ian Roberts
18

Schauen Sie sich dieses Beispiel an:

public class Test {

    class TestInner{

    }

    public TestInner method(){
        return new TestInner();
    }

    public static void main(String[] args) throws Exception{
        Test t = new Test();
        Test.TestInner ti = t.new TestInner();
    }
}

Mit javap können wir Anweisungen anzeigen, die für diesen Code generiert wurden

Hauptmethode:

public static void main(java.lang.String[])   throws java.lang.Exception;
  Code:
   0:   new     #2; //class Test
   3:   dup
   4:   invokespecial   #3; //Method "<init>":()V
   7:   astore_1
   8:   new     #4; //class Test$TestInner
   11:  dup
   12:  aload_1
   13:  dup
   14:  invokevirtual   #5; //Method java/lang/Object.getClass:()Ljava/lang/Class;
   17:  pop
   18:  invokespecial   #6; //Method Test$TestInner."<init>":(LTest;)V
   21:  astore_2
   22:  return
}

Konstruktor der inneren Klasse:

Test$TestInner(Test);
  Code:
   0:   aload_0
   1:   aload_1
   2:   putfield        #1; //Field this$0:LTest;
   5:   aload_0
   6:   invokespecial   #2; //Method java/lang/Object."<init>":()V
   9:   return

}

Alles ist einfach - beim Aufrufen des TestInner-Konstruktors übergibt Java die Testinstanz als erstes Argument main: 12 . Wenn Sie sich diesen TestInner nicht ansehen, sollte er einen Konstruktor ohne Argumente haben. TestInner wiederum speichert nur den Verweis auf das übergeordnete Objekt Test $ TestInner: 2 . Wenn Sie den Konstruktor der inneren Klasse über eine Instanzmethode aufrufen, wird der Verweis auf das übergeordnete Objekt automatisch übergeben, sodass Sie ihn nicht angeben müssen. Eigentlich geht es jedes Mal vorbei, aber wenn es von außerhalb aufgerufen wird, sollte es explizit übergeben werden.

t.new TestInner(); - ist nur eine Möglichkeit, das erste versteckte Argument für den TestInner-Konstruktor anzugeben, nicht einen Typ

method () ist gleich:

public TestInner method(){
    return this.new TestInner();
}

TestInner ist gleich:

class TestInner{
    private Test this$0;

    TestInner(Test parent){
        this.this$0 = parent;
    }
}
Mikhail
quelle
7

Wenn Java in Version 1.1 der Sprache innere Klassen hinzugefügt wurden, wurden sie ursprünglich als Umwandlung in 1.0-kompatiblen Code definiert. Wenn Sie sich ein Beispiel für diese Transformation ansehen, wird es meiner Meinung nach viel klarer machen, wie eine innere Klasse tatsächlich funktioniert.

Betrachten Sie den Code aus Ian Roberts 'Antwort:

public class Foo {
  int val;
  public Foo(int v) { val = v; }

  class Bar {
    public void printVal() {
      System.out.println(val);
    }
  }

  public Bar createBar() {
    return new Bar();
  }
}

Bei der Transformation in 1.0-kompatiblen Code Barwürde diese innere Klasse ungefähr so ​​aussehen:

class Foo$Bar {
  private Foo this$0;

  Foo$Bar(Foo outerThis) {
    this.this$0 = outerThis;
  }

  public void printVal() {
    System.out.println(this$0.val);
  }
}

Dem inneren Klassennamen wird der äußere Klassenname vorangestellt, um ihn eindeutig zu machen. Ein verstecktes privates this$0Mitglied wird hinzugefügt, das eine Kopie des äußeren enthält this. Und ein versteckter Konstruktor wird erstellt, um dieses Mitglied zu initialisieren.

Und wenn Sie sich die createBarMethode ansehen , würde sie sich in etwa so verwandeln:

public Foo$Bar createBar() {
  return new Foo$Bar(this);
}

Mal sehen, was passiert, wenn Sie den folgenden Code ausführen.

Foo f = new Foo(5);
Foo.Bar b = f.createBar();                               
b.printVal();

Zuerst instanziieren wir eine Instanz von Foound initialisieren das valMitglied auf 5 (dh f.val = 5).

Als nächstes rufen wir auf f.createBar(), das eine Instanz von instanziiert Foo$Barund das this$0Mitglied auf den Wert von thisübergeben von createBar(dh b.this$0 = f) initialisiert .

Schließlich rufen wir auf, b.printVal()welche Versuche zu drucken b.this$0.valsind, f.valwelche 5 sind.

Das war eine regelmäßige Instanziierung einer inneren Klasse. Schauen wir uns an, was passiert, wenn Barvon außen instanziiert wird Foo.

Foo f = new Foo(5);
Foo.Bar b = f.new Bar();
b.printVal();

Wenn Sie unsere 1.0-Transformation erneut anwenden, wird diese zweite Zeile ungefähr so ​​aussehen:

Foo$Bar b = new Foo$Bar(f);

Dies ist fast identisch mit dem f.createBar()Anruf. Wieder instanziieren wir eine Instanz von Foo$Barund initialisieren das this$0Mitglied auf f. Also nochmal , b.this$0 = f.

Und wieder, wenn Sie anrufen b.printVal(), drucken Sie b.thi$0.val, f.valwas 5 ist.

Das Wichtigste ist, dass die innere Klasse ein verstecktes Mitglied hat, das eine Kopie thisder äußeren Klasse enthält. Wenn Sie eine innere Klasse innerhalb der äußeren Klasse instanziieren, wird sie implizit mit dem aktuellen Wert von initialisiert this. Wenn Sie die innere Klasse von außerhalb der äußeren Klasse instanziieren, geben Sie über das Präfix des newSchlüsselworts explizit an, welche Instanz der äußeren Klasse verwendet werden soll .

James Holderness
quelle
4

Stellen Sie sich new receiverein einzelnes Zeichen vor. Ein bisschen wie ein Funktionsname mit einem Leerzeichen.

Natürlich hat die Klasse KnockKnockServernicht buchstäblich eine Funktion namens new receiver, aber ich vermute, die Syntax soll dies vorschlagen. Es soll so aussehen, als würden Sie eine Funktion aufrufen, die eine neue Instanz der KnockKnockServer.receiverVerwendung einer bestimmten Instanz KnockKnockServerfür alle Zugriffe auf die einschließende Klasse erstellt.

David Z.
quelle
Danke, ja - es hilft mir jetzt, mich new receiverals ein Zeichen vorzustellen! vielen Dank!
Kaffee
1

Beschattung

Wenn eine Deklaration eines Typs (z. B. eine Mitgliedsvariable oder ein Parametername) in einem bestimmten Bereich (z. B. einer inneren Klasse oder einer Methodendefinition) denselben Namen wie eine andere Deklaration im umschließenden Bereich hat, wird die Deklaration durch die Deklaration beschattet des umschließenden Umfangs. Sie können eine schattierte Deklaration nicht nur mit ihrem Namen bezeichnen. Das folgende Beispiel, ShadowTest, zeigt dies:

public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

Das Folgende ist die Ausgabe dieses Beispiels:

x = 23
this.x = 1
ShadowTest.this.x = 0

In diesem Beispiel werden drei Variablen mit dem Namen x definiert: Die Elementvariable der Klasse ShadowTest, die Elementvariable der inneren Klasse FirstLevel und der Parameter in der Methode methodInFirstLevel. Die als Parameter der Methode methodInFirstLevel definierte Variable x schattiert die Variable der inneren Klasse FirstLevel. Wenn Sie die Variable x in der Methode methodInFirstLevel verwenden, verweist sie folglich auf den Methodenparameter. Verwenden Sie das Schlüsselwort this, um den umschließenden Bereich darzustellen, um auf die Elementvariable der inneren Klasse FirstLevel zu verweisen:

System.out.println("this.x = " + this.x);

Verweisen Sie auf Elementvariablen, die größere Bereiche nach dem Klassennamen einschließen, zu dem sie gehören. Die folgende Anweisung greift beispielsweise über die Methode methodInFirstLevel auf die Mitgliedsvariable der Klasse ShadowTest zu:

System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);

Siehe die Dokumente

JavaTechnical
quelle