Сторона C ++ была обработана в других ответах, но я хочу добавить замечание на стороне Java этого. Вызов виртуальной функции из конструктора является проблемой во всех случаях, а не только в C ++. По сути, код пытается выполнить метод для объекта, который еще не был создан, и это ошибка.
Два решения, реализованные на разных языках, различаются в попытках понять, что пытается сделать ваш код. В C ++ решение заключается в том, что во время конструирования базового объекта и до тех пор, пока не начнется построение производного объекта, фактический тип объекта будет base , что означает, что не будет динамической диспетчеризации. То есть тип объекта в любой момент - это тип выполняемого конструктора [*] . Хотя это удивительно для некоторых (и вас, в том числе), оно дает разумное решение проблемы.
[*] И наоборот, деструктор . Тип также изменяется по мере завершения большинства производных конструкторов.
Альтернатива в Java состоит в том, что объект имеет конечный тип с самого начала, даже за до завершения построения . В Java, как вы продемонстрировали, вызов будет отправлен окончательному переопределению (здесь я использую сленг C ++: до последней реализации виртуальной функции в цепочке выполнения), и это может вызвать нежелательное поведение. Рассмотрим, например, эту реализацию initialize()
:
public class MyClass extends MyAbstractClass {
final int k1 = 1;
final int k2;
MyClass() {
k2 = 2;
}
void initialize() {
System.out.println( "Constant 1 is " + k1 + " and constant 2 is " + k2 );
}
}
Каков вывод предыдущей программы? (Ответ внизу)
Больше, чем просто игрушечный пример, учтите, что MyClass
предоставляет некоторые инварианты, которые устанавливаются во время построения и сохраняются в течение всего времени жизни объекта. Может быть, он содержит ссылку на регистратор, в который могут быть выгружены данные. Посмотрев на класс, вы увидите, что регистратор установлен в конструкторе, и предположите, что он не может быть сброшен нигде в коде:
public class MyClass extends MyAbstractClass {
Logger logger;
MyClass() {
logger = new Logger( System.out );
}
void initialize() {
logger.debug( "Starting initialization" );
}
}
Вы, наверное, теперь видите, куда это идет. Глядя на реализацию MyClass
, кажется, нет ничего плохого вообще. logger
устанавливается в конструкторе, поэтому его можно использовать во всех методах класса. Теперь проблема в том, что если MyAbstractClass
вызывает виртуальную функцию, которая будет отправлена, то приложение будет аварийно завершать работу с NullPointerException.
К настоящему времени я надеюсь, что вы понимаете и цените решение C ++ о том, что не следует выполнять динамическую диспетчеризацию, и, таким образом, избегаете выполнения функций над объектами, которые еще не были полностью инициализированы (или, наоборот, уже уничтожены, если виртуальный вызов находится в деструкторе). ).
( Ответ : это может зависеть от компилятора / JVM, но когда я попробовал это давным-давно, строка, напечатанная Константа 1, равна 1, а константа 2 - 0 . Если вы довольны этим, хорошо для меня, но я обнаружил, что это удивительно ... Причиной 1/0 в этом компиляторе является то, что процесс инициализации сначала устанавливает значения, которые находятся в определении переменной, а затем вызывает конструкторы. Это означает, что первый шаг построения установит k1
перед вызовом MyAbstractBase
конструктора, который вызовет initialize()
до запуска MyBase
конструктора и установить значение второй константы).