34
System out println("i = " + i + ". j = " + j), J = 39,
}
private static int xl =
printlnitC"Поле static Insect xl инициализировано"), static int printlnit(String s) { print(s). return 47.
public class Beetle extends Insect {
private int k = рппШЩ"Поле Beetle k инициализировано"), public BeetleO {
prtC'k = " + k), prtC'j = " + j).
}
private static int x2 =
printInit("Пoлe static Beetle x2 инициализировано"), public static void main(String[] args) { print("Конструктор Beetle"). Beetle b = new BeetleO;
}
} /*
Поле static Insect.xl инициализировано Поле static Beetle x2 инициализировано Конструктор Beetle i = 9. j = 0
Поле Beetle k инициализировано k = 47
j = 39 */// ~
Запуск класса Beetle в Java начинается с выполнения метода Beetle.main() (статического), поэтому загрузчик пытается найти скомпилированный код класса Beetle (он должен находиться в файле Beetle.class). При этом загрузчик обнаруживает, что у класса имеется базовый класс (о чем говорит ключевое слово extends), который затем и загружается. Это происходит независимо от того, собираетесь вы создавать объект базового класса или нет. (Чтобы убедиться в этом, попробуйте закомментировать создание объекта.)
Если у базового класса имеется свой базовый класс, этот второй базовый класс будет загружен в свою очередь, и т. д. Затем проводится static-инициализация корневого базового класса (в данном случае это Insect), затем следующего за ним производного класса, и т. д. Это важно, так как производный класс и инициализация его static-объектов могут зависеть от инициализации членов базового класса.
В этой точке все необходимые классы уже загружены, и можно переходить к созданию объекта класса. Сначала всем примитивам данного объекта присваиваются значения по умолчанию, а ссылкам на объекты задается значение null — это делается за один проход посредством обнуления памяти. Затем вызывается конструктор базового класса. В нашем случае вызов происходит автоматически, но вы можете явно указать в программе вызов конструктора базового класса (записав его в первой строке описания конструктора Beetle()) с помощью ключевого слова super. Конструирование базового класса выполняется по тем же правилам и в том же порядке, что и для производного класса. После завершения работы конструктора базового класса инициализируются переменные, в порядке их определения. Наконец, выполняется оставшееся тело конструктора.
Резюме
Как наследование, так и композиция позволяют создавать новые типы на основе уже существующих. Композиция обычно применяется для повторного использования реализации в новом типе, а наследование — для повторного использования интерфейса. Так как производный класс имеет интерфейс базового класса, к нему можно применить восходящее преобразование к базовому классу; это очень важно для работы полиморфизма (см. следующую главу).
Несмотря на особое внимание, уделяемое наследованию в ООП, при начальном проектировании обычно предпочтение отдается композиции, а к наследованию следует обращаться только там, где это абсолютно необходимо. Композиция обеспечивает несколько большую гибкость. Вдобавок, применяя хитрости наследования к встроенным типам, можно изменять точный тип и, соответственно, поведение этих встроенных объектов во время исполнения. Таким образом, появляется возможность изменения поведения составного объекта во время исполнения программы.
При проектировании системы вы стремитесь создать иерархию, в которой каждый класс имеет определенную цель, чтобы он не был ни излишне большим (не содержал слишком много функциональности, затрудняющей его повторное использование), ни раздражающе мал (так, что его нельзя использовать сам по себе, не добавив перед этим дополнительные возможности). Если архитектура становится слишком сложной, часто стоит внести в нее новые объекты, разбив существующие объекты на меньшие составные части.
Важно понимать, что проектирование программы является пошаговым, последовательным процессом, как и обучение человека. Оно основано на экспериментах; сколько бы вы ни анализировали и ни планировали, в начале работы над проектом у вас еще останутся неясности. Процесс пойдет более успешно — и вы быстрее добьетесь результатов, если начнете «выращивать» свой проект как живое, эволюционирующее существо, нежели «воздвигнете» его сразу, как небоскреб из стекла и металла. Наследование и композиция — два важнейших инструмента объектно-ориентированного программирования, которые помогут вам выполнять эксперименты такого рода.
Полиморфизм
Меня спрашивали: «Скажите, мистер Бэббидж, если заложить в машину неверные числа, на выходе она все равно выдаст правильный ответ?» Не представляю, какую же кашу надо иметь в голове, чтобы задавать подобные вопросы.Чарльз Бэббидж (1791-1871)
Полиморфизм является третьей неотъемлемой чертой объектно-ориентиро- ванного языка, вместе с абстракцией данных и наследованием.
Он предоставляет еще одну степень отделения интерфейса от реализации, разъединениячтооткак. Полиморфизм улучшает организацию кода и его читаемость, а также способствует созданиюрасширяемыхпрограмм, которые могут «расти» не только в процессе начальной разработки проекта, но и при добавлении новых возможностей.
Инкапсуляция создает новые типы данных за счет объединения характеристик и поведения. Сокрытие реализации отделяет интерфейс от реализации за счет изоляции технических подробностей в private-частях класса. Подобное механическое разделение понятно любому, кто имел опыт работы с процедурными языками. Но полиморфизм имеет дело с логическим разделением в контекстетипов. В предыдущей главе вы увидели, что наследование позволяет работать с объектом, используя как его собственный тип, так и его базовый тип. Этот факт очень важен, потому что он позволяет работать со многими типами (производными от одного базового типа) как с единым типом, что дает возможность единому коду работать с множеством разных типов единообразно. Вызов полиморфного метода позволяет одному типу выразить свое отличие от другого, сходного типа, хотя они и происходят от одного базового типа. Это отличие выражается различным действием методов, вызываемых через базовый класс.
В этой главе рассматривается полиморфизм (также называемыйдинамическим связыванием, илипоздним связыванием, илисвязыванием во время выполнения).Мы начнем с азов, а изложение материала будет поясняться простыми примерами, полностью акцентированными на полиморфном поведении программы.
Снова о восходящем преобразовании
Как было показано в главе 7, с объектом можно работать с использованием как его собственного типа, так и его базового типа. Интерпретация ссылки на объект как ссылки на базовый тип называетсявосходящим преобразованием.Также были представлены проблемы, возникающие при восходящем преобразовании и наглядно воплощенные в следующей программе с музыкальными инструментами. Поскольку мы будем проигрывать с их помощью объекты Note (нота), логично создать эти объекты в отдельном пакете:
II polymorphism/music/Musi с java
// Объекты Note для использования с Instrument
package polymorphism.music,
public enum Note {
MIDDLE_C. C_SHARP, B_FLAT, // И т.д } /// ~
Перечисления были представлены в главе 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.pi ay(Г);
}
}
III ~
//• polymorphism/music/Wind java package polymorphism.music;
// Объекты Wind также являются объектами Instrument, II поскольку имеют тот же интерфейс: public class Wind extends Instrument { // Переопределение метода интерфейса public void pi ay(Note n) {
System out pri ntl n( "Wind playO " + n),
}
} III-
II polymorphism/music/Music.java II Наследование и восходящее преобразование 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 WindO
tune(flute). // Восходящее преобразование
}
} /* Output
Wind playO MIDDLE_C
*/// -
Метод Music.tune() получает ссылку на Instrument, но последняя также может указывать на объект любого класса, производного от Instrument. В методе main() ссылка на объект Wind передается методу tune() без явных преобразований. Это нормально; интерфейс класса Instrument должен существовать и в классе Wind, поскольку последний был унаследован'от Instrument. Восходящее преобразование от Wind к Instrument способно «сузить» этот интерфейс, но не сделает его «меньше», чем полный интерфейс класса Instrument.
Потеря типа объекта
Программа Music.java выглядит немного странно. Зачем умышленноигнорироватьфактический тип объекта? Именно это мы наблюдаем при восходящем преобразовании, и казалось бы, программа стала яснее, если бы методу tune() передавалась ссылка на объект Wind. Но при этом мы сталкиваемся с очень важным обстоятельством: если поступить подобным образом, то потом придется писать новый метод tune() для каждого типа Instrument, присутствующего в системе. Предположим, что в систему были добавлены новые классы Stringed и Brass:
// polymorphi sm/musi c/Musi c2.java // Перегрузка вместо восходящего преобразования package polymorphism.music, import static net.mindview util Print *;
class Stringed extends Instrument {
public void play(Note n) {
pri nt ("Stri nged.pl ay() " + n):
}
}
class Brass extends Instrument {
public void play(Note n) {
printC'Brass playO " + 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 StnngedO.
Brass frenchHorn = new BrassO.
tune(flute), // Без восходящего преобразования
tune(violin);
tune(frenchHorn).
}
} /* Output
Wind playO MIDDLE_C
Stringed.pi ayО MIDDLE_C
Brass pi ayО MIDDLE_C *///-
Программа работает, но у нее есть огромный недостаток: для каждого нового Instrument приходится писать новый, зависящий от конкретного типа метод tune(). Объем программного кода увеличивается, а при добавлении нового метода (такого, как tune()) или нового типа инструмента придется выполнить немало дополнительной работы. А если учесть, что компилятор не выводит сообщений об ошибках, если вы забудете перегрузить один из ваших методов, весь процесс работы с типами станет совершенно неуправляемым.
Разве не лучше было бы написать единственный метод, в аргументе которого передается базовый класс, а не один из производных классов? Разве не удобнее было бы забыть о производных классах и написать обобщенный код для базового класса?
Именно это и позволяет делать полиморфизм. Однако большинство программистов с опытом работы на процедурных языках при работе с полиморфизмом испытывают некоторые затруднения.