33

33


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



При написании конструктора руководствуйтесь следующим правилом: не пытайтесь сделать больше для того, чтобы привести объект в нужное состоя­ние, и по возможности избегайте вызова каких-либо методов. Единственные методы, которые можно вызывать в конструкторе без опаски — неизменные (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.



Разработка с наследованием



После знакомства с полиморфизмом может показаться, что его следует приме­нять везде и всегда. Однако злоупотребление полиморфизмом ухудшит архи­тектуру ваших приложений.



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



// polymorphi sm/Transmogrify.java // Динамическое изменение поведения объекта // с помощью композиции (шаблон проектирования «Состояние») • import static net.mindview.util.Print.*;



class Actor {



public void act О {}



}



class HappyActor extends Actor {



public void actO { pri nt ("HappyActor"), }



class SadActor extends Actor {



public void act() { printCSadActor"). }



}



class Stage {



private Actor actor = new HappyActor(); public void changeO { actor = new SadActorO. } public void performPlayO { actor.act(), }



}



public class Transmogrify {



public static void main(String[] args) { Stage stage = new StageO; stage. performPlayO; stage. changeO; stage. performPlayO;



}



} /* Output-



HappyActor



SadActor



*///:-



Объект Stage содержит ссылку на объект Actor, которая инициализируется объектом HappyActor. Это значит, что метод performPlayO имеет определенное поведение. Но так как ссылку на объект можно заново присоединить к другому объекту во время выполнения программы, ссылке actor назначается объект SadActor, и после этого поведение метода performPlayO изменяется. Таким обра­зом значительно улучшается динамика поведения на стадии выполнения про­граммы. С другой стороны, переключиться на другой способ наследования во время работы программы невозможно; иерархия наследования раз и навсе­гда определяется в процессе компиляции программы.



Нисходящее преобразование и динамическое определение типов



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



Должен существовать какой-то механизм, гарантирующий правильность нисходящего преобразования; в противном случае вы можете случайно исполь­зовать неверный тип, послав ему сообщение, которое он не в состоянии при­нять. Это было бы небезопасно.



В некоторых языках (подобных С++) для проведения безопасного нисхо­дящего преобразования типов необходимо провести специальную операцию, но в Javaкаждое преобразованиеконтролируется! Поэтому, хотя внешне все вы­глядит как обычное приведение типов в круглых скобках, во время выполнения программы это преобразование проходит проверку на фактическое соответст­вие типу. Если типы не совпадают, происходит исключение ClassCastException. Процесс проверки типов во время выполнения программы называетсядинами­ческим определением типов(run-time type identification, RTTI). Следующий пример демонстрирует действие RTTI:



//: polymorphi sm/RTTI java



// Нисходящее преобразование и динамическое определение типов (RTTI)



// {ThrowException}



class Useful {



public void f() {} public void g() {}



}



class MoreUseful extends Useful { public void f() {} public void g() {} public void u() {} public void v() {} public void w() {}



}



public class RTTI {



public static void main(String[] args) { Useful[] x = {



new Useful О. new MoreUsefulО



}:



x[0].f(): x[l] g().



// СТадия компиляции- метод не найден в классе Useful• //! x[l].u().



((MoreUseful)х[1]) u(); // Нисх преобразование /RTTI ((MoreUseful)x[0]).u0; // Происходит исключение



}



} ///:-



Класс MoreUseful расширяет интерфейс класса Useful. Но благодаря наследо­ванию он также может быть преобразован к типу Useful. Вы видите, как это про­исходит, при инициализации массива х в методе main(). Так как оба объекта в массиве являются производными от Useful, вы можете послать сообщения (вызвать методы) f() и д() для обоих объектов, но при попытке вызова метода и() (который существует только в классе MoreUseful) вы получите сообщение об ошибке компиляции.



Чтобы получить доступ к расширенному интерфейсу объекта MoreUseful, ис­пользуйте нисходящее преобразование. Если тип указан правильно, все прой­дет успешно; иначе произойдет исключение ClassCastException. Вам не пона­добится писать дополнительный код для этого исключения, поскольку оно указывает на общую ошибку, которая может произойти в любом месте про­граммы.



Впрочем, RTTI не сводится к простой проверке преобразований. Например, можно узнать, с каким типом вы имеете дело,прежде чемпроводить нисходя­щее преобразование. Глава 11 полностью посвящена изучению различных ас­пектов динамического определения типов Java.



Резюме



Полиморфизм означает «многообразие форм». В объектно-ориентированном программировании базовый класс предоставляет общий интерфейс, а различ­ные версии динамически связываемых методов — разные формы использова­ния интерфейса.



Как было показано в этой главе, невозможно понять или создать примеры с использованием полиморфизма, не прибегнув к абстракции данных и насле­дованию. Полиморфизм — это возможность языка, которая не может рассмат­риваться изолированно; она работает только согласованно, как часть «общей картины» взаимоотношений классов.



Чтобы эффективно использовать полиморфизм — а значит, все объектно- ориентированные приемы — в своих программах, необходимо расширить свои представления о программировании, чтобы они охватывали не только члены и сообщения отдельного класса, но и общие аспекты классов, их взаимоотноше­ния. Хотя это потребует значительных усилий, результат стоит того. Наградой станет ускорение разработки программ, улучшение структуры кода, расширяе­мые программы и сокращение усилий по сопровождению кода.






Интерфейсы



Интерфейсы и абстрактные классы улучшают структуру кода и способствуют отделению интерфейса от реализации.



В традиционных языках программирования такие механизмы не получили особого распространения. Например, в С++ существует лишь косвенная под­держка этих концепций. Сам факт их существования в Java показывает, что эти концепции были сочтены достаточно важными для прямой поддержки в языке.



Мы начнем с понятияабстрактного класса, который представляет собой своего рода промежуточную ступень между обычным классом и интерфейсом. Абстрактные классы — важный и необходимый инструмент для создания клас­сов, содержащих нереализованные методы. Применение «чистых» интерфейсов возможно не всегда.



Абстрактные классы и методы



В примере с классами музыкальных инструментов из предыдущей главы мето­ды базового класса Instrument всегда оставались «фиктивными». Попытка вызо­ва такого метода означала, что в программе произошла какая-то ошибка. Это объяснялось тем, что класс Instrument создавался для определенияобщего ин­терфейсавсех классов, производных от него.



В этих примерах общий интерфейс создавался для единственной цели— его разной реализации в каждом производном типе. Интерфейс определяет базовую форму, общность всех производных классов. Такие классы, как Instrument, также называютабстрактными базовыми классамиили простоабстрактными классамиЕсли в программе определяется абстрактный класс вроде Instrument, созда­ние объектов такого класса практически всегда бессмысленно. Абстрактный класс создается для работы с набором классов через общий интерфейс. А если Instrument только выражает интерфейс, а создание объектов того класса не име­ет смысла, вероятно, пользователю лучше запретить создавать такие объекты. Конечно, можно заставить все методы Instrument выдавать ошибки, но в этом случае получение информации откладывается до стадии выполнения. Ошибки такого рода лучше обнаруживать во время компиляции.



В языке Java для решения подобных задач применяютсяабстрактные мето­ды
1
.
Абстрактный метод незавершен; он состоит только из объявления и не имеет тела. Синтаксис объявления абстрактных методов выглядит так:



abstract void f();



Класс, содержащий абстрактные методы, называетсяабстрактным классом.Такие классы тоже должны помечаться ключевым словом abstract (в противном случае компилятор выдает сообщение об ошибке).



Если вы объявляете класс, производный от абстрактного класса, но хотите иметь возможность создания объектов нового типа, вам придется предоставить определения для всех абстрактных методов базового класса. Если этого не сде­лать, производный класс тоже останется абстрактным, и компилятор заставит пометитьновыйкласс ключевым словом abstract.



Можно создавать класс с ключевым словом abstract даже тогда, когда в нем не имеется ни одного абстрактного метода. Это бывает полезно в ситуациях, где в классе абстрактные методы просто не нужны, но необходимо запретить созда­ние экземпляров этого класса.



Класс Instrument очень легко можно сделать абстрактным. Только некоторые из его методов станут абстрактными, поскольку объявление класса как abstract не подразумевает, что все его методы должны быть абстрактными. Вот что по­лучится:












А вот как выглядит реализация примера оркестра с использованием абст­рактных классов и методов:



//. interfaces/music4/Musiс4 java // Абстрактные классы и методы package interfaces.music4; import polymorphism.music.Note, import static net mindview util Print *.



abstract class Instrument {



private int i; // Память выделяется для каждого объекта public abstract void play(Note n); public String whatO { return "Instrument"; } public abstract void adjustO,



}



class Wind extends Instrument { public void play(Note n) {



print("Wind playО " + n),