`someObject.new` ทำอะไรใน Java?


100

ใน Java ฉันเพิ่งพบว่ารหัสต่อไปนี้ถูกกฎหมาย:

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

FYI ผู้รับเป็นเพียงคลาสผู้ช่วยที่มีลายเซ็นดังต่อไปนี้:

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

ฉันไม่เคยเห็นXYZ.newสัญกรณ์มาก่อน ทำงานอย่างไร? มีวิธีใดในการเขียนโค้ดที่ตรงตามอัตภาพมากกว่านี้หรือไม่?


7
สำหรับการอ้างอิงของคุณระดับชั้น
Alvin Wong

1
นอกจากนี้ฉันเคยเชื่อว่าnewเป็นโอเปอเรเตอร์ในหลายภาษา (ฉันคิดว่าคุณอาจใช้newC ++ มากเกินไปหรือไม่) คลาสภายในของ Java ค่อนข้างแปลกสำหรับฉัน
Alvin Wong

5
ไม่มีคำถามโง่ ๆ ใน StackOverflow!
Isaac Rabinovitch

2
@IsaacRabinovitch - ไม่มีคำถามโง่ ๆ อย่างไรก็ตามมีคนโง่จำนวนมาก (และบางครั้งก็ตอบแบบโง่ ๆ เช่นกัน)
Hot Licks

2
@HotLicks และคำจำกัดความของคำถามโง่ ๆ ของคุณคืออะไร? คุณฉลาดเกินกว่าที่จะถามฉันคิดว่า ดีที่คุณมีความภาคภูมิใจในตัวเองมากขนาดนี้
Isaac Rabinovitch

คำตอบ:


120

เป็นวิธีการสร้างอินสแตนซ์คลาสภายในแบบไม่คงที่จากภายนอกคลาสที่มีเนื้อหาตามที่อธิบายไว้ในเอกสาร Oracleเอกสารของ

อินสแตนซ์คลาสภายในทุกตัวเชื่อมโยงกับอินสแตนซ์ของคลาสที่มี เมื่อคุณnewเป็นคลาสภายในจากภายในคลาสที่มีคลาสนั้นจะใช้thisอินสแตนซ์ของคอนเทนเนอร์ตามค่าเริ่มต้น:

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

แต่ถ้าคุณต้องการสร้างอินสแตนซ์ของ Bar นอก Foo หรือเชื่อมโยงอินสแตนซ์ใหม่กับอินสแตนซ์ที่มีนอกเหนือthisจากนั้นคุณต้องใช้สัญกรณ์นำหน้า

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

18
และอย่างที่คุณบอกสิ่งนี้อาจสร้างความสับสนได้อย่างไม่น่าเชื่อ ตามหลักการแล้วชั้นเรียนภายในควรเป็นรายละเอียดการใช้งานของชั้นนอกและไม่เปิดเผยกับโลกภายนอก
Eric Jablow

10
@EricJablow เป็นหนึ่งในไวยากรณ์ที่ต้องมีอยู่เพื่อให้ข้อมูลจำเพาะสอดคล้องกัน แต่ 99.9999% ของเวลาที่คุณไม่จำเป็นต้องใช้จริง f.newหากบุคคลภายนอกจริงๆต้องสร้างอินสแตนซ์บาร์แล้วผมให้วิธีการที่โรงงานฟูแทนที่จะได้ใช้พวกเขา
Ian Roberts

แก้ไขฉันถ้าฉันผิด แต่ถ้าpublicระดับการเข้าถึงKnockKnockServer.receiverถูกสร้างขึ้นprivateมันจะเป็นไปไม่ได้ที่จะสร้างอินสแตนซ์ด้วยวิธีนี้ใช่ไหม หากต้องการขยายความคิดเห็นของ @EricJablow โดยทั่วไปคลาสภายในควรเป็นprivateระดับการเข้าถึงโดยปริยาย
Andrew Bissell

1
@AndrewBissell ใช่ แต่ก็เป็นไปไม่ได้ที่จะอ้างถึงreceiverชั้นเรียนจากภายนอก ถ้าฉันออกแบบมันฉันอาจจะมีคลาสสาธารณะ แต่ตัวสร้างได้รับการป้องกันหรือแพ็กเกจส่วนตัวและมีวิธีในKnockKnockServerการสร้างอินสแตนซ์ตัวรับ
Ian Roberts

1
@emory ฉันไม่ได้พูดอย่างนั้นฉันรู้ว่าอาจมีเหตุผลที่ถูกต้องอย่างสมบูรณ์ที่ต้องการทำให้คลาสชั้นในเป็นสาธารณะและส่งคืนอินสแตนซ์ของคลาสภายในจากวิธีการของชั้นนอก แต่ฉันมักจะออกแบบโค้ดของฉันเช่นนั้น " คนนอก" x.newไม่จำเป็นต้องสร้างอินสแตนซ์ของระดับชั้นโดยตรงโดยใช้
Ian Roberts

18

ดูตัวอย่างนี้:

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

การใช้ javap เราสามารถดูคำแนะนำที่สร้างขึ้นสำหรับรหัสนี้

วิธีการหลัก:

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
}

ตัวสร้างชั้นใน:

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

}

ทุกอย่างเป็นเรื่องง่าย - เมื่ออัญเชิญ TestInner คอนสตรัค, Java ผ่านอินสแตนซ์ทดสอบเป็นอาร์กิวเมนต์แรกหลัก: 12 ไม่ได้มองว่า TestInner ไม่ควรมีตัวสร้างอาร์กิวเมนต์ TestInner ในการเปิดเพียงบันทึกการอ้างอิงถึงวัตถุแม่, ทดสอบ $ TestInner: 2 เมื่อคุณเรียกใช้ตัวสร้างคลาสภายในจากเมธอดอินสแตนซ์การอ้างอิงไปยังอ็อบเจ็กต์พาเรนต์จะถูกส่งผ่านโดยอัตโนมัติดังนั้นคุณไม่จำเป็นต้องระบุ จริงๆแล้วมันผ่านทุกครั้ง แต่เมื่อเรียกจากภายนอกควรส่งผ่านอย่างชัดเจน

t.new TestInner(); - เป็นเพียงวิธีระบุอาร์กิวเมนต์แรกที่ซ่อนอยู่ให้กับตัวสร้าง TestInner ไม่ใช่ประเภท

method ()เท่ากับ:

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

TestInnerเท่ากับ:

class TestInner{
    private Test this$0;

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

7

เมื่อคลาสภายในถูกเพิ่มลงใน Java ในเวอร์ชัน 1.1 ของภาษาเดิมพวกเขาถูกกำหนดให้เป็นการแปลงเป็นโค้ดที่เข้ากันได้ 1.0 ถ้าคุณดูตัวอย่างของการเปลี่ยนแปลงนี้ฉันคิดว่ามันจะทำให้ชัดเจนขึ้นมากว่าคลาสชั้นในทำงานอย่างไร

พิจารณารหัสจากคำตอบของ 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();
  }
}

เมื่อเปลี่ยนเป็นโค้ดที่เข้ากันได้ 1.0 คลาสภายในBarนั้นจะกลายเป็นดังนี้:

class Foo$Bar {
  private Foo this$0;

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

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

ชื่อคลาสด้านในถูกนำหน้าด้วยชื่อคลาสภายนอกเพื่อให้ไม่ซ้ำกัน มีการthis$0เพิ่มสมาชิกส่วนตัวที่ซ่อนอยู่ซึ่งเก็บสำเนาของด้านนอกthisสมาชิกจะมีการเพิ่มที่เก็บสำเนาของด้านนอกได้และตัวสร้างที่ซ่อนอยู่จะถูกสร้างขึ้นเพื่อเริ่มต้นสมาชิกนั้น

และถ้าคุณดูcreateBarวิธีการมันจะถูกเปลี่ยนเป็นแบบนี้:

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

ลองดูว่าจะเกิดอะไรขึ้นเมื่อคุณรันโค้ดต่อไปนี้

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

อันดับแรกเราสร้างอินสแตนซ์ของอินสแตนซ์Fooและเริ่มต้นvalสมาชิกเป็น 5 (เช่นf.val = 5)

ต่อไปเราจะเรียกf.createBar()ว่าอินสแตนซ์ของอินสแตนซ์Foo$Barและเริ่มต้นthis$0สมาชิกเป็นค่าของการthisส่งผ่านจากcreateBar(เช่นb.this$0 = f)

ในที่สุดเราเรียกb.printVal()ซึ่งพยายามที่จะพิมพ์b.this$0.valซึ่งเป็นf.valซึ่งเป็น 5

ตอนนี้เป็นอินสแตนซ์ปกติของคลาสชั้นใน ดู Let 's สิ่งที่เกิดขึ้นเมื่อ instantiating จากภายนอกBarFoo

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

ใช้การแปลง 1.0 ของเราอีกครั้งบรรทัดที่สองจะกลายเป็นดังนี้:

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

นี่แทบจะเหมือนกับการf.createBar()โทร อีกครั้งเรากำลังสร้างอินสแตนซ์Foo$Barและเริ่มต้นthis$0สมาชิกเป็น f อีกครั้งb.this$0 = fดังนั้นอีกครั้ง

และอีกครั้งเมื่อคุณโทรb.printVal()คุณกำลังพิมพ์b.thi$0.valซึ่งก็f.valคือ 5

สิ่งสำคัญที่ต้องจำไว้คือชั้นในมีสมาชิกที่ซ่อนอยู่ซึ่งถือสำเนาthisจากชั้นนอก เมื่อคุณยกตัวอย่างระดับชั้นจากภายในชั้นนอกมัน initialised thisโดยปริยายด้วยมูลค่าปัจจุบันของ เมื่อคุณสร้างอินสแตนซ์คลาสภายในจากภายนอกคลาสภายนอกคุณต้องระบุอินสแตนซ์ของคลาสภายนอกที่จะใช้อย่างชัดเจนผ่านคำนำหน้าของnewคีย์เวิร์ด


4

คิดว่าnew receiverเป็นโทเค็นเดียว คล้ายกับชื่อฟังก์ชันที่มีช่องว่างอยู่

แน่นอนว่าคลาสKnockKnockServerนี้ไม่มีฟังก์ชันที่ตั้งชื่ออย่างแท้จริงnew receiverแต่ฉันเดาว่าไวยากรณ์มีไว้เพื่อแนะนำสิ่งนั้น ดูเหมือนว่าคุณกำลังเรียกใช้ฟังก์ชันที่สร้างอินสแตนซ์ใหม่ของการKnockKnockServer.receiverใช้อินสแตนซ์เฉพาะKnockKnockServerสำหรับการเข้าถึงคลาสที่ปิดล้อม


ขอบคุณใช่ - ตอนนี้ช่วยให้ฉันคิดว่าnew receiverเป็นโทเค็นเดียว! ขอบคุณมาก ๆ!
กาแฟ

1

เงา

หากการประกาศประเภท (เช่นตัวแปรสมาชิกหรือชื่อพารามิเตอร์) ในขอบเขตเฉพาะ (เช่นคลาสภายในหรือนิยามวิธีการ) มีชื่อเดียวกันกับการประกาศอื่นในขอบเขตการปิดการประกาศจะแสดงเงาการประกาศ ของขอบเขตการปิดล้อม คุณไม่สามารถอ้างถึงการประกาศเงาโดยใช้ชื่อเพียงอย่างเดียว ตัวอย่างต่อไปนี้ 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);
    }
}

ต่อไปนี้เป็นผลลัพธ์ของตัวอย่างนี้:

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

ตัวอย่างนี้กำหนดตัวแปรสามตัวชื่อ x: ตัวแปรสมาชิกของคลาส ShadowTest ตัวแปรสมาชิกของคลาสชั้นใน FirstLevel และพารามิเตอร์ใน method methodInFirstLevel ตัวแปร x ที่กำหนดเป็นพารามิเตอร์ของ method methodInFirstLevel จะทำเงาตัวแปรของ FirstLevel คลาสภายใน ดังนั้นเมื่อคุณใช้ตัวแปร x ใน method methodInFirstLevel จะอ้างถึงพารามิเตอร์ method หากต้องการอ้างถึงตัวแปรสมาชิกของคลาสภายใน FirstLevel ให้ใช้คีย์เวิร์ดนี้เพื่อแสดงขอบเขตการปิดล้อม:

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

อ้างถึงตัวแปรสมาชิกที่ล้อมรอบขอบเขตที่ใหญ่กว่าด้วยชื่อคลาสที่เป็นสมาชิก ตัวอย่างเช่นคำสั่งต่อไปนี้เข้าถึงตัวแปรสมาชิกของคลาส ShadowTest จาก method methodInFirstLevel:

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

อ้างถึงเอกสาร

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.