Состояние объекта производного класса, когда конструктор базового класса вызывает переопределенный метод в 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 происходит, потому что производный объект наполовину инициализирован; похоже на то, что здесь говорит Джон Скит

Мои вопросы:

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

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

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

4 ответа

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. Экземпляр подкласса создан
  2. Все члены данных инициализируются со значениями по умолчанию
  3. Вызываемый конструктор немедленно передает управление соответствующему конструктору суперкласса.
  4. Супер-конструктор инициализирует некоторые / все свои собственные элементы данных, а затем вызывает виртуальный метод.
  5. Метод переопределяется подклассом, поэтому вызывается реализация подкласса.
  6. Метод пытается использовать члены данных подкласса, предполагая, что они уже инициализированы, но это не так - стек вызовов еще не вернулся в конструктор подкласса.

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

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

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

Другие вопросы по тегам