Философия Java

Поведение полиморфных методов внутри конструкторов


Иерархия вызовов конструкторов принесла нам интересную дилемму. Что происходит, если Вы внутри конструктора вызовите динамически компонуемый метод существующего объекта? Внутри обычного метода Вы можете представить, что случится - динамически компонуемый метод разрешится во время работы программы, поскольку объект не знает какого типа данный объект или от какого типа он произошел. В силу последовательности, Вы можете думать, что тоже самое случится и внутри конструктора.

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

Понятно, что работа конструктора заключается в оживлении объектов (что на самом деле сродни подвигу). Внутри любого конструктора, целый объект может быть сформирован только по частям, Вы можете знать только то, что базовый объект был проинициализирован, но Вы не можете знать, какие классы наследованы от вашего класса. Динамически связываемые методы в произошедших от них классах. Если Вы сделаете такой фокус внутри конструктора, Вы вызовете метод, который может обрабатывать объекты, которые еще не были инициализированы. Хороший способ для создания катастрофы!

Вы можете разглядеть эту проблему в следующем примере:

//: c07:PolyConstructors.java

// Конструткоры и полиморфизм

// не производите то, что вы не можете ожидать.

abstract class Glyph { abstract void draw(); Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } }

class RoundGlyph extends Glyph { int radius = 1; RoundGlyph(int r) { radius = r; System.out.println( "RoundGlyph.RoundGlyph(), radius = "

+ radius); } void draw() { System.out.println( "RoundGlyph.draw(), radius = " + radius); } }

public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } ///:~


В Glyph, метод draw( )

- abstract, так что он спроектирован для переопределения. В замен этого Вы принудительного переопределяете его в RoundGlyph. Но конструктор Glyph вызывает этот метод и этот вызов заканчивается в RoundGlyph.draw( ), что в общем-то выглядит как то, что было нужно. Но посмотрите на вывод:

Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5

Когда конструктор Glyph-а вызывает draw( ), значение radius еще не приняло значение по умолчанию 1. Оно еще равно 0. Это означает, что не будет нарисована точка на экране, Вы будете пытаться нарисовать эту фигуру на экране и пытаться сообразить, почему программа не работает.

Порядок инициализации, описанный в предыдущей секции, не совсем полон и вот Вам ключ для разрешения этой загадки. Настоящий процесс инициализации:

  • Место отведенное под объекты инициализировано в ноль, до того, как что-то произойдет.
  • Вызывается конструктор базового класса (как и было описано ранее). В этот момент вызывается переопределенный метод draw( )(да, до того, как будет вызван конструткор RoundGlyph), который открывает, что значение radius равно нулю, как и было описано в шаге 1.
  • Инициализация элементов вызывается в порядке их определения.
  • Вызывается тело конструткора базового класса.


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

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

    В качестве результата, хорошие руководящие принципы для конструктора "Делайте в конструкторе настолько меньше, насколько можете и если это возможно, то не вызывайте никаких методов". Существует только один тип методов, которые безопасно вызывать из конструктора, это final методы из базового класса. (Это так же применимо и к private

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


    Содержание раздела