Выбрать главу

Разве не лучше было бы написать единственный метод, в аргументе которо­го передается базовый класс, а не один из производных классов? Разве не удоб­нее было бы забыть о производных классах и написать обобщенный код для ба­зового класса?

Именно это и позволяет делать полиморфизм. Однако большинство про­граммистов с опытом работы на процедурных языках при работе с полимор­физмом испытывают некоторые затруднения.

Особенности

Сложности с программой Music.java обнаруживаются после ее запуска. Она вы­водит строку Wind.play(). Именно это и требуется, но не понятно, откуда берется такой результат. Взгляните на метод tune():

public static void tune(Instrument i) {

//

i play(Note.MIDDLE_C),

}

Метод получает ссылку на объект Instrument. Как компилятор узнает, что ссылка на Instrument в данном случае указывает на объект Wind, а не на Brass или Stringed? Компилятор и не знает. Чтобы в полной мере разобраться в сути про­исходящего, необходимо рассмотреть понятие связывания (binding).

Связывание «метод-вызов»

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

Неоднозначность предыдущей программы кроется именно в раннем связы­вании: компилятор не может знать, какой метод нужно вызывать, когда у него есть только ссылка на объект Instrument

Проблема решается благодаря позднему связыванию (late binding), то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называют динамическим (dynamic) или связыванием на стадии выполнения (runtime binding). В языках, реализую­щих позднее связывание, должен существовать механизм определения факти­ческого типа объекта во время работы программы, для вызова подходящего ме­тода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова методов определяет его и вызывает соответствующее тело метода. Механизм позднего связывания зависит от конкретного языка, но нетрудно предполо­жить, что для его реализации в объекты должна включаться какая-то дополни­тельная информация.

Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как final (приватные методы являются final по умолчанию). Следовательно, вам не придется принимать решений относитель­но использования позднего связывания — оно осуществляется автоматически.

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

Получение нужного результата

Теперь, когда вы знаете, что связывание всех методов в Java осуществляется полиморфно, через позднее связывание, вы можете писать код для базового класса, не сомневаясь в том, что для всех производных классов он также будет работать верно. Другими словами, вы «посылаете сообщение объекту и позво­ляете ему решить, что следует делать дальше».

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

В примере с фигурами имеется базовый класс с именем 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­lclass="underline" 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()). Производные классы переопределяют этот интерфейс, чтобы реа­лизовать уникальное поведение для каждой конкретной фигуры.