Apa yang dilakukan `someObject.new` di Java?


100

Di Java, saya baru tahu bahwa kode berikut ini legal:

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

FYI, penerima hanyalah kelas pembantu dengan tanda tangan berikut:

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

Saya belum pernah melihat XYZ.newnotasi sebelumnya. Bagaimana cara kerjanya? Apakah ada cara untuk membuat kode yang lebih konvensional?


7
Untuk referensi Anda, kelas batin .
Alvin Wong

1
Juga, saya percaya itu newadalah operator dalam banyak bahasa. (Saya pikir Anda mungkin juga kelebihan beban newdi C ++?) Kelas dalam Java agak aneh bagi saya.
Alvin Wong

5
Tidak ada pertanyaan konyol di StackOverflow!
Isaac Rabinovitch

2
@IsaacRabinovitch - Tidak ada pertanyaan bodoh. Namun, ada banyak hal konyol. (Dan jawaban konyol sesekali juga.)
Hot Licks

2
@ HotLicks Dan apa definisi Anda tentang pertanyaan konyol? Salah satu yang terlalu pintar untuk Anda tanyakan, saya kira. Bagus karena Anda memiliki harga diri yang tinggi.
Isaac Rabinovitch

Jawaban:


120

Ini adalah cara untuk membuat instance kelas dalam non-statis dari luar badan kelas yang memuatnya, seperti yang dijelaskan dalam dokumen Oracle .

Setiap instance kelas dalam dikaitkan dengan sebuah instance dari kelas yang memuatnya. Saat Anda newkelas dalam dari dalam kelas yang memuatnya, ia menggunakan thisinstance container secara default:

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

Tetapi jika Anda ingin membuat sebuah instance dari Bar di luar Foo, atau mengasosiasikan sebuah instance baru dengan sebuah instance yang mengandung selain thismaka Anda harus menggunakan notasi prefiks.

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

18
Dan, seperti yang Anda tahu, ini bisa sangat membingungkan. Idealnya, kelas dalam harus merupakan detail implementasi kelas luar dan tidak diekspos ke dunia luar.
Eric Jablow

10
@EricJablow memang, ini adalah salah satu bit sintaks yang harus ada untuk menjaga agar spesifikasi tetap konsisten tetapi 99,9999% dari waktu Anda sebenarnya tidak perlu menggunakannya. Jika orang luar benar-benar perlu membuat instance Bar maka saya akan memberikan metode pabrik di Foo daripada meminta mereka menggunakannya f.new.
Ian Roberts

Koreksi saya jika saya salah tetapi jika publictingkat akses pada KnockKnockServer.receiverdibuat privatemaka tidak mungkin untuk membuat contoh dengan cara ini, bukan? Untuk memperluas komentar @EricJablow, kelas dalam biasanya harus selalu default ke privatetingkat akses.
Andrew Bissell

1
@AndrewBissell ya, tetapi juga tidak mungkin untuk merujuk ke receiverkelas sama sekali dari luar. Jika saya mendesainnya, saya mungkin akan memiliki kelas publik tetapi konstruktornya dilindungi atau paket-pribadi, dan memiliki metode KnockKnockServeruntuk membuat instance penerima.
Ian Roberts

1
@emory Saya tidak mengatakan itu, saya tahu mungkin ada alasan yang benar-benar valid untuk ingin membuat kelas dalam menjadi publik, dan mengembalikan contoh kelas dalam dari metode luar, tetapi saya akan cenderung mendesain kode saya sedemikian rupa " orang luar "tidak perlu membuat instance dari kelas dalam secara langsung menggunakan x.new.
Ian Roberts

18

Lihat contoh ini:

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

Menggunakan javap kita dapat melihat instruksi yang dibuat untuk kode ini

Metode utama:

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 kelas dalam:

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

}

Semuanya sederhana - saat menjalankan konstruktor TestInner, java melewati Contoh uji sebagai argumen pertama main: 12 . Tidak melihat TestInner itu seharusnya memiliki konstruktor tanpa argumen. TestInner pada gilirannya hanya menyimpan referensi ke objek induk, Test $ TestInner: 2 . Saat Anda memanggil konstruktor kelas dalam dari metode instance, referensi ke objek induk akan diteruskan secara otomatis, jadi Anda tidak perlu menentukannya. Sebenarnya itu lolos setiap saat, tetapi ketika memanggil dari luar itu harus diteruskan secara eksplisit.

t.new TestInner(); - hanyalah cara untuk menetapkan argumen tersembunyi pertama ke konstruktor TestInner, bukan tipe

method () sama dengan:

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

TestInner sama dengan:

class TestInner{
    private Test this$0;

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

7

Saat kelas dalam ditambahkan ke Java dalam versi 1.1 bahasa, kelas tersebut awalnya didefinisikan sebagai transformasi ke kode 1.0 yang kompatibel. Jika Anda melihat contoh transformasi ini, saya pikir itu akan membuatnya jauh lebih jelas bagaimana sebenarnya kelas dalam bekerja.

Perhatikan kode dari jawaban Ian Roberts:

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

Ketika diubah menjadi kode yang kompatibel 1.0, kelas dalam itu Barakan menjadi seperti ini:

class Foo$Bar {
  private Foo this$0;

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

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

Nama kelas dalam diawali dengan nama kelas luar untuk membuatnya unik. this$0Anggota pribadi tersembunyi ditambahkan yang memegang salinan luar this. Dan konstruktor tersembunyi dibuat untuk menginisialisasi anggota itu.

Dan jika Anda melihat createBarmetodenya, itu akan berubah menjadi seperti ini:

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

Jadi mari kita lihat apa yang terjadi ketika Anda menjalankan kode berikut.

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

Pertama kita membuat instance dari Foodan menginisialisasi valanggota ke 5 (yaitu f.val = 5).

Selanjutnya kita panggil f.createBar(), yang membuat instance dari Foo$Bardan menginisialisasi this$0anggota ke nilai yang thisditeruskan dari createBar(yaitu b.this$0 = f).

Akhirnya kita sebut b.printVal()yang mencoba mencetak b.this$0.valyaitu f.valyang 5.

Nah, itu adalah contoh reguler dari kelas dalam. Mari kita lihat apa yang terjadi saat membuat contoh Bardari luar Foo.

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

Menerapkan transformasi 1.0 kami lagi, baris kedua itu akan menjadi seperti ini:

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

Ini hampir identik dengan f.createBar()panggilan. Sekali lagi kami membuat instance dari Foo$Bardan menginisialisasi this$0anggota ke f. Sekali lagi b.this$0 = f,.

Dan lagi ketika Anda menelepon b.printVal(), Anda mencetak b.thi$0.valyang f.valmana 5.

Hal utama yang perlu diingat adalah bahwa kelas dalam memiliki anggota tersembunyi yang memegang salinan dari thiskelas luar. Saat Anda membuat instance kelas dalam dari dalam kelas luar, itu secara implisit diinisialisasi dengan nilai saat ini this. Saat Anda membuat instance kelas dalam dari luar kelas luar, Anda secara eksplisit menentukan instance kelas luar mana yang akan digunakan, melalui awalan pada newkata kunci.


4

Anggap saja new receiversebagai token tunggal. Semacam nama fungsi dengan spasi di dalamnya.

Tentu saja, kelas KnockKnockServertidak secara harfiah memiliki fungsi bernama new receiver, tapi saya rasa sintaksnya dimaksudkan untuk menyarankan itu. Ini dimaksudkan agar terlihat seperti Anda memanggil fungsi yang membuat instance baru KnockKnockServer.receivermenggunakan instance tertentu KnockKnockServeruntuk akses apa pun ke kelas yang melingkupinya.


Terima kasih, ya - sekarang saya terbantu untuk menganggapnya new receiversebagai satu tanda! Terima kasih banyak!
Kopi

1

Pembayangan

Jika sebuah deklarasi tipe (seperti variabel anggota atau nama parameter) dalam lingkup tertentu (seperti kelas dalam atau definisi metode) memiliki nama yang sama dengan deklarasi lain dalam cakupan yang melingkupinya, maka deklarasi tersebut membayangi deklarasi tersebut dari ruang lingkup terlampir. Anda tidak dapat merujuk ke deklarasi berbayang hanya dengan namanya. Contoh berikut, ShadowTest, menunjukkan ini:

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

Berikut adalah keluaran dari contoh ini:

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

Contoh ini mendefinisikan tiga variabel bernama x: Variabel anggota kelas ShadowTest, variabel anggota kelas dalam FirstLevel, dan parameter dalam metode methodInFirstLevel. Variabel x yang didefinisikan sebagai parameter metode methodInFirstLevel membayangi variabel kelas dalam FirstLevel. Akibatnya, saat Anda menggunakan variabel x dalam metode methodInFirstLevel, ini merujuk ke parameter metode. Untuk merujuk ke variabel anggota kelas dalam FirstLevel, gunakan kata kunci this untuk mewakili cakupan yang melingkupi:

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

Mengacu ke variabel anggota yang menyertakan cakupan yang lebih besar dengan nama kelasnya. Misalnya, pernyataan berikut mengakses variabel anggota kelas ShadowTest dari metode methodInFirstLevel:

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

Lihat dokumennya

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.