31

31


В примере с фигурами имеется базовый класс с именем Shape (фигура) и различные производные типы: Circle (окружность), Square (прямоугольник), Triangle (треугольник) и т. п. Выражения типа «окружность есть фигура» оче­видны и не представляют трудностей для понимания. Взаимосвязи показаны на следующей диаграмме наследования:












Восходящее преобразование имеет место даже в такой простой команде: Shape s = new CircleO;



Здесь создается объект Circle, и полученная ссылка немедленно присваивает­ся типу Shape. На первый взгляд это может показаться ошибкой (присвоение одного типа другому), но в действительности все правильно, потому что тип Circle (окружность) является типом Shape (фигура) посредством наследования. Компилятор принимает команду и не выдает сообщения об ошибке.



Предположим, вызывается один из методов базового класса (из тех, что были переопределены в производных классах):



s.drawO;



Опять можно подумать, что вызывается метод draw() из класса Shape, раз имеется ссылка на объект Shape — как компилятор может сделать что-то дру­гое? И все же будет вызван правильный метод Circle.draw(), так как в программе используется позднее связывание (полиморфизм).



Следующий пример показывает несколько другой подход:



//: polymorph!sm/shape/Shapes java package polymorphism.shape;



public class Shape {



public void drawO {} public void eraseO {} }Hi­ll'.polymorphism/shape/Circle java package polymorphism shape: import static net.mindview.util.Print.*,



public class Circle extends Shape {



public void drawO { printC'Circle.drawO"); } public void eraseO { printC'Circle.eraseO"). }} Hi­ll-.polymorphism/shape/Square.java package polymorphism.shape: import static net.mindview.util Print *.



public class Square extends Shape {



public void drawO { printC'Square.drawO"), }                                                       _
Лпродолжение &public void eraseO { printC'Square.eraseO"); } } ///.-



//• polymorphism/shape/Triangle java



package polymorphism.shape;



import static net mindview.util Print.*;



public class Triangle extends Shape {



public void drawO { printC'Triangle.drawO"). } public void eraseO { printC'Triangle eraseO"). } }Hi­ll.polymorphism/shape/RandomShapeGenerator java II "Фабрика", случайным образом создающая объекты package polymorphism.shape; import java.util *;



public class RandomShapeGenerator {



private Random rand = new Random(47); public Shape next О {



switch(rand nextlnt(3)) { default-



case 0: return new CircleO; case 1: return new SquareO, case 2: return new TriangleO;



}



}



}Hi­ll:polymorphism/Shapes.java II Polymorphism in Java, import polymorphism.shape.*;



public class Shapes {



private static RandomShapeGenerator gen =



new RandomShapeGeneratorO; public static void main(String[] args) { Shape[] s = new Shape[9]; II Заполняем массив фигурами: for(int i = 0, i < s.length; i++)



s[i] = gen nextO; II Полиморфные вызовы методов- for(Shape shp • s) shp.drawO,



}



} /* Output: Triangle.drawO Triangle.drawO Square drawO Triangle.drawO Square.drawO Triangle drawO Square drawO Triangle drawO Circle.drawO *///.-






Базовый класс Shape устанавливает общий интерфейс для всех классов, про­изводных от Shape — то есть любую фигуру можно нарисовать (draw()) и сте­реть (erase()). Производные классы переопределяют этот интерфейс, чтобы реа­лизовать уникальное поведение для каждой конкретной фигуры.



Класс RandomShapeGenerator — своего рода «фабрика», при каждом вызове метода next() производящая ссылку на случайно выбираемый объект Shape. За­метьте, что восходящее преобразование выполняется в командах return, каждая из которых получает ссылку на объект Circle, Square или Triangle, а выдает ее за пределы next() в виде возвращаемого типа Shape. Таким образом, при вызове этого метода вы не сможете определить конкретный тип объекта, поскольку всегда получаете просто Shape.



Метод main() содержит массив ссылок на Shape, который заполняется после­довательными вызовами RandomShapeGenerator.next(). К этому моменту вам из­вестно, что имеются объекты Shape, но вы не знаете об этих объектах ничего конкретного (так же, как и компилятор). Но если перебрать содержимое масси­ва и вызвать draw() для каждого его элемента, то, как по волшебству, произой­дет верное, свойственное для определенного типа действие — в этом нетрудно убедиться, взглянув на результат работы программы.



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



Расширяемость



Теперь вернемся к программе Music.java. Благодаря полиморфизму вы можете добавить в нее сколько угодно новых типов, не изменяя метод tune(). В хорошо спланированной ООП-программе большая часть ваших методов (или даже все методы) следуют модели метода tune(), оперируя только с интерфейсом базово­го класса. Такая программа являетсярасширяемой, поскольку в нее можно до­бавить дополнительную функциональность, определяя новые типы данных от общего базового класса. Методы, работающие на уровне интерфейса базово­го класса, совсем не нужно изменять, чтобы приспособить их к новым классам.



Давайте возьмем пример с объектами Instrument и включим дополнительные методы в базовый класс, а также определим несколько новых классов. Рассмот­рим диаграмму (см. рисунок на обороте).



Все новые классы правильно работают со старым, неизмененным методом tune(). Даже если метод tune() находится в другом файле, а к классу Instrument присоединяются новые методы, он все равно будет работать верно без повтор­ной компиляции. Ниже приведена реализация рассмотренной диаграммы:продолжение &//. polymorph"!sm/music3/Music3.java // Расширяемая программа package polymorphism music3; import polymorphism.music Note; import static net.mindview.util.Print *;



class Instrument {















void play(Note л) { print("Instrument playO " + n). }



String what О { return "Instrument". }



void adjustO { printC'Adjusting Instrument"). }



}



class Wind extends Instrument {



void play(Note n) { print ("Wind playO " + n), }



String whatO { return "Wind"; }



void adjustO { printC'Adjusting Wind"). }



}



class Percussion extends Instrument {



void play(Note n) { printC'Percussion.playO " + n). }



String whatO { return "Percussion"; }



void adjustO { printC'Adjusting Percussion"), }



}



class Stringed extends Instrument {



void play(Note n) { printC'Stringed playO " + n), }



String whatO { return "Stringed". }



void adjustO { printC'Adjusting Stringed"); }



}



class Brass extends Wind {



void play(Note n) { print("Brass.play() " + n); } void adjustO { printC'Adjusting Brass"); }



}



class Woodwind extends Wind {



void play(Note n) { print ("Woodwind playO " + n); } String whatO { return "Woodwind"; }






public class Music3 {



// Работа метода не зависит от фактического типа объекта, // поэтому типы, добавленные в систему, будут работать правильно public static void tune(Instrument i) { // ...



i.play(Note.MIDDLE_C),



}



public static void tuneAll(Instrument!!] e) { for(Instrument i : e) tune(i);



}



public static void main(String[] args) {



// Восходящее преобразование при добавлении в массив Instrument!!] orchestra = { new WindO. new PercussionO. new StringedO, new BrassO, new WoodwindО



}:



tuneAll(orchestra),



}



} /* Output. Wind.pi ayО MIDDLE_C Percussion.playO MIDDLE_C Stringed.pi ayО MIDDLE_C Brass.playO MIDDLE_C Woodwind pi ayО MIDDLE_C *///:-



Новый метод what() возвращает строку (String) с информацией о классе, а метод adjust() предназначен для настройки инструментов.



В методе main() сохранение любого объекта в массиве orchestra автоматиче­ски приводит к выполнению восходящего преобразования к типу Instrument.



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



Проблема: «переопределение» закрытых методов



Перед вами одна из ошибок, совершаемых по наивности:



//: polymorph!sm/PrivateOverride.java



// Попытка переопределения приватного метода



package polymorphism;



import static net.mindview.util.Print.*;



public class PrivateOverride {



private void f() { printCprivate f(D; } public static void main(String[] args) {



Pri vateOverride po = new DerivedO; po.fO:



}



class Derived extends PrivateOverride {



public void f() { print("public f()"). } } /* Output



private f() *///-



Вполне естественно было бы ожидать, что программа выведет сообщение public f(), но закрытый (private) метод автоматически является неизменным (final), а заодно и скрытым от производного класса. Так что метод f() класса Derived в нашем случае является полностью новым — он даже не был перегру­жен, так как метод f() базового класса классу Derived недоступен.



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



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



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



Порядок вызова конструкторов



Порядок вызова конструкторов коротко обсуждался в главах 5 и 7, но в то вре­мя мы еще не рассматривали полиморфизм.



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



Следующий пример показывает, как композиция, наследование и полимор­физм влияют на порядок конструирования:



// polymorphism/Sandwich.java



// Порядок вызова конструкторов.



package polymorphism,



import static net mindview.util.Print.*;



class Meal {



Meal О { printCMealO"). }



}



class Bread {



BreadO { printCBreadO"). }



}



class Cheese {



CheeseO { printC'CheeseO"). }



}



class Lettuce {



LettuceO { print("Lettuce()"); }



}



class Lunch extends Meal {



Lunch0 { printC'LunchO"). }



}



class PortableLunch extends Lunch {



PortableLunchO { printC'PortableLunchO");}



}



public class Sandwich extends PortableLunch { private Bread b = new BreadO, private Cheese с = new CheeseO, private Lettuce 1 = new LettuceO; public Sandwich0 { print("Sandwich()"); } public static void main(String[] args) { new SandwichO;



}



} /* Output: Meal О LunchO



PortableLunchO BreadO CheeseO LettuceO SandwichO *///:-



В этом примере создается сложный класс, собранный из других классов, и в каждом классе имеется конструктор, который сообщает о своем выполне­нии. Самый важный класс — Sandwich, с тремя уровнями наследования (четырьмя, если считать неявное наследование от класса Object) и тремя встроенными объ­ектами. Результат виден при создании объекта Sandwich в методе main(). Это значит, что конструкторы для сложного объекта вызываются в следующей по­следовательности: