Порядок инициализации конечных полей

Рассмотрим эти два класса:

public abstract class Bar {
    protected Bar() {
        System.out.println(getValue());
    }

    protected abstract int getValue();
}

public class Foo extends Bar {
    private final int i = 20;

    public Foo() {
    }

    @Override
    protected int getValue() {
        return i;
    }

    public static void main(String[] args) {
        new Foo();
    }
}

Если я выполнил Foo, выход будет равен 20.

Если я делаю поле не финальным, или если я инициализирую его в конструкторе Foo, вывод будет 0.

Мой вопрос: каков порядок инициализации в случае конечных полей и где это поведение описано в JLS?

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

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

Ответ 1

Ваша переменная-член final int i является постоянной переменной: 4.12.4. final Переменные

Переменная примитивного типа или типа String, то есть final и инициализированная выражением константы времени компиляции (§15.28), называется постоянной переменной.

Это имеет последствия для порядка, в котором вещи инициализируются, как описано в 12.4.2. Подробная процедура инициализации.

Ответ 2

Прогулка по тому, как это выглядит в стиле "байт-код-иш".

Вы уже должны знать, что первая фактическая инструкция в конструкторе должна быть вызовом super (с аргументами или без).

Эта супер команда возвращается, когда родительский конструктор завершен, и супер-объект полностью сконструирован. Поэтому, когда вы строите Foo, следующее происходит (по порядку):

// constant fields are initialized by this point
Object.construction // constructor call of Object, done by Bar
Bar.construction // aka: Foo.super()
callinterface getValue() // from Bar constructor
// this call is delegated to Foo, since that the actual type responsible
// and i is returned to be printed
Foo.construction

если вы должны были инициализировать его в конструкторе, это произойдет "сейчас", после того, как getValue() уже был вызван.

Ответ 3

Вот еще более подлая версия, просто для удовольствия.

public abstract class Bar {
    protected Bar() {
        System.out.println(getValue());
    }

    protected abstract Object getValue();
}

public class Foo extends Bar {
    private final String i = "Hello";

    public Foo() {
    }

    @Override
    protected Object getValue() {
        return i;
    }

    public static void main(String[] args) {
        new Foo();
    }
}

Результат: Печать Hello.

Теперь измените эту строку:

private final String i = "Hello";

в

private final Object i = "Hello";

Результат: Печать null.

Это связано с тем, что String (и примитивные типы) обрабатываются специально, как описано в JLS 4.12.4, о котором я упоминал в своем другом ответе.