Лекция
Привет, сегодня поговорим про понятия ооп, обещаю рассказать все что знаю. Для того чтобы лучше понимать что такое понятия ооп, класс объект абстрагирование инкапсуляция полиморфизм наследование , настоятельно рекомендую прочитать все из категории ООП и практические JAVA.
Внимание! На данном этапе обучения вы уже должны владеть знаниями по этой теме. Если их нет, а материалы для повторения непонятны или недостаточны, вы с заданиями не справитесь! Необходимо срочно обратиться к литературе по данной теме.
1. Гради Буч. Объектно-ориентированный анализ и проектирование.
Java — полностью объектно-ориентированный язык, поэтому, как мы уже отмечали, все действия, выполняемые программой, находятся в методах тех или иных классов.
Описание класса начинается с ключевого слова class, после которого указывается идентификатор — имя класса. Затем в фигурных скобках перечисляются атрибуты и методы класса. Атрибуты в языке Java называются полями (в дальнейшем мы будем использовать это наименование). Поля и методы называются членами класса.
Поля описываются как обычные переменные.
Правила записи методов рассматривались на предыдущем занятии.
Опишем для примера класс Dog
(собака). У него будет два поля: кличка и возраст. При описании поведения собаки в этом простом примере ограничимся лаем. Конечно, лаять по-настоящему наша собака не будет (ведь это всего лишь программная конструкция), она будет выводить в консоль «гав-гав». Чтобы было интереснее, предположим, что все собаки, с которыми имеет дело наша программа, умны настолько, что когда их вынуждают лаять, они говорят «гав-гав» столько раз, сколько им лет.
Заметим, что в программе уже есть один класс (тот, в котором описан метод main()
). Поскольку этот класс к собакам отношения не имеет, описывать новый класс Dog
следует за его пределами.
class Dog {
int age; // возраст
String name; // кличка
public void voice() {
for (int i = 1; i <= age; i++) {
System.out.println("гав-гав");
}
}
}
Самое главное — понять, что означает, когда некоторые переменные (поля) и функции (методы) собраны (описаны) в каком-то классе.
Класс должен описывать некоторое законченное понятие. Это может быть понятие из предметной области программы (собака, велосипед, аквариум, сессия) или понятие, необходимое для работы самой программы (очередь, список, строка, окно, кнопка, программа*).
Полями класса должны быть данные, относящиеся к этому понятию. Для собаки это возраст, кличка, порода и т.д., а для сессии — дата начала, продолжительность и т.д.
Методы класса, как правило, работают с данными этого класса. Например, метод voice()
в нашем примере обращается к полю age
(возраст).
Когда какой-то класс описан, могут создаваться объекты этого класса и с ними можно работать, вызывая их методы (кормить собаку, выгуливать, просить ее лаять — словом делать все то, что позволяет поведение класса, т.е. совокупность его методов).
Для обращения к объектам удобно использовать переменные, имеющие тип класса. Например, для работы с собаками опишем переменную типа Dog
:
Dog x;
Переменная типа класса является ссылочной переменной, она не хранит данные (как переменные простых типов int,char и т.д.), а указывает на место в памяти, где эти данные хранятся (как переменные типа массива). Данными, на которые указывает только что описанная переменная x
, может быть объект класса Dog
. Его необходимо предварительно создать командой new:
x = new Dog();
Теперь переменная x
указывает на некий объект класса Dog
, хранящий в памяти свои данные (возраст и кличку). Кроме того, эту собаку можно заставить лаять, вызвав соответствующий метод командой:
x.voice();
Для того, чтобы обратиться к члену класса, необходимо указать его имя после имени объекта через точку.
Обратите внимание, «залаяла» именно та собака, на которую «указывала» переменная x
. Если в программе были созданы другие собаки, они будут молчать до тех пор, пока не будет вызван их метод voice()
.
Таким образом, когда данные (поля) и команды (методы) описываются в одном классе, они оказываются тесно связаны друг с другом в объектах этого класса. Метод вызывается не сам по себе, а для конкретного объекта и работает с полями именно этого объекта.
Поэтому команды
voice();
age += 1;
не имеют никакого смысла, если употребляются вне методов класса Dog
. Обязательно указание на конкретный объект, с которым производится действие. Внутри метода указание на конкретный объект вовсе не обязательно: в рассмотренном примере запись
for (int i = 1; i <= age; i++)
означает, что для определения «продолжительности» лая будет проверяться возраст того самого объекта, для которого будет вызван данный метод. Этот объект обозначается ключевым словом this.
Метод voice()
можно было описать и так:
public void voice() {
for (int i = 1; i <= this.age; i++) {
System.out.println("гав-гав");
}
}
Ключевое слово this в этом примере наглядно указывает, что используется атрибут age
именно того объекта классаDog
, для которого вызывается метод voice()
.
Когда имя метода или атрибута записывается без указания объекта, всегда подразумевается объект this.
Конструктор — это особенный метод класса, который вызывается автоматически в момент создания объектов этого класса. Имя конструктора совпадает с именем класса.
Например, в классе Dog
может быть конструктор с двумя параметрами, который при создании новой собаки позволяет сразу задать ее кличку и возраст.
public Dog(String n, int a) {
name = n;
age = a;
}
Конструктор вызывается после ключевого слова new в момент создания объекта. Теперь, когда у нас есть такой конструктор, мы можем им воспользоваться:
Dog dog1 = new Dog("Тузик", 2);
В результате переменная dog1
будет указывать на «собаку» по кличке Тузик, имеющую возраст 2 года. Кстати, этот возраст можно узнать, заставив собаку лаять командой
dog1.voice();*
Конструкторы добавляются в класс, если в момент создания объекта нужно выполнить какие-то действия (начальную настройку) с его данными (полями). Сразу задать кличку и возраст собаки более естественно, чем каждый раз порождать безымянного щенка, а затем давать ему имя и быстро выращивать до нужного возраста (с учетом того, что программа скорее всего обрабатывает данные о собаках, которые на самом деле уже давно родились). Хотя до появления ООП программисты часто делали именно так.
Наследование — это отношение между классами, при котором один класс расширяет функциональность другого. Это значит, что он автоматически перенимает все его поля и методы, а также добавляет некоторые свои.
Наследование обычно возникает, когда все объекты одного класса одновременно являются объектами другого класса (отношение общее/частное). Например, все объекты класса Студент
являются объектами класса Человек
. В этом случае говорят, что класс Студент
наследует от класса Человек
. Аналогично класс Собака
может наследовать от классаЖивотное
, а класс Далматинец
от класса Собака
. Класс, который наследует, называется подклассом или потомком, а класс, от которого наследуют, называется суперклассом или предком.
Заметим, что если класс №2 является потомком класса №1, а класс №3 является потомком класса №2, то класс №3 является также потомком класса №1.
Наследование избавляет программиста от лишней работы. Например, если в программе необходимо ввести новый классДалматинец
, его можно создать на основе уже существующего класса Собака
, не программируя заново все поля и методы, а лишь добавив те, которых не хватало в суперклассе.
Для того, чтобы один класс был потомком другого, необходимо при его объявлении после имени класса указать ключевое слово extends и название суперкласса.
Например:
class Dalmatian extends Dog {
// дополнительные поля и методы
...
}
Если ключевое слово extends не указано, считается, что класс унаследован от универсального класса Object.
С класса Object
начинается иерархия наследования. Любой другой класс является потомком класса Object
и наследует от него три метода:
equals(Object obj)
— позволяет сравнивать два объекта. Возвращает true, если объекты равны. Например, мы сравнивали строки (объекты класса String
):
if (s1.equals(s2)) { ... }
toString()
— «переводит» объект в строку (которую можно вывести в методе println()
).
hashCode()
— возвращает целое число, уникальное для всех объектов данного класса (не может быть двух объектов, для которых это число совпадает).
При наследовании эти методы чаще всего необходимо переопределить, чтобы они работали так, как надо программисту. Подробнее о переопределении см. далее.
Доступ к любому члену класса — полю или методу — может быть ограничен. Для этого перед его объявлением ставится ключевое слово private. Оно означает, что к этому члену класса нельзя будет обратиться из методов других классов.
Ключевое слово public может употребляться в тех же случаях, но имеет противоположный смысл. Оно означает, что данный член класса является доступным. Если это поле, его можно использовать в выражениях или изменять при помощи присваивания, а если метод, его можно вызывать.
Ключевое слово protected означает, что доступ к полю или методу имеет сам класс и все его потомки.
Если при объявлении члена класса не указан ни один из перечисленных модификаторов, используется модификатор по умолчанию (default). Он означает, что доступ к члену класса имеют все классы, объявленные в том же пакете.
Перепишем класс Dog
следующим образом:
class Dog {
private int age;// возраст
private String name; // кличка
public Dog(String n, int a) {
name = n; age = a;
}
public void voice() {
for(int i = 1; i <= age; i++) {
System.out.println("гав-гав");
}
}
}
Поля age
и name
окажутся скрытыми. Об этом говорит сайт https://intellect.icu . Это значит, что мы не можем изменять их (или считывать их значение) где-либо за пределами класса*. Мы не сможем в методе main()
создать объект класса Dog
, а затем присвоить его полю age
илиname
новое значение, как в следующем примере:
public static void main(String[] args) {
Dog dog1 = new Dog("Тузик", 4);
dog1.age = 10; // нельзя, поле age скрыто
dog1.name = "Жучка"; // переименовать собаку тоже нельзя, поле name скрыто
dog1.voice(); // это можно, метод voice() открытый
}
Возможность скрывать поля и методы класса используется для того, чтобы уберечь программиста от возможных ошибок, сделать классы понятнее и проще в использовании. При этом реализуется принцип инкапсуляции.
Инкапсуляция означает сокрытие деталей реализации класса. Класс разделяется на две части: внутреннюю и внешнюю. Внешняя часть (интерфейс) тщательно продумывается исходя из того, каким образом могут взаимодействовать с объектами данного класса другие объекты программы. Внутренняя часть закрыта от посторонних, она нужна только самому классу для обеспечения правильной работы открытых методов.
Например, в классе Dog
есть целочисленное поле age
(возраст). Можно оставить его открытым, тогда его при необходимости можно будет изменить простым присваиванием (очень удобно). Но при этом ничто не мешает присвоить этому полю заведомо некорректное значение (например, 666 или -5 или 3000). Это может произойти из-за ошибки в программе. Или, к примеру, пользователь вводит возраст собаки в текстовое поле, а программа присваивает его в ответ на нажатие кнопки (и пользователь может ошибиться). Это нежелательный случай. Лучше сделать поле age
закрытым (private) и добавить два открытых метода: getAge()
и setAge()
. Первый метод будет просто возвращать значение скрытого поля:
public int getAge() {
return age;
}
Второй метод позволит задать новый возраст собаки, производя при этом проверку присваиваемого значения.
public void setAge (int newAge) {
if (newAge < 0) System.out.println("Как это понимать? Собака еще не родилась?");
else if (newAge > 30) System.out.println("Они столько не живут");
else age = newAge;
}
Теперь мы видим, что возраст можно изменить, лишь вызвав метод setAge()
, который в случае неподходящего параметра выведет в консоль сообщение и не будет ничего изменять.
Профессиональные программисты, разрабатывающие программы по объектно-ориентированной методологии, скрывают все поля своих классов, создавая для каждого из них открытые методы c приставками get
и set
, причем в методах set
проводятся все необходимые проверки.
Заметим напоследок, что в нашем примере неправильный возраст может «прорваться» через конструктор при создании нового объекта. Никто ведь не мешает написать:
Dog dog1 = new Dog("Тузик", 2000);
Для того, чтобы этого не случилось, необходимо переписать конструктор:
public Dog(String n, int a) {
name = n;
age = setAge(a);
}
Теперь проверка осуществляется в конструкторе. Попытка завести в программе собаку с явно некорректными данными не увенчается успехом. Если возраст будет меньше 0 или больше 30, присваивание не выполнится и атрибут age
будет иметь значение по умолчанию (для типа int это 0). Такая вот маленькая собачка...
Наш класс Dog
содержит принципиальную ошибку. Он имеет скрытое поле name
, которому можно присвоить начальное значение во время создания объекта, но нельзя изменить и даже узнать впоследствии. Напишите методы getName()
иsetName()
. Никаких проверок проводить не нужно.
В одном классе можно создать несколько методов с одним и тем же именем, различающихся по своим параметрам. Этот прием называется перегрузкой методов. Когда один из этих методов будет вызван, произойдет сопоставление переданных ему параметров (их количества и типов) с параметрами всех методов класса с таким именем. Если подходящий метод будет найден, выполнится именно он.
Например, в дополнение к конструктору, который уже есть в классе Dog
, мы можем описать конструктор без параметров:
public Dog() {
name = "Незнакомец";
}
В этом конструкторе объекту класса Dog
(очевидно, собака с неизвестной кличкой) присваивается при регистрации в программе имя «Незнакомец». Теперь мы можем воспользоваться одним из двух конструкторов:
Dog dog1 = new Dog("Тузик", 2); // Собака по кличке Тузик, возраст 2 года
Dog dog2 = new Dog(); // Собака по кличке «Незнакомец», возраст 0
Dog dog3 = new Dog(10); // Неверно! Не существует конструктора с такими параметрами
Нельзя создавать несколько одноименных методов с одинаковым числом и типом параметров.
Добавьте в класс Dog
третий конструктор для случаев, когда известен возраст, но неизвестная кличка собаки так, чтобы третья команда в примере имела смысл.
Полиморфизм — это возможность класса выступать в программе в роли любого из своих предков, несмотря на то, что в нем может быть изменена реализация любого из методов.
Изменить работу любого из методов, унаследованных от класса-предка, класс-потомок может, описав новый метод с точно таким же именем и параметрами. Это называется переопределением. При вызове такого метода для объекта класса-потомка будет выполнена новая реализация.
Пусть, к примеру, мы хотим расширить наш класс 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 = new Point(30, 30); // добавили в массив точку с координатами 30, 30
figures = new Circle(60, 20, 10); // добавили круг с координатами 60, 20 радиуса 10
figures = 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
у нас хранится именно точка. Хотя мы обращаемся с ней как с просто фигурой, Java знает, что при вызове метода paint()
нужно использовать именно тот вариант, который переопределен в классе Point
. Аналогично команда figures .paint();
нарисует круг, а figures .paint();
нарисует прямоугольник.
Мы рассмотрели очень подробный пример, поскольку описанный прием является одним из наиболее часто используемых средств в арсенале объектно-ориентированного программирования.
Если в классе не описан ни один конструктор, для него автоматически создается конструктор по умолчанию. Этот конструктор не имеет параметров, все что он делает — это вызывает конструктор без параметров класса-предка.
Поэтому мы и смогли создать объект класса BigDog
в примере с большой собакой, хотя не описывали в классе никаких конструкторов. Если вспомнить конструктор без параметров, который у нас есть в классе Dog
, мы поймем, что переменнаяbigdog
в предыдущем примере ссылалась на собаку по кличке "Незнакомец".
В примере с большой собакой нам удалось создать ее с помощью конструктора без параметров, т.е. не указывая ее имя и возраст. Оказывается, если бы мы попытались сделать это по-другому, у нас бы не получилось. Дело в том, что конструкторы не считаются членами класса и, в отличие от других методов, не наследуются.
BigDog bigdog = new BigDog("Полкан", 8); // Ошибка. Такого конструктора в классе нет
Для того, чтобы мы могли создавать больших собак с интересующими нас именем и возрастом, необходимо написать подходящий конструктор. При этом не обязательно повторять те команды, которые мы писали в конструкторе класса Dog
(их всего две, но ведь могло быть гораздо больше). Вместо этого мы можем написать:
BigDog (String n, int a) {
super(n, a);
}
Ключевое слово super означает суперкласс (в нашем случае это класс Dog
). В примере мы вызываем с его помощью конструктор суперкласса. При этом мы передаем два параметра — строку и число, — так что из всех конструкторов будет выбран именно тот, который нас интересует.
Вызов конструктора суперкласса должен происходить в самом начале конструктора.
Вместо вызова конструктора суперкласса можно вызвать один из конструкторов того же самого класса. Это делается с помощью ключевого слова this()
— с параметрами в скобках, если они нужны.
Если в начале конструктора нет ни вызова this()
, ни вызова super()
, автоматически происходит обращение к конструктору суперкласса без аргументов.
Объект класса-потомка можно присвоить переменной типа класса-предка. При этом Java производит автоматическое преобразование типа, называемое расширением. Расширение — это переход от более конкретного типа к менее конкретному. Переход от byte к int — это тоже расширение.
Рассмотрим пример, имеющий отношение к основному заданию. В программе есть класс User
, предназначенный для обработки информации о пользователях системы. В этом классе есть метод enter(String login, String password)
, который возвращает true, если переданные в метод логин и пароль совпадают с логином и паролем, скрытым в полях класса.
Мы наследуем от класса User
два подкласса: Admin
и, к примеру, Member
(для программы координации участников встречи, см. задание 13). Класс Admin
может понадобиться нам впоследствии для каких-то специфичных действий, связанных с управлением системой, а класс Member
моделирует участников проекта, которые с помощью программы пытаются выбрать оптимальное место для встречи. Открытый метод addRequest(String place, int day, int from, int to)
вызывается, когда участник проекта предлагает новый вариант времени и места встречи.
В главном классе программы мы храним массив* users
, содержащий всех пользователей системы. Элементы этого массива имеют тип User
, но мы можем присваивать им ссылки на объекты как класса Member
, так и класса Admin
. В этот момент и будет происходить расширение типа.
Member member = new Member(...);
users = member; // Java проводит автоматическое преобразование типа Member к типу User, чтобы поместить переменную member в массив users
Для того, чтобы найти пользователя с введенными логином и паролем программа выполняет запрос:
for (int i = 0; i < users.length; i++) {
if (users[i].enter(log, passw)) currentUser = users[i];
}
Несмотря на то, что все объекты, добавленные в массив, сохраняют свой "настоящий" класс, программа работает с ними как с объектами класса User
. Этого вполне достаточно, чтобы можно было найти нужного пользователя по логину и паролю (ведь метод enter()
у них общий) и присвоить найденный объект переменной currentUser
типа User
. В этой переменной хранится текущий пользователь, авторизовавшийся в системе.
Предположим, нам известно, что переменная currentUser
сейчас ссылается на объект класса Member
и текущий пользователь предлагает встретиться у фонтана в среду с 17 до 19 часов. Необходимо вызвать метод addRequest()
, но у нас не получится сделать это командой
currentUser.addRequest("Фонтан", 3, 17, 19);
поскольку в классе User
нет метода addRequest()
.
Однако мы можем осуществить явное преобразование переменной currentUser
к типу Member
. Такое преобразование (переход от менее конкретного типа к более конкретному) называется сужением. Явное преобразование делается с помощью оператора, представляющего собой имя целевого типа в скобках.
((Member)currentUser).addRequest("Фонтан", 3, 17, 19);
Здесь мы, прежде чем вызвать метод addRequest()
, преобразовали переменную currentUser
к типу Member
. Нам было позволено сделать это, поскольку Member
является потомком User
. Однако, если бы во время выполнения программы оказалось, что на самом деле переменная currentUser
не ссылалась на объект класса Member
, в программе возникла бы ошибка.
Чтобы уточнить, соответствует ли текущее значение переменной конкретному типу, используется оператор instanceof.
Проверим, не является ли текущий пользователь администратором (в этом случае программа должна перейти в режим управления):
if (currentUser instanceof Admin) {...}
Внутри фигурных скобок, заключающих в себе тело одного класса, помимо описания его полей и методов можно поместить описание другого класса. Он будет называться вложенным классом.
Эта возможность иногда используется, чтобы подчеркнуть отношение агрегации между классами. Например, нам может понадобиться класс Eye
, чтобы описать сложную структуру и поведение глаза (он может открываться и закрываться). Но объекты этого класса не будут создаваться сами по себе, они являются неотъемлемой частью объектов класса Dog
и только их. Поэтому мы помещаем описание класса Eye
в класс Dog
:
class Dog {
...
class Eye {
private boolean opened;
public void close() {
opened = false;
System.out.println("глаз закрыт");
}
public void open() {
opened = true;
System.out.println("глаз открыт");
}
public boolean isOpened() {
return opened;
}
}
Eye rightEye = new Eye(), leftEye = new Eye();
}
Мы сразу же добавили в класс Dog
два поля класса Eye
и проинициализировали их вновь созданными объектами (это можно было сделать и в конструкторе). Теперь у собаки есть два глаза и она может открывать их и закрывать. Например, предположим, что все собаки лают с закрытым правым глазом. Тогда метод voice()
надо переписать так:
public void voice() {
rightEye.close();
for (int i = 1; i <= age; i++) {
System.out.println("гав-гав");
}
rightEye.open();
}
Обратиться ко вложенному классу нужно с помощью составного имени (в нашем случае это Dog.Eye
). Если обращение происходит из содержащего его класса, имя можно сократить (как и было сделано выше).
Класс можно объявить внутри метода другого класса. В этом случае класс "виден" только внутри метода (за пределами метода нельзя объявить переменную типа этого класса).
Анонимным классом называется класс, не имеющий имени. Очевидно, если у класса имени нет, к нему нельзя обратиться из программы. Точнее, это можно сделать только один раз — в том месте, где класс объявляется.
Описание анонимного класса начинается с вызова конструктора его суперкласса, после чего в фигурных скобках описывается тело класса.
Анонимные классы используются в том случае, когда нужен единственный объект такого класса на всю программу.
Пусть, например, в нашей программе собачьего питомника имеется массив dogs
объектов типа Dog
. И мы хотим добавить в этот массив совершенно уникальную собаку, которая не лает, а разговаривает. Необходимо описать класс, унаследованный от класса Dog
, в котором будет соответствующим образом переопределен метод voice()
. Но поскольку нам гарантированно понадобится только одна такая собака, мы можем описать анонимный класс прямо в месте добавления собаки в питомник (посадим ее в клетку № 10):
dogs[10] = new Dog(){
public void voice() {
System.out.println("Я уникальная говорящая собака.");
}
};
И, несмотря на то, что мы не сможем создавать переменных этого класса, мы спокойно можем пользоваться новой собакой как объектом класса Dog
. А благодаря полиморфизму вызов метода
dogs[10].voice();
приведет к тому, что уникальная собака будет уникально подавать голос.
Любой член класса можно объявить статическим, указав перед его объявлением ключевое слово static. Статический член класса «разделяется» между всеми его объектами.
Для поля это означает, что любое изменение его значения, сделанное одним из объектов класса, сразу же «увидят» все остальные объекты.
Метод, объявленный с модификатором static, "дает обещание" не изменять никаких полей класса, кроме статических.
Для обращения к статическому члену класса можно использовать любой объект этого класса. Более того, это обращение можно осуществлять даже тогда, когда не создано ни одного такого объекта. Вместо имени объекта можно просто указывать имя класса.
Модифицируйте класс Dog
таким образом, чтобы можно было считать собак, созданных во время работы программы. Для этого введите статический атрибут count
(изначально равный нулю) и увеличивайте его на единицу в каждом конструкторе. Создайте в методе main()
несколько объектов класса Dog
, а затем выполните для проверки команду:
System.out.println("Всего было создано собак: " + Dog.count);
Любое поле класса можно объявить неизменяемым, указав перед его объявлением ключевое слово final. Неизменяемому полю можно присвоить значение только один раз (обычно это делается сразу при объявлении).
Константы в языке Java очевидным образом описываются путем совмещения модификаторов static и final. Например, мы можем объявить константу PI (лучше это делать в основном классе, а не в классе Dog
*), написав:
final static double PI = 3.14;
Если ключевое слово final указать перед объявлением метода, это будет обозначать, что метод нельзя переопределять при наследовании (т.е. данная версия метода будет окончательной).
Перед объявлением класса модификатор final ставится в том случае, если необходимо запретить от него наследование.
Внимание! На данном этапе обучения вы уже должны владеть знаниями по этой теме. Если их нет, а материалы для повторения непонятны или недостаточны, вы с заданиями не справитесь! Необходимо срочно обратиться к литературе по данной теме.
Приступите к реализации классов вашей задачи в соответствии с предложенной диаграммой. В каждом классе напишите конструкторы, необходимые для заполнения атрибутов класса (при необходимости реализуйте проверку переданных в конструктор параметров). Вы должны отчитаться по этому заданию на пятом занятии.
На этом все! Теперь вы знаете все про понятия ооп, Помните, что это теперь будет проще использовать на практике. Надеюсь, что теперь ты понял что такое понятия ооп, класс объект абстрагирование инкапсуляция полиморфизм наследование и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории ООП и практические JAVA
Комментарии
Оставить комментарий
ООП и практические JAVA
Термины: ООП и практические JAVA