Ответ 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
читается и печатается.