`SomeObject.new` làm gì trong Java?


100

Trong Java, tôi vừa phát hiện ra rằng mã sau là hợp pháp:

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

FYI, người nhận chỉ là một lớp trợ giúp với chữ ký sau:

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

Tôi chưa bao giờ nhìn thấy XYZ.newký hiệu trước đây. Nó hoạt động như thế nào? Có cách nào để viết mã thông thường hơn không?


7
Để bạn tham khảo, lớp bên trong .
Alvin Wong,

1
Ngoài ra, tôi đã tin rằng đó newlà một toán tử trong rất nhiều ngôn ngữ. (Tôi nghĩ bạn cũng có thể quá tải newtrong C ++?) Mặc dù vậy, lớp bên trong của Java hơi lạ đối với tôi.
Alvin Wong,

5
Không có câu hỏi ngớ ngẩn nào trên StackOverflow!
Isaac Rabinovitch

2
@IsaacRabinovitch - Không có câu hỏi ngu ngốc nào. Tuy nhiên, có rất nhiều cái ngớ ngẩn. (Và một câu trả lời ngớ ngẩn thỉnh thoảng là tốt.)
Hot Licks

2
@HotLicks Và định nghĩa của bạn về một câu hỏi ngớ ngẩn là gì? Tôi cho rằng một điều bạn quá thông minh để cần hỏi. Thật tốt khi bạn có rất nhiều lòng tự trọng.
Isaac Rabinovitch

Câu trả lời:


120

Đó là cách để khởi tạo một lớp bên trong không tĩnh từ bên ngoài phần thân lớp chứa, như được mô tả trong tài liệu Oracle .

Mọi cá thể lớp bên trong được liên kết với một thể hiện của lớp chứa nó. Khi bạn newmột lớp bên trong từ bên trong lớp chứa của nó, nó sử dụng thisthể hiện của vùng chứa theo mặc định:

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

Nhưng nếu bạn muốn tạo một thể hiện của Bar bên ngoài Foo, hoặc liên kết một thể hiện mới với một thể hiện chứa khác thisthì bạn phải sử dụng ký hiệu tiền tố.

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

18
Và, như bạn có thể nói, điều này có thể cực kỳ khó hiểu. Lý tưởng nhất, các lớp bên trong phải là chi tiết triển khai của lớp bên ngoài và không được tiếp xúc với thế giới bên ngoài.
Eric Jablow,

10
@EricJablow thực sự, đó là một trong những bit cú pháp phải tồn tại để giữ cho thông số kỹ thuật nhất quán nhưng 99,9999% thời gian bạn thực sự không cần sử dụng nó. Nếu những người bên ngoài thực sự cần tạo các phiên bản Bar thì tôi sẽ cung cấp một phương thức factory trên Foo hơn là để họ sử dụng f.new.
Ian Roberts,

Hãy sửa cho tôi nếu tôi sai nhưng nếu publiccấp độ truy cập trên KnockKnockServer.receiverđược thực hiện privatethì không thể khởi tạo theo cách này, phải không? Để mở rộng nhận xét của @EricJablow, các lớp bên trong thường luôn được mặc định là một privatecấp độ truy cập.
Andrew Bissell,

1
@AndrewBissell vâng, nhưng cũng không thể tham khảo receiverlớp học từ bên ngoài. Nếu tôi được thiết kế nó tôi có lẽ muốn có lớp nào nhưng nó constructor bảo vệ hoặc gói-tin, và có một phương pháp trên KnockKnockServerđể tạo ra trường hợp người nhận.
Ian Roberts,

1
@emory Tôi không nói vậy, tôi biết có thể có những lý do hoàn toàn hợp lệ để muốn công khai một lớp bên trong và trả về các thể hiện của lớp bên trong từ các phương thức của bên ngoài, nhưng tôi sẽ có xu hướng thiết kế mã của mình như vậy " bên ngoài "không cần trực tiếp tạo các thể hiện của lớp bên trong bằng cách sử dụng x.new.
Ian Roberts,

18

Hãy xem ví dụ này:

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

Sử dụng javap, chúng tôi có thể xem hướng dẫn được tạo cho mã này

Phương pháp chính:

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
}

Hàm tạo lớp bên trong:

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

}

Mọi thứ đều đơn giản - khi gọi phương thức khởi tạo TestInner, java chuyển đối tượng Test làm đối số đầu tiên chính: 12 . Không nhìn vào TestInner đó sẽ không có hàm tạo đối số. Đến lượt mình, TestInner chỉ lưu tham chiếu đến đối tượng mẹ, Test $ TestInner: 2 . Khi bạn đang gọi phương thức khởi tạo lớp bên trong từ một phương thức thể hiện, tham chiếu đến đối tượng mẹ sẽ được chuyển tự động, vì vậy bạn không cần phải chỉ định nó. Trên thực tế, nó đi qua mọi lúc, nhưng khi gọi từ bên ngoài, nó phải được chuyển một cách rõ ràng.

t.new TestInner(); - chỉ là một cách để chỉ định đối số ẩn đầu tiên cho hàm tạo TestInner, không phải là một kiểu

method () bằng:

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

TestInner bằng:

class TestInner{
    private Test this$0;

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

7

Khi các lớp bên trong được thêm vào Java trong phiên bản 1.1 của ngôn ngữ, ban đầu chúng được định nghĩa là sự chuyển đổi sang mã tương thích 1.0. Nếu bạn nhìn vào một ví dụ về sự chuyển đổi này, tôi nghĩ nó sẽ làm cho nó rõ ràng hơn rất nhiều về cách một lớp bên trong thực sự hoạt động.

Hãy xem xét đoạn mã từ câu trả lời của 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();
  }
}

Khi được chuyển đổi thành mã tương thích 1.0, lớp bên trong Barđó sẽ trở thành một cái gì đó như thế này:

class Foo$Bar {
  private Foo this$0;

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

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

Tên lớp bên trong được đặt trước tên lớp bên ngoài để làm cho nó là duy nhất. Một this$0thành viên riêng tư ẩn được thêm vào để giữ một bản sao của bên ngoài this. Và một hàm tạo ẩn được tạo để khởi tạo thành viên đó.

Và nếu bạn nhìn vào createBarphương thức, nó sẽ được chuyển đổi thành một cái gì đó như thế này:

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

Vì vậy, hãy xem điều gì sẽ xảy ra khi bạn thực thi đoạn mã sau.

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

Đầu tiên, chúng tôi khởi tạo một thể hiện của Foovà intialise valthành viên thành 5 (tức là f.val = 5).

Tiếp theo, chúng tôi gọi f.createBar(), khởi tạo một thể hiện của Foo$Barvà khởi tạo this$0thành viên thành giá trị thisđược truyền vào từ createBar(tức là b.this$0 = f).

Cuối cùng chúng ta gọi b.printVal()đó cố gắng để in b.this$0.valmà là f.valđó là 5.

Bây giờ đó là một mô tả thông thường của một lớp bên trong. Hãy xem điều gì sẽ xảy ra khi khởi tạo Bartừ bên ngoài Foo.

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

Áp dụng lại phép biến đổi 1.0 của chúng tôi, dòng thứ hai đó sẽ trở thành như thế này:

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

Điều này gần giống với f.createBar()cuộc gọi. Một lần nữa, chúng tôi khởi tạo một phiên bản của Foo$Barvà khởi tạo this$0thành viên thành f. Vì vậy, một lần nữa b.this$0 = f,.

Và một lần nữa khi bạn gọi b.printVal(), bạn đang in b.thi$0.valmà là f.valđó là 5.

Điều quan trọng cần nhớ là lớp bên trong có một thành viên ẩn giữ một bản sao của thislớp bên ngoài. Khi bạn khởi tạo một lớp bên trong từ bên trong lớp bên ngoài, nó được khởi tạo ngầm với giá trị hiện tại là this. Khi bạn khởi tạo lớp bên trong từ bên ngoài lớp ngoài, bạn chỉ định rõ ràng phiên bản của lớp bên ngoài sẽ sử dụng, thông qua tiền tố trên newtừ khóa.


4

Hãy new receivercoi như một mã thông báo duy nhất. Kiểu giống như một tên chức năng với một khoảng trắng trong đó.

Tất nhiên, lớp KnockKnockServerkhông có một hàm được đặt tên theo nghĩa đen new receiver, nhưng tôi đoán rằng cú pháp được dùng để gợi ý điều đó. Nó có nghĩa là bạn đang gọi một hàm tạo một phiên bản mới KnockKnockServer.receiverbằng cách sử dụng một phiên bản cụ thể của KnockKnockServerbất kỳ quyền truy cập nào vào lớp bao quanh.


Cảm ơn, vâng - nó giúp tôi bây giờ nghĩ về new receivermột mã thông báo! cảm ơn rất nhiều!
Cà phê

1

Đổ bóng

Nếu một khai báo của một kiểu (chẳng hạn như một biến thành viên hoặc một tên tham số) trong một phạm vi cụ thể (chẳng hạn như một lớp bên trong hoặc một định nghĩa phương thức) có cùng tên với một khai báo khác trong phạm vi bao quanh, thì khai báo sẽ che khuất khai báo. của phạm vi bao quanh. Bạn không thể tham chiếu đến một khai báo ẩn chỉ bằng tên của nó. Ví dụ sau, ShadowTest, minh họa điều này:

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

Sau đây là kết quả của ví dụ này:

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

Ví dụ này định nghĩa ba biến có tên x: Biến thành viên của lớp ShadowTest, biến thành viên của lớp FirstLevel bên trong và tham số trong phương thức methodInFirstLevel. Biến x được định nghĩa là một tham số của phương thức methodInFirstLevel làm bóng biến của lớp bên trong FirstLevel. Do đó, khi bạn sử dụng biến x trong phương thức methodInFirstLevel, nó tham chiếu đến tham số phương thức. Để tham chiếu đến biến thành viên của FirstLevel lớp bên trong, hãy sử dụng từ khóa this để đại diện cho phạm vi bao quanh:

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

Tham chiếu đến các biến thành viên bao gồm các phạm vi lớn hơn theo tên lớp mà chúng thuộc về. Ví dụ: câu lệnh sau truy cập biến thành viên của lớp ShadowTest từ phương thức methodInFirstLevel:

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

Tham khảo tài liệu

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.