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