
- •Конструкторы классов
- •Наследование
- •Модификаторы видимости
- •Перегрузка
- •Полиморфизм
- •Приемы программирования: наследование и полиморфизм
- •Конструктор по умолчанию
- •Вызов конструктора суперкласса
- •Приведение типов
- •Оператор instanceof
- •Анонимные и вложенные классы
- •Модификатор static
- •Модификатор final
- •Абстрактные классы
- •Множественное наследование
- •Описание интерфейса
- •Реализация интерфейса
- •Переменные интерфейсного типа
- •Приемы программирования: пример применения интерфейсов
- •Пакеты и области видимости Пакеты
- •Импортирование пакетов
- •Файловая структура Java-проекта
- •Области видимости классов
- •Области видимости членов класса
- •Области видимости переменных
- •Конфликты имен
- •Ход работы:
- •Задание №1
- •Задание №2
- •Задание №2
- •Задание № 3
- •Контрольные вопросы:
Перегрузка
В одном классе можно создать несколько методов с одним и тем же именем, различающихся по своим параметрам. Этот прием называется перегрузкой методов. Когда один из этих методов будет вызван, произойдет сопоставление переданных ему параметров (их количества и типов) с параметрами всех методов класса с таким именем. Если подходящий метод будет найден, выполнится именно он.
Например, в дополнение к конструктору, который уже есть в классе Dog, мы можем описать конструктор без параметров:
public Dog() {
name = "Незнакомец";
}
В этом конструкторе объекту класса Dog (очевидно, собака с неизвестной кличкой) присваивается при регистрации в программе имя «Незнакомец». Теперь мы можем воспользоваться одним из двух конструкторов:
Dog dog1 = new Dog("Тузик", 2); // Собака по кличке Тузик, возраст 2 года
Dog dog2 = new Dog(); // Собака по кличке «Незнакомец», возраст 0
Dog dog3 = new Dog(10); // Неверно! Не существует конструктора с такими параметрами
Нельзя создавать несколько одноименных методов с одинаковым числом и типом параметров.
Полиморфизм
Полиморфизм – это возможность класса выступать в программе в роли любого из своих предков, несмотря на то, что в нем может быть изменена реализация любого из методов.
Изменить работу любого из методов, унаследованных от класса-предка, класс-потомок может, описав новый метод с точно таким же именем и параметрами. Это называется переопределением. При вызове такого метода для объекта класса-потомка будет выполнена новая реализация.
Пусть, к примеру, мы хотим расширить наш класс Dog классом BigDog, для того, чтобы наша программа особым образом моделировала поведение больших злых собак. В частности, большие собаки лают по-другому. Во-первых, громче, а во-вторых, они не умеют считать. Поэтому мы переопределим метод voice():
class BigDog extends Dog {
public void voice() {
for (int i = 1; i <= 30; i++) {
System.out.print("ГАВ-");
}
}
}
Теперь создадим в методе main() двух разных собак: обычную и большую и заставим их лаять.
Dog dog = new Dog("Тузик", 2);
dog.voice();
BigDog bigdog = new BigDog();
bigdog.voice();
Объект подкласса всегда будет одновременно являться объектом любого из своих суперклассов. Поэтому в том же примере мы могли бы обойтись и одной переменной:
Dog dog = new Dog("Тузик", 2);
dog.voice();
dog = new BigDog();
dog.voice();
Т. е. переменная 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(); нарисует прямоугольник.
Мы рассмотрели очень подробный пример, поскольку описанный прием является одним из наиболее часто используемых средств в арсенале объектно-ориентированного программирования.