Состояние объекта класса Derived, когда конструктор базового класса вызывает переопределенный метод в Java

Обратитесь к коду Java ниже:

class Base{
     Base(){
         System.out.println("Base Constructor");
         method();
     }
     void method(){}    
}

class Derived extends Base{
    int var = 2;
    Derived(){
         System.out.println("Derived Constructor");  
    }

     @Override
     void method(){
        System.out.println("var = "+var);
     }
 }

class Test2{
    public static void main(String[] args) {
        Derived b = new Derived();
    }
}

Выводимый результат:

Base Constructor
var = 0
Derived Constructor

Я думаю, что var = 0 возникает, потому что исходный объект имеет половину инициализации; похоже на то, что Джон Скит говорит здесь

Мои вопросы:

Почему метод overridden вызывается, если объект класса Derived еще не создан?

В какой момент времени задано значение var 0?

Существуют ли случаи, когда такое поведение желательно?

Ответ 1

  • Объект Derived был создан - это просто, что конструктор еще не запущен. Тип объекта никогда не изменяется в Java после момента его создания, что происходит до того, как все конструкторы выполняются.

  • var присваивается значение по умолчанию 0 как часть процесса создания объекта перед запуском конструкторов. В принципе, ссылка на тип устанавливается, а остальная часть памяти, представляющая объект, обрезается до нуля (концептуально, во всяком случае, возможно, она уже была обрезана до нуля, как часть сбора мусора)

  • Такое поведение, по крайней мере, приводит к последовательности, но это может быть болью. В терминах согласованности предположим, что у вас есть подклассы только для чтения изменяемого базового класса. Базовый класс может иметь свойство isMutable(), которое по умолчанию было по умолчанию равно true, но подкласс перенаправил его, чтобы всегда возвращать false. Было бы странно, если бы объект был изменен до запуска конструктора подкласса, но неизменяемый впоследствии. С другой стороны, это определенно странно в ситуациях, когда вы заканчиваете запуск кода в классе до того, как конструктор для этого класса запустил: (

Несколько рекомендаций:

  • Попытайтесь не много работать в конструкторе. Один из способов избежать этого - выполнить работу в статическом методе, а затем сделать заключительную часть статического метода вызовом конструктора, который просто устанавливает поля. Конечно, это означает, что вы не получите преимуществ полиморфизма, пока выполняете эту работу, но при этом в вызове конструктора все равно будет опасно.

  • Постарайтесь, чтобы избежать вызовов не конечных методов во время конструктора - это очень вероятно приведет к путанице. Документируйте любые вызовы методов, которые вам действительно нужно сделать очень четко, так что кто-то, переопределяющий их, знает, что они будут вызваны до завершения инициализации.

  • Если вам нужно вызвать метод во время построения, обычно его не следует потом вызывать. Если это дело, запишите его и попытайтесь указать его в названии.

  • Попытайтесь не злоупотреблять наследованием в первую очередь - это только станет проблемой, если у вас есть подкласс, полученный из суперкласса, отличного от Object:). Проектирование для наследования сложнее.

Ответ 2

Почему получается переопределенный метод вызывается, если объект класса Derived еще не создан?

Derived Конструктор классов неявно вызывает конструктор класса Base в качестве первого оператора. Base вызывает конструктор классов method(), который вызывает переопределенную реализацию в классе Derived, потому что это класс, объект которого создается. method() в классе Derived видит var как 0 в этой точке.

В какой момент времени назначается var значение 0?

var присваивается значение по умолчанию для int типа i.e. 0 перед вызовом конструктора класса Derived. Ему присваивается значение 2 после, завершение неявного надстрочного суперкласса завершено, а до выполняются инструкции в конструкторе класса Derived.

Существуют ли какие-либо варианты использования, когда такие поведение желательно?

Как правило, плохая идея использовать методы non final non private в конструкторах/инициализаторах класса non final. Причины очевидны в вашем коде. Если создаваемый объект является экземпляром подкласса, методы могут давать неожиданные результаты.

Ответ 3

Обратите внимание, что это отличается от С++, когда тип изменяется во время создания объекта, поэтому вызов виртуального метода из конструкторов базового класса не вызывает переопределение производного класса. То же самое происходит во время разрушения. Таким образом, это может быть небольшая ловушка для программистов на С++, приходящих на Java.

Ответ 4

Есть некоторые свойства спецификации языка Java, которые следует отметить, чтобы объяснить это поведение:

  • Конструктор суперкласса всегда неявно/явно вызывается перед конструктором подкласса.
  • Вызов метода из конструктора аналогичен любому вызову метода; если этот метод не является окончательным, то вызов является виртуальным вызовом, что означает, что реализация метода для вызова является той, которая связана с типом среды выполнения объекта.
  • До выполнения конструктора все члены данных автоматически инициализируются значениями по умолчанию (0 для числовых примитивов, null для объектов, false для булевых).

Последовательность событий следующая:

  • Создан экземпляр подкласса
  • Все элементы данных инициализируются значениями по умолчанию
  • Вызываемый конструктор немедленно передает управление соответствующему конструктору суперкласса.
  • Супер-конструктор инициализирует некоторые/все свои собственные элементы данных, а затем вызывает виртуальный метод.
  • Метод переопределяется подклассом, поэтому выполняется реализация подкласса.
  • Метод пытается использовать элементы данных подкласса, если они уже инициализированы, но это не так - стек вызовов еще не вернулся к конструктору подкласса.

Короче говоря, всякий раз, когда конструктор суперкласса вызывает не конечный метод, у нас есть потенциальный риск проникновения в эту ловушку, поэтому делать это не рекомендуется. Обратите внимание, что нет элегантного решения, если вы настаиваете на этом шаблоне. Вот два сложных и креативных, требующих синхронизации потоков (!):

http://www.javaspecialists.eu/archive/Issue086.html

http://www.javaspecialists.eu/archive/Issue086b.html