Эккель Брюс
Шрифт:
// polymorphism/PolyConstructors java // Конструкторы и полиморфизм дают не тот // результат, который можно было бы ожидать import static net mindview util Print *.
class Glyph {
void drawO { print("Glyph drawO"), } GlyphO {
printCGlyphO перед вызовом drawO");
drawO.
print ("GlyphO после вызова drawO").
class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r.
print("RoundGlyph RoundGlyph. radius = " + radius);
}
void drawO {
print ("RoundGlyph. drawO, radius = " + radius);
public class PolyConstructors {
public static void main(String[] args) { new RoundGlyph(5);
}
} /* Output-
GlyphO перед вызовом drawO RoundGlyph drawO, radius = 0 GlyphO после вызова drawO RoundGlyph RoundGlyphO, radius = 5 *///:-
Метод Glyph.draw изначально предназначен для переопределения в производных классах, что и происходит в RoundGlyph. Но конструктор Glyph вызывает этот метод, и в результате это приводит к вызову метода RoundGlyph.draw, что вроде бы и предполагалось. Однако из результатов работы программы видно — когда конструктор класса Glyph вызывает метод draw, переменной radius еще не присвоено даже значение по умолчанию 1. Переменная равна 0. В итоге класс может не выполнить свою задачу, а вам придется долго всматриваться в код программы, чтобы определить причину неверного результата.
Порядок инициализации, описанный в предыдущем разделе, немного неполон, и именно здесь кроется ключ к этой загадке. На самом деле процесс инициализации проходит следующим образом:
• Память, выделенная под новый объект, заполняется двоичными нулями.
• Конструкторы базовых классов вызываются в описанном ранее порядке. В этот момент вызывается переопределенный метод draw (да, перед вызовом конструктора класса RoundGlyph), где обнаруживается, что переменная radius равна нулю из-за первого этапа.
• Вызываются инициализаторы членов класса в порядке их определения.
• Исполняется тело конструктора производного класса.
У происходящего есть и положительная сторона — по крайней мере, данные инициализируются нулями (или тем, что понимается под нулевым значением для определенного типа данных), а не случайным «мусором» в памяти. Это относится и к ссылкам на объекты, внедренные в класс с помощью композиции. Они принимают особое значение null. Если вы забудете инициализировать такую ссылку, то получите исключение во время выполнения программы. Остальные данные заполняются нулями, а это обычно легко заметить по выходным данным программы.
С другой стороны, результат программы выглядит довольно жутко. Вроде бы все логично, а программ ведет себя загадочно и некорректно без малейших объяснений со стороны компилятора. (В языке С++ такие ситуации обрабатываются более рациональным способом.) Поиск подобных ошибок занимает много времени.
При написании конструктора руководствуйтесь следующим правилом: не пытайтесь сделать больше для того, чтобы привести объект в нужное состояние, и по возможности избегайте вызова каких-либо методов. Единственные методы, которые можно вызывать в конструкторе без опаски — неизменные (final) методы базового класса. (Сказанное относится и к закрытым (private) методам, поскольку они автоматически являются неизменными.) Такие методы невозможно переопределить, и поэтому они застрахованы от «сюрпризов».
Ковариантность возвращаемых типов
В Java SE5 появилась концепция ковариантности возвращаемых типов; этот термин означает, что переопределенный метод производного класса может вернуть тип, производный от типа, возвращаемого методом базового класса:
//: polymorph!sm/CovanantReturn java
class Grain {
public String toStringO { return "Grain"; }
}
class Wheat extends Grain {
public String toStringO { return "Wheat"; }
class Mill {
Grain process О { return new GrainO; }
}
class WheatMill extends Mill {
Wheat process О { return new WheatO; }
}
public class CovariantReturn {
public static void main(String[] args) { Mill m = new Mi 11; Grain g = m.processO; System out println(g); m = new WheatMi 110; g = m process О, System out.println(g);
}
} /* Output Grain Wheat */// ~
Главное отличие Java SE5 от предыдущих версий Java заключается в том, что старые версии заставляли переопределение process возвращать Grain вместо Wheat, хотя тип Wheat, производный от Grain, является допустимым возвращаемым типом. Ковариантность возвращаемых типов позволяет вернуть более специализированный тип Wheat.
Разработка с наследованием
После знакомства с полиморфизмом может показаться, что его следует применять везде и всегда. Однако злоупотребление полиморфизмом ухудшит архитектуру ваших приложений.
Лучше для начала использовать композицию, пока вы точно не уверены в том, какой именно механизм следует выбрать. Композиция не стесняет разработку рамками иерархии наследования. К тому же механизм композиции более гибок, так как он позволяет динамически выбирать тип (а следовательно, и поведение), тогда как наследование требует, чтобы точный тип был известен уже во время компиляции. Следующий пример демонстрирует это: