Приемы программирования: наследование и полиморфизм

Главное преимущество полиморфизма – это возможность работать с объектами разных классов, происходящих от одного общего предка так, как будто бы они относились к одному классу.

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

Время от времени программа должна выполнять над этими фигурами какие-то действия. Например, когда окно программы сворачивается, а потом снова разворачивается, надо заново нарисовать все эти фигуры. Когда пользователь щелкает по фигуре мышкой, ее надо выделить, а когда пользователь перетаскивает границы фигуры – изменить ее размеры.

Придерживаясь методологии объектно-ориентированного программирования, формируется вывод, что каждая фигура должна рисовать себя «сама». То есть, команды для прорисовки круга выполняются в одном из методов класса Circle, например, в методе paint(). Действительно, все параметры фигуры должны храниться в полях ее класса, поэтому легко можно написать такой метод. Аналогично, фигура «сама» рисует себе выделение – для этого есть метод paintSelection() – и передвигается – метод move(int x, int y). Задача основной программы – просто обращаться к этим методам при необходимости.

Программа должна где-то хранить объекты, которые создаст пользователь. Поскольку заранее неизвестно, сколько будет этих объектов, необходимо воспользоваться какой-нибудь структурой для хранения множества объектов, например массивом. Но при создании массива требуется указать тип его элементов. А в нашей программе пользователь может создавать самые разные объекты. Так что придется завести несколько массивов: один для точек, один для кругов и так далее. Если понадобится заново нарисовать все объекты на экране, нужно будет перебрать все элементы в каждом из этих массивов:

for (int i = 0; i < points.length; i++) {

points[i].paint();

}

for (int i = 0; i < circles.length; i++) {

circles[i].paint();

}

и так далее, для каждого типа фигуры.

Более того, если пользователь щелкнул мышкой по экрану, чтобы выбрать фигуру, программа, получившая координаты мыши, должна найти фигуру, в которую попадают эти координаты. Предположим, каждая фигура сама может осуществить проверку с помощью метода checkPoint(int x, int y), который возвращает значение true, если точка с координатами x, y находится внутри этой фигуры. Но для того, чтобы вызвать этот метод, снова придется перебрать все массивы. И так для каждой операции, что очень неудобно.

Благодаря наследованию существует две прекрасные возможности. Для того, чтобы ими воспользоваться, нужно создать класс Figure и описать в нем методы, общие для всех фигур: paint(), checkPoint(int x, int y) и так далее. Не обязательно программировать эти методы, мы все равно не будем обращаться к ним. Важно, чтобы они были.

Первая возможность: можно присваивать объекты классов-потомков переменным любого из классов-предков.

Это вполне логично. Ведь если класс Кошка унаследован от класса Животное, то объект Мурзик является одновременно объектом класса Кошка и объектом класса Животное.

Следовательно, можно создать один большой массив для хранения объектов класса Figure:

Figure[] figures = new Figure[100]; // создаем массив для хранения 100 фигур

Теперь можно помещать в этот массив любые фигуры:

figures[0] = new Point(30, 30); // добавили в массив точку с координатами 30, 30

figures[1] = new Circle(60, 20, 10); // добавили круг с координатами 60, 20 радиуса 10

figures[2] = new Rectangle(0, 0, 30, 40); // добавили прямоугольник

Вторая возможность. Можно обращаться к методам, объявленным в классе-предке, но вызываться будет перегруженный метод, в зависимости от того, к какому классу на самом деле относится объект, к которому обращаются.

Можно нарисовать все фигуры, хранящиеся в массиве:

for (int i = 0; i < figures.length; i++) {

if (figures[i] != null) figures[i].paint();

}

В массиве хранятся элементы типа Figure. В этом классе есть метод paint(), поэтому вполне можно к нему обратиться. Но в самом классе Figure этот метод не делает ничего (ведь нельзя разработать процедуру рисования, подходящую для всех без исключения фигур). Зато в классе Point, унаследованном от класса Figure, переопределен этот метод – написан так, чтобы он рисовал точку (координаты точки хранятся в скрытых атрибутах класса Point). А в первом элементе массива figures[0] хранится именно точка. Хотя интерфейс обращается с ней как с просто фигурой, Java знает, что при вызове метода paint() нужно использовать именно тот вариант, который переопределен в классе Point. Аналогично команда figures[1].paint(); нарисует круг, а figures[2].paint();нарисует прямоугольник.