Подтвердить что ты не робот

Виртуальные таблицы и аннотация в Java

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

public abstract class Base {
    public int x = 1;
    public Base() {
        foo();
    }
    public abstract void foo();
}

public class Derived extends Base {
    int x = 2;
    @Override
    public void foo() {
        System.out.println("Derived: "+x);
    }
}

class Main {
    public static void main(String... args) {
        Base base = new Derived();
        base.foo();
    }
}

Они спросили:

Что будет напечатано?

Если мы использовали С++, я думаю, что код должен давать ошибку компиляции, потому что, когда конструктор Derived вызывается сначала, вызывается конструктор класса Base. На данный момент метод foo не существует.

Кроме того, я знаю, что сначала вызывается метод наследуемого класса, прежде чем все переменные создаются.

Однако в Java мы получаем:

Derived: 0
Derived: 2

Почему?

Я знаю, что как в С++ Java наследование всегда основано на виртуальных таблицах, и конструктор класса Base вызывается перед конструктором класса Derived.

4b9b3361

Ответ 1

Это порядок выполнения кода. Подробнее см. Ниже.

  • main()
    • вызывает Derived.<init>() (неявный нулевой конструктор)
      • вызывает Base.<init>()
        • устанавливает Base.x в 1.
        • вызывает Derived.foo()
          • печатает Derived.x, который по-прежнему имеет значение по умолчанию 0
      • устанавливает Derived.x в 2.
    • вызывает Derived.foo().
      • печатает Derived.x, который теперь 2.

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

Тень поля

Base x и Derived x - совершенно разные поля, которые имеют одно и то же имя. Derived.foo выводит Derived.x, а не Base.x, поскольку последний "затенен" первым.

Неявные конструкторы

Так как Derived не имеет явного конструктора, компилятор генерирует неявный конструктор с нулевым аргументом. В Java каждый конструктор должен вызывать один конструктор суперкласса (за исключением Object, который не имеет суперкласса), что дает суперклассу возможность безопасно инициализировать свои поля. Генератор, созданный компилятором, просто вызывает нулевой конструктор своего суперкласса. (Если суперкласс не имеет нулевого конструктора, возникает ошибка компиляции.)

Итак, неявный конструктор Derived выглядит как

public Derived() {
    super();
}

Инициализационные блоки и определения полей

Блоки инициализатора объединяются в порядке объявления, чтобы сформировать большой блок кода, который вставляется во все конструкторы. В частности, он вставлен после вызова super(), но до остальной части конструктора. Назначения начальных значений в определениях полей обрабатываются так же, как блоки инициализации.

Итак, если мы имеем

class Test {
    {x=1;}
    int x = 2;
    {x=3;}

    Test() {
        x = 0;
    }
}

Это эквивалентно

class Test {
    int x;

    {
        x = 1;
        x = 2;
        x = 3;
    }

    Test() {
        x = 0;
    }
}

И вот что будет выглядеть скомпилированный конструктор:

Test() {
    // implicit call to the superclass constructor, Object.<init>()
    super();
    // initializer blocks, in declaration order
    x = 1
    x = 2
    x = 3
    // the explicit constructor code
    x = 0
}

Теперь вернемся к Base и Derived. Если мы декомпилировали их конструкторы, мы увидели бы что-то вроде

public Base() {
    super(); // Object.<init>()
    x = 1; // assigns Base.x
    foo();
}

public Derived() {
    super(); // Base.<init>()
    x = 2; // assigns Derived.x
}

Виртуальные Invocations

В Java вызовы методов экземпляра обычно проходят через таблицы виртуальных методов. (Есть исключения из этого: конструкторы, частные методы, конечные методы и методы конечных классов не могут быть переопределены, поэтому эти методы могут быть вызваны без прохождения через vtable. И вызовы super не проходят через vtables, так как они по сути не полиморфный.)

Каждый объект содержит указатель на дескриптор класса, содержащий vtable. Этот указатель устанавливается сразу после выделения объекта (с помощью NEW) и перед вызовом любых конструкторов. Поэтому в Java безопасно для конструкторов совершать вызовы виртуальных методов, и они будут правильно направлены на целевую реализацию виртуального метода.

Поэтому, когда конструктор Base вызывает foo(), он вызывает Derived.foo, который печатает Derived.x. Но Derived.x еще не назначено, поэтому значение по умолчанию 0 читается и печатается.

Ответ 2

Очевидно, вызывается только производный класс foo().

Он печатает 0 в первый раз, потому что это происходит до, назначая x = 2, что происходит только в конструкторе Derived, после завершения инициализации Base. Он печатает 0, а не 1, потому что к нему обращается Derived.x и не Base.x, и он еще не инициализирован и по-прежнему 0. Объявление x в Derived скрывает поле в Base, поэтому, когда Derived печатает x, он печатает Derived.x.

EDIT: порядок активации при создании Derived(): [схематический]

1. create Base:
   1.1. assign Base.x = 1
   1.2. invoke foo()
      1.2.1 print Derived: Derived.x //Derived.x was not initialized here yet!
2. assign Derived.x = 2

Вторая - тривиальная и ожидаемая (по крайней мере, по моему мнению).