Виртуальные таблицы и аннотация в Java - PullRequest
25 голосов
/ 04 марта 2012

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

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

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

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

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

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

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

Derived: 0
Derived: 2

Почему?

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

Ответы [ 2 ]

23 голосов
/ 04 марта 2012

Это порядок выполнения кода.Более подробная информация приведена ниже.

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

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

Field Shadowing

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
}

Виртуальные вызовы

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

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

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

10 голосов
/ 04 марта 2012

Очевидно, вызывается только 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

Второе тривиально и ожидаемо [по моему мнению, по крайней мере].

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...