Содержание
· 1 ПОЛИМОРФИЗМ
o 1.1 Снова о восходящем преобразовании
o 1.2 Потеря типа объекта
o 1.3 Связывание «метод-вызов»
o 1.4 Получение нужного результата
o 1.5 Расширяемость
o 1.6 Проблема: «переопределение» закрытых методов
o 1.7 Конструкторы и полиморфизм
§ 1.7.1 Порядок вызова конструкторов
§ 1.7.2 Наследование и завершающие действия
§ 1.7.3 Поведение полиморфных методов при вызове из конструкторов
o 1.8 Ковариантность возвращаемых типов
o 1.9 Разработка с наследованием
o 1.10 Нисходящее преобразование и динамическое определение типов
o 1.11 Резюме
Полиморфизм является третьей неотъемлемой чертой объектно-ориентированного языка, вместе с абстракцией данных и наследованием.
Он предоставляет еще одну степень отделения интерфейса от реализации, разъединения что от как. Полиморфизм улучшает организацию кода и его читаемость, а также способствует созданию расширяемых программ, которые могут «расти» не только в процессе начальной разработки проекта, но и при добавлении новых возможностей.
Инкапсуляция создает новые типы данных за счет объединения характеристик и поведения. Сокрытие реализации отделяет интерфейс от реализации за счет изоляции технических подробностей вprivate-частях класса. Подобное механическое разделение понятно любому, кто имел опыт работы с процедурными языками. Но полиморфизм имеет дело с логическим разделением в контексте типов. В предыдущей главе вы увидели, что наследование позволяет работать с объектом, используя как его собственный тип, так и его базовый тип.
Этот факт очень важен, потому что он позволяет работать со многими типами (производными от одного базового типа) как с единым типом, что дает возможность единому коду работать с множеством разных типов единообразно. Вызов полиморфного метода позволяет одному типу выразить свое отличие от другого, сходного типа, хотя они и происходят от одного базового типа. Это отличие выражается различным действием методов, вызываемых через базовый класс.
В этой главе рассматривается полиморфизм (также называемый динамическим связыванием, или поздним связыванием, или связыванием во время выполнения). Мы начнем с азов, а изложение материала будет поясняться простыми примерами, полностью акцентированными на полиморфном поведении программы.
Снова о восходящем преобразовании
Как было показано в главе 7, с объектом можно работать с использованием как его собственного типа, так и его базового типа. Интерпретация ссылки на объект как ссылки на базовый тип называется восходящим преобразованием.
Также были представлены проблемы, возникающие при восходящем преобразовании и наглядно воплощенные в следующей программе с музыкальными инструментами. Поскольку мы будем проигрывать с их помощью объекты Note (нота), логично создать эти объекты в отдельном пакете:
//: polymorphism/music/Note.java
// Ноты для игры на музыкальных инструментах.
package polymorphism.music;
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
Перечисления были представлены в главе 5. В следующем примере Wind является частным случаем инструмента (Instrument), поэтому класс Wind наследует от Instrument:
//: polymorphism/music/Instrument.java
package polymorphism.music;
import static net.mindview.util.Print.*;
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
//: polymorphism/music/Wind.java
package polymorphism.music;
// Объекты Wind также являются объектами Instrument
// поскольку имеют тот же интерфейс:
public class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
//: polymorphism/music/Music.java
// Наследование и восходящее преобразование
package polymorphism.music;
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Восходящее преобразование
}
}
<spoiler text="Output:">
Wind.play() MIDDLE_C
</spoiler> Метод Music.tune() получает ссылку на Instrument, но последняя также может указывать на объект любого класса, производного от Instrument. В методе main() ссылка на объект Wind передается методу tune() без явных преобразований. Это нормально; интерфейс класса Instrument должен существовать и в классе Wind, поскольку последний был унаследован от Instrument. Восходящее преобразование от Wind к Instrument способно «сузить» этот интерфейс, но не сделает его «меньше», чем полный интерфейс класса Instrument.
Потеря типа объекта
Программа Music.java выглядит немного странно. Зачем умышленно игнорировать фактический тип объекта? Именно это мы наблюдаем при восходящем преобразовании, и казалось бы, программа стала яснее, если бы методу tune() передавалась ссылка на объект Wind. Но при этом мы сталкиваемся с очень важным обстоятельством: если поступить подобным образом, то потом придется писать новый метод tune() для каждого типа Instrument, присутствующего в системе. Предположим, что в систему были добавлены новые классы Stringed иBrass:
//: polymorphism/music/Music2.java
// Перегрузка вместо восходящего преобразования
package polymorphism.music;
import static net.mindview.util.Print.*;
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
class Brass extends Instrument {
public void play(Note n) {
print("Brass.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // Без восходящего преобразования
tune(violin);
tune(frenchHorn);
}
}
<spoiler text="Output:">
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
</spoiler> Программа работает, но у нее есть огромный недостаток: для каждого нового Instrument приходится писать новый, зависящий от конкретного типа метод tune(). Объем программного кода увеличивается, а при добавлении нового метода (такого, как tune()) или нового типа инструмента придется выполнить немало дополнительной работы. А если учесть, что компилятор не выводит сообщений об ошибках, если вы забудете перегрузить один из ваших методов, весь процесс работы с типами станет совершенно неуправляемым.
Разве не лучше было бы написать единственный метод, в аргументе которого передается базовый класс, а не один из производных классов? Разве не удобнее было бы забыть о производных классах и написать обобщенный код для базового класса?
Именно это и позволяет делать полиморфизм. Однако большинство программистов с опытом работы на процедурных языках при работе с полиморфизмом испытывают некоторые затруднения.