Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лр Основные понятия ООП.docx
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
41.6 Кб
Скачать

О впечатляющих возможностях полиморфизма

Т.е. переменная dog имеет тип Dog, но в третьей строке она начинает указывать на объект класса BigDog, то есть БОЛЬШУЮ собаку, которая при вызове метода voice() будет лаять как БОЛЬШАЯ собака. Это одна из впечатляющих возможностей объектно-ориентированного программирования.

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

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

Рассмотрим типичный пример.

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

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

Придерживаясь методологии объектно-ориентированного программирования, мы приходим к выводу, что каждая фигура должна рисовать себя «сама». То есть, команды для прорисовки круга выполняются в одном из методов класса 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(); нарисует прямоугольник.

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

Конструктор по умолчанию

Если в классе не описан ни один конструктор, для него автоматически создается конструктор по умолчанию. Этот конструктор не имеет параметров, все что он делает — это вызывает конструктор без параметров класса-предка.

Поэтому мы и смогли создать объект класса BigDog в примере с большой собакой, хотя не описывали в классе никаких конструкторов. Если вспомнить конструктор без параметров, который у нас есть в классе Dog, мы поймем, что переменная bigdog в предыдущем примере ссылалась на собаку по кличке "Незнакомец".

Вызов конструктора суперкласса

В примере с большой собакой нам удалось создать ее с помощью конструктора без параметров, т.е. не указывая ее имя и возраст. Оказывается, если бы мы попытались сделать это по-другому, у нас бы не получилось. Дело в том, что конструкторы не считаются членами класса и, в отличие от других методов, не наследуются.

BigDog bigdog = new BigDog("Полкан", 8); // Ошибка. Такого конструктора в классе нет

Для того, чтобы мы могли создавать больших собак с интересующими нас именем и возрастом, необходимо написать подходящий конструктор. При этом не обязательно повторять те команды, которые мы писали в конструкторе класса Dog (их всего две, но ведь могло быть гораздо больше). Вместо этого мы можем написать:

BigDog (String n, int a) {

super(n, a);

}

Ключевое слово super означает суперкласс (в нашем случае это класс Dog). В примере мы вызываем с его помощью конструктор суперкласса. При этом мы передаем два параметра — строку и число, — так что из всех конструкторов будет выбран именно тот, который нас интересует.

Вызов конструктора суперкласса должен происходить в самом начале конструктора.

Вместо вызова конструктора суперкласса можно вызвать один из конструкторов того же самого класса. Это делается с помощью ключевого слова this() — с параметрами в скобках, если они нужны.

Если в начале конструктора нет ни вызова this(), ни вызова super(), автоматически происходит обращение к конструктору суперкласса без аргументов.