Что делает someObject.new в Java?

100

В Java я только что обнаружил, что следующий код является допустимым:

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

К вашему сведению, получатель - это просто вспомогательный класс со следующей подписью:

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

Я никогда раньше не видел XYZ.newобозначений. Как это работает? Есть ли способ написать более традиционный код?

Кофе
источник
7
Для справки, внутренний класс .
Элвин Вонг
1
Кроме того, я считал, что newэто оператор на многих языках. (Я думал, вы также можете перегрузить newв C ++?) Однако внутренний класс Java для меня немного странный.
Элвин Вонг
5
В StackOverflow нет глупых вопросов!
Исаак Рабинович
2
@IsaacRabinovitch - Глупых вопросов нет. Однако есть много глупостей. (И случайный глупый ответ.)
Hot Licks
2
@HotLicks А какой у тебя глупый вопрос? Полагаю, вы слишком умны, чтобы об этом спрашивать. Хорошо, что у тебя такая высокая самооценка.
Исаак Рабинович

Ответы:

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
И, как видите, это может сбивать с толку. В идеале внутренние классы должны быть деталями реализации внешнего класса и не должны быть открыты внешнему миру.
Эрик Джаблоу
10
@EricJablow действительно, это один из тех фрагментов синтаксиса, который должен существовать, чтобы поддерживать согласованность спецификации, но в 99,9999% случаев вам на самом деле не нужно его использовать. Если посторонним действительно нужно создавать экземпляры Bar, я бы предоставил фабричный метод для Foo, а не заставил бы их использовать f.new.
Ян Робертс
Поправьте меня, если я ошибаюсь, но если бы publicуровень доступа KnockKnockServer.receiverбыл включен, privateто было бы невозможно создать экземпляр таким образом, верно? Чтобы расширить комментарий @EricJablow, внутренние классы обычно всегда должны по умолчанию использовать privateуровень доступа.
Эндрю Бисселл
1
@AndrewBissell да, но также было бы невозможно вообще ссылаться на receiverкласс извне. Если бы я его разрабатывал, у меня, вероятно, был бы класс public, но его конструктор был бы защищенным или закрытым для пакета, и имел бы метод KnockKnockServerдля создания экземпляров получателя.
Ян Робертс
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 передает экземпляр Test в качестве первого аргумента main: 12 . Не смотря на то, что TestInner не должен иметь конструктора аргументов. TestInner, в свою очередь, просто сохраняет ссылку на родительский объект Test $ 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. Если вы посмотрите на пример этого преобразования, я думаю, он сделает более понятным, как на самом деле работает внутренний класс.

Рассмотрим код из ответа Яна Робертса:

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.

Теперь это было обычное создание внутреннего класса. Давайте посмотрим, что происходит при создании экземпляра Barизвне Foo.

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внешнего класса. Когда вы создаете экземпляр внутреннего класса из внешнего класса, он неявно инициализируется текущим значением 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 и параметр в методе methodInFirstLevel. Переменная x, определенная как параметр метода methodInFirstLevel, затеняет переменную внутреннего класса FirstLevel. Следовательно, когда вы используете переменную x в методе methodInFirstLevel, она ссылается на параметр метода. Чтобы обратиться к переменной-члену внутреннего класса FirstLevel, используйте ключевое слово this для представления охватывающей области:

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

Обращайтесь к переменным-членам, охватывающим более крупные области, по имени класса, к которому они принадлежат. Например, следующий оператор обращается к переменной-члену класса ShadowTest из метода methodInFirstLevel:

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

Обратитесь к документации

JavaTechnical
источник