Почему вызов родительского конструктора не является первым вызовом в сгенерированном компилятором конструкторе для класса Inner?

Рассмотрим следующий класс Test, чтобы продемонстрировать поведение внутреннего класса в Java. Основной код находится в методе run. Остальное — это просто сантехнический код.

 public class Test {

        private static Test instance = null;

        private Test() {
        }

        private void run() {
            new Sub().foo();  
        }

        public static void main(String[] args) {
            instance = new Test();
            instance.run();
        }

        class Super {
            protected void foo() {
                System.out.println("Test$Super.Foo");
            }
        }

        class Sub extends Super {
            public void foo() {
                System.out.println("Test$Sub.Foo");
                super.foo();
            }
        }
    }

Я просто печатаю ниже вывод javap для скрытого конструктора Sub:

so.Test$Sub(so.Test);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #1                  // Field this$0:Lso/Test;
       5: aload_0       
       6: aload_1       
       7: invokespecial #2                  // Method so/Test$Super."<init>":(Lso/Test;)V
  10: return   

Обычно компилятор гарантирует, что конструктор подкласса сначала вызовет конструктор суперкласса, прежде чем перейти к инициализации своих собственных полей. Это помогает в правильно сконструированном объекте, но я вижу отклонение от нормативного поведения в случае конструктора, который компилятор генерирует для внутреннего класса. Почему так? Это указано в JLS?

PS: я знаю, что внутренний класс содержит скрытую ссылку на внешний класс, и эта ссылка устанавливается здесь в приведенном выше выводе javap. Но вопрос в том, почему он устанавливается перед вызовом суперконструктора. Что мне не хватает?


person Geek    schedule 07.08.2014    source источник
comment
Охватывающий экземпляр по спецификации доступен из конструктора суперкласса.   -  person William F. Jameson    schedule 07.08.2014


Ответы (1)


Внутренний класс — это абстракция, которая должна быть максимально прозрачна для Java-программиста. Рассмотрите следующую структуру класса и подумайте, что произойдет, если вы установите поле $this только после вызова суперконструктора внутреннего класса.

class Foo {
  Foo() { System.out.println(foo()); }

  String foo() { return "foo"; }
}

class Bar {
  String bar() { return "bar"; }

  class Qux extends Foo {
    @Override 
    String foo() { return bar(); }
  }
}

Обратите внимание, как переопределенный метод в классе Qux вызывает метод своего внешнего класса Bar. Чтобы это работало, поле $this, содержащее экземпляр Bar, должно быть установлено до вызова суперконструктора Foo. В противном случае вы получите NullPointerException, так как поле еще не инициализировано. Чтобы сделать это более понятным, посмотрите на следующую цепочку вызовов любого экземпляра экземпляра Qux:

Qux() -> Foo() -> this.foo() -> $this.bar()

Как программист, не знакомый с реализацией внутреннего класса, вам может быть интересно, откуда берется это исключение. Чтобы сделать внутреннюю абстракцию класса прозрачной, вы должны сначала установить поле, иначе вы застряли бы с весьма дырявая абстракция. Я не утверждаю, что это делает приведенный выше пример хорошей реализацией, но это законно.

person Rafael Winterhalter    schedule 07.08.2014
comment
Вызов переопределяемых методов из конструктора всегда может привести к отображению неинициализированных полей, это не относится к внутренним классам. - person William F. Jameson; 07.08.2014
comment
Да, но это явно разрешено для внутренних классов. Но, как я уже сказал в своем ответе, этого все же следует избегать. Обычно это признак плохого дизайна. - person Rafael Winterhalter; 07.08.2014
comment
Вызов переопределяемых методов всегда разрешен, просто не работает. Я только что прочитал 8.8.7.1, но все еще не понимаю, что вы имеете в виду. Было бы очень полезно для качества вашего ответа, если бы вы фактически процитировали оператор JLS, который делает настройку включающего экземпляра обязательной перед супервызовом. - person William F. Jameson; 07.08.2014
comment
Я понял, что вложил в раздел какой-то смысл, которого там не было. На самом деле я не могу найти соответствующую заметку в JLS. - person Rafael Winterhalter; 07.08.2014