Co robi `someObject.new` w Javie?


100

W Javie właśnie się dowiedziałem, że poniższy kod jest legalny:

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

FYI, receiver to tylko klasa pomocnicza z następującym podpisem:

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

Nigdy wcześniej nie widziałem XYZ.newnotacji. Jak to działa? Czy istnieje sposób, aby zakodować to bardziej konwencjonalnie?


7
Dla twojej informacji, klasa wewnętrzna .
Alvin Wong,

1
Wierzyłem też, że newjest to operator w wielu językach. (Myślałem, że możesz również przeciążać neww C ++?) Jednak wewnętrzna klasa Javy jest dla mnie trochę dziwna.
Alvin Wong

5
Nie ma głupich pytań na StackOverflow!
Izaak Rabinowicz

2
@IsaacRabinovitch - Nie ma głupich pytań. Jest jednak wiele głupstw. (I od czasu do czasu głupia odpowiedź.)
Hot Licks,

2
@HotLicks A jaka jest Twoja definicja głupiego pytania? Przypuszczam, że jesteś zbyt mądry, żeby o to pytać. Dobrze, że masz taką samoocenę.
Izaak Rabinowicz

Odpowiedzi:


120

Jest to sposób na utworzenie wystąpienia niestatycznej klasy wewnętrznej spoza treści klasy zawierającej, zgodnie z opisem w dokumentacji Oracle .

Każda instancja klasy wewnętrznej jest skojarzona z instancją klasy zawierającej. Kiedy newwewnętrzna klasa od wewnątrz swojej klasie zawierającej używa thiswystąpienie kontenera domyślnie:

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

Ale jeśli chcesz utworzyć instancję Bar poza Foo lub skojarzyć nową instancję z instancją zawierającą inną niż thiswtedy, musisz użyć notacji prefiksu.

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

18
Jak widać, może to być niesamowicie zagmatwane. W idealnym przypadku klasy wewnętrzne powinny być szczegółami implementacji klasy zewnętrznej i nie powinny być ujawniane światu zewnętrznemu.
Eric Jablow

10
@EricJablow rzeczywiście, jest to jeden z tych fragmentów składni, które muszą istnieć, aby zachować spójność specyfikacji, ale w 99,9999% przypadków nie trzeba jej używać. Jeśli osoby z zewnątrz naprawdę potrzebują tworzyć instancje Bar, to zamiast tego udostępniłbym metodę fabryczną na Foo, zamiast kazać im używać f.new.
Ian Roberts,

Popraw mnie jeśli się mylę, ale jeśli publicpoziom dostępu na KnockKnockServer.receiverdokonano privatewtedy byłoby niemożliwe do instancji w ten sposób, prawda? Aby rozszerzyć komentarz @EricJablow, klasy wewnętrzne powinny generalnie zawsze mieć domyślny privatepoziom dostępu.
Andrew Bissell

1
@AndrewBissell tak, ale byłoby też niemożliwe odwołanie się do receiverklasy z zewnątrz. Gdybym go projektował, prawdopodobnie miałbym klasę publiczną, ale jej konstruktor chroniony lub prywatny pakiet i mam włączoną metodę KnockKnockServertworzenia instancji odbiornika.
Ian Roberts

1
@emory Nie mówię tego, wiem, że mogą istnieć całkowicie uzasadnione powody, aby chcieć upublicznić klasę wewnętrzną i zwrócić wystąpienia klasy wewnętrznej z metod zewnętrznych, ale miałbym tendencję do projektowania mojego kodu w taki sposób, że " osoby z zewnątrz „nie muszą konstruować instancji klasy wewnętrznej bezpośrednio przy użyciu x.new.
Ian Roberts

18

Spójrz na ten przykład:

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

Za pomocą javap możemy przeglądać instrukcje wygenerowane dla tego kodu

Główna metoda:

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 klasy wewnętrznej:

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

}

Wszystko jest proste - przy wywołaniu konstruktora TestInner java przekazuje instancję Test jako pierwszy argument main: 12 . Nie patrząc na ten TestInner, powinien mieć konstruktor bez argumentów. TestInner z kolei po prostu zapisuje odniesienie do obiektu nadrzędnego, Test $ TestInner: 2 . Gdy wywołujesz konstruktor klasy wewnętrznej z metody instancji, odwołanie do obiektu nadrzędnego jest przekazywane automatycznie, więc nie musisz go określać. Właściwie to przebiega za każdym razem, ale przy wywoływaniu z zewnątrz powinno być jawnie przekazywane.

t.new TestInner(); - to tylko sposób na określenie pierwszego ukrytego argumentu do konstruktora TestInner, a nie typ

method () jest równe:

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

TestInner jest równy:

class TestInner{
    private Test this$0;

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

7

Gdy klasy wewnętrzne zostały dodane do języka Java w wersji 1.1 języka, zostały pierwotnie zdefiniowane jako transformacja do kodu zgodnego z wersją 1.0. Jeśli spojrzysz na przykład tej transformacji, myślę, że to znacznie wyjaśni, jak faktycznie działa klasa wewnętrzna.

Rozważ kod z odpowiedzi Iana Robertsa:

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

Po przekształceniu do kodu kompatybilnego z 1.0, ta klasa wewnętrzna Barwyglądałaby mniej więcej tak:

class Foo$Bar {
  private Foo this$0;

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

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

Nazwa klasy wewnętrznej jest poprzedzona nazwą klasy zewnętrznej, aby była unikalna. this$0Dodawany jest ukryty prywatny członek, który przechowuje kopię zewnętrznego this. I tworzony jest ukryty konstruktor w celu zainicjowania tego elementu członkowskiego.

A jeśli spojrzysz na createBarmetodę, zostanie przekształcony w coś takiego:

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

Zobaczmy więc, co się stanie, gdy wykonasz następujący kod.

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

Najpierw tworzymy instancję Fooi inicjalizujemy element valczłonkowski do 5 (tj f.val = 5.).

Następnie wywołujemy f.createBar(), co tworzy instancję Foo$Bari inicjalizuje element this$0członkowski do wartości thisprzekazanej z createBar(tj b.this$0 = f.).

Wreszcie nazywamy b.printVal()który próbuje drukować b.this$0.val, który jest f.val, który jest 5.

To była zwykła instancja klasy wewnętrznej. Spójrzmy, co się dzieje podczas tworzenia instancji Barz zewnątrz Foo.

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

Stosując ponownie naszą transformację 1.0, ta druga linia wyglądałaby mniej więcej tak:

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

Jest to prawie identyczne z f.createBar()wezwaniem. Ponownie tworzymy wystąpienieFoo$Bar i inicjalizujemy element this$0członkowski f. Więc znowu b.this$0 = f.

I znowu, kiedy dzwonisz b.printVal(), drukujeszb.thi$0.val , który jest f.val, który jest 5.

Kluczową rzeczą do zapamiętania jest to, że klasa wewnętrzna ma ukrytego członka przechowującego kopię thisklasy zewnętrznej. Kiedy tworzysz wystąpienie klasy wewnętrznej z poziomu klasy zewnętrznej, jest ona niejawnie inicjowana z bieżącą wartością this. Podczas tworzenia wystąpienia klasy wewnętrznej spoza klasy zewnętrznej jawnie określasz, które wystąpienie klasy zewnętrznej ma być używane, za pomocą prefiksu newsłowa kluczowego.


4

Pomyśl o new receiverpojedynczym tokenie. Coś jak nazwa funkcji ze spacją.

Oczywiście klasa KnockKnockServernie ma dosłownie nazwanej funkcji new receiver, ale zgaduję, że składnia ma to sugerować. Ma to wyglądać tak, jakbyś wywoływał funkcję, która tworzy nową instancję KnockKnockServer.receiverużycia określonej instancji KnockKnockServerdla dowolnego dostępu do otaczającej klasy.


Dzięki, tak - teraz myślę o new receiverjednym tokenach! Dziękuję bardzo!
Kawa

1

Sledzenie

Jeśli deklaracja typu (na przykład zmienna składowa lub nazwa parametru) w określonym zakresie (na przykład klasa wewnętrzna lub definicja metody) ma taką samą nazwę jak inna deklaracja w otaczającym zakresie, to deklaracja przesłania deklarację zakresu załączającego. Nie można odnosić się do deklaracji w tle, używając samej nazwy. Pokazuje to poniższy przykład ShadowTest:

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

Poniżej przedstawiono wyniki tego przykładu:

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

W tym przykładzie zdefiniowano trzy zmienne o nazwie x: zmienną składową klasy ShadowTest, zmienną składową klasy wewnętrznej FirstLevel oraz parametr w metodzie methodInFirstLevel. Zmienna x zdefiniowana jako parametr metody methodInFirstLevel zacienia zmienną klasy wewnętrznej FirstLevel. W związku z tym, gdy używasz zmiennej x w metodzie methodInFirstLevel, odwołuje się ona do parametru metody. Aby odwołać się do zmiennej składowej klasy wewnętrznej FirstLevel, użyj słowa kluczowego this do reprezentowania otaczającego zakresu:

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

Odwołaj się do zmiennych składowych, które obejmują większe zakresy przez nazwę klasy, do której należą. Na przykład poniższa instrukcja uzyskuje dostęp do zmiennej składowej klasy ShadowTest z metody methodInFirstLevel:

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

Zapoznaj się z dokumentacją

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.