Ответ 1
Во-первых, вообще нет проблем с вызовами методов в конструкторе. Проблемы конкретно связаны с конкретными случаями вызова переопределяемых методов класса конструктора и передачи объекта this
ссылки на методы (включая конструкторы) других объектов.
Причины для избежания переопределяемых методов и "утечки this
" могут быть сложными, но в основном они направлены на предотвращение использования не полностью инициализированных объектов.
Избегайте вызывать переопределяемые методы
Причины отказа от вызова переопределяемых методов в конструкторах являются следствием процесса создания экземпляра, определенного в §12.5 Спецификации языка Java (JLS).
Кроме того, процесс § 12.5 гарантирует, что при создании экземпляра производного класса [1] инициализация его базового класса (т.е. установка его членов в их начальные значения и выполнение его конструктора ) происходит до его собственной инициализации. Это позволяет обеспечить последовательную инициализацию классов с помощью двух ключевых принципов:
- Инициализация каждого класса может сосредоточиться на инициализации только тех членов, которые он явно декларирует сам, безопасно в понимании того, что все остальные члены, унаследованные от базового класса, уже инициализированы.
- Инициализация каждого класса может безопасно использовать члены его базового класса в качестве исходных данных для инициализации его собственных членов, так как гарантировано, что они были правильно инициализированы к моменту начала инициализации класса.
Однако есть улов: Java допускает динамическую отправку в конструкторах [2]. Это означает, что если конструктор базового класса, выполняющий роль экземпляра производного класса, вызывает метод, который существует в производном классе, он вызывается в контексте этого производного класса.
Прямым следствием всего этого является то, что при создании экземпляра производного класса конструктор базового класса вызывается до инициализации производного класса. Если этот конструктор вызывает вызов метода, который переопределяется производным классом, это метод производного класса (а не метод базового класса), который называется , хотя производный класс еще не инициализирован. Очевидно, это проблема, если этот метод использует любые члены производного класса, так как они еще не были инициализированы.
Очевидно, что проблема возникает из-за метода вызова конструктора базового класса, который может быть переопределен производным классом. Чтобы предотвратить проблему, конструкторы должны вызывать только методы своего собственного класса, которые являются окончательными, статическими или частными, поскольку эти методы не могут быть переопределены производными классами. Конструкторы конечных классов могут вызывать любой из своих методов, поскольку (по определению) они не могут быть получены из.
Пример 12.5-2 JLS - хорошая демонстрация этой проблемы:
class Super {
Super() { printThree(); }
void printThree() { System.out.println("three"); }
}
class Test extends Super {
int three = (int)Math.PI; // That is, 3
void printThree() { System.out.println(three); }
public static void main(String[] args) {
Test t = new Test();
t.printThree();
}
}
Эта программа печатает 0
, затем 3
. Последовательность событий в этом примере выглядит следующим образом:
-
new Test()
вызывается в методеmain()
. - Так как
Test
не имеет явного конструктора, вызывается конструктор по умолчанию его суперкласса (а именноSuper()
). - Конструктор
Super()
вызываетprintThree()
. Это отправляется в переопределенную версию метода в классеTest
. - Метод
printThree()
классаTest
печатает текущее значение переменной-членаthree
, которое является значением по умолчанию0
(поскольку экземплярTest
еще не инициализирован). - Метод
printThree()
иSuper()
конструктор каждого выхода, и экземплярTest
инициализируется (в этот моментthree
устанавливается значение3
). - Метод
main()
снова вызываетprintThree()
, который на этот раз печатает ожидаемое значение3
(поскольку экземплярTest
теперь инициализирован).
Как описано выше, в §12.5 говорится, что (2) должно произойти до (5), чтобы гарантировать, что Super
инициализируется до Test
. Однако динамическая отправка означает, что вызов метода в (3) выполняется в контексте неинициализированного класса Test
, что приводит к неожиданному поведению.
Избегать утечки this
Ограничение на передачу this
от конструктора к другому объекту немного легче объяснить.
В принципе, объект не может считаться полностью инициализированным до тех пор, пока его конструктор не завершит выполнение (поскольку его целью является завершение инициализации объекта). Итак, если конструктор передает объект this
другому объекту, этот другой объект затем ссылается на объект, даже если он не был полностью инициализирован (поскольку его конструктор все еще запущен). Если другой объект затем пытается получить доступ к неинициализированному элементу или вызвать метод исходного объекта, который полагается на полностью инициализированный, может возникнуть неожиданное поведение.
Пример того, как это может привести к неожиданному поведению, см. в этой статье.
<ч/" > [1] Технически каждый класс в Java, кроме Object
, является производным классом. Я просто использую термины "производный класс" и "базовый класс" здесь, чтобы описать взаимосвязь между конкретными классами.
[2] В JLS нет (по моим сведениям) причин, почему это так. Альтернатива - запрещая динамической диспетчеризации в конструкторах. - бы весь вопрос спорный, который, вероятно, почему именно С++ не позволяет