Тема: Анонимные классы, интерфейсы, методы. Лямбда-выражения. Встроенные функциональные интерфейсы. Ссылка на методы и конструкторы.
При создании объектов с помощью оператора new возвращается ссылка на вновь созданный объект. Однако нас никто не обязывает эту ссылку присваивать в качестве значения ссылочной переменной. В таких случаях создается анонимный объект. Другими словами, объект есть, а переменной, которая бы содержала ссылку на этот объект, нет.
С практической точки зрения это может выглядеть бесполезным, однако, анонимные объекты требуются довольно часто - обычно в тех ситуациях, когда объект класса используется один раз. Рассмотрим пример:
class PrintManager {
// Различные поля и методы
public void printFile(File file) {
// Распечатка файла на принтере
}
}
В данном случае, нам нужен объект класса PrintManager только для одного действия - для распечатки файла. То есть, мы создаем объект, вызываем метод, после чего объект нам больше не нужен.
В таких случаях удобно использовать анонимные объекты.
File file = new File("file.txt");
// Обычное создание объекта и вызов его метода
PrintManager manager = new PrintManager();
manager.printFile(file);
// Использование анонимного объекта
new PrintManager().printFile(file);
// Использование двух анонимных объектов
new PrintManager().printFile(new File("file.txt"));
В первом случае используется обычный порядок работы с объектами - создаем объект класса PrintManager, ссылка на объект записывается в ссылочную переменную manager.
Во втором случае мы создаем анонимный объект. Инструкция
new PrintManager().printFile(file);
выполняется следующим образом. Сначала создается новый объект класса PrintManager, оператор new возвращает ссылку на созданный объект. После чего, мы вызываем метод printFile() с аргументом file.
В третьем случае, мы в качестве аргумента метода printFile() передаем анонимный объект класса File. В этом случае, ссылку на объект класса File будет хранить параметр метода printFile() внутри тела метода.
Анонимные классы
Механизм анонимных классов позволяет объявить класс и сразу создать его экземпляр. Это позволяет сделать код кратким и выразительным. Анонимные классы удобно использовать, если класс нужен единожды.
Основная особенность - анонимный класс не имеет имени. Анонимный класс может быть подклассом существующего класса или реализацией интерфейса.
Особенности анонимного класса:
нет явного конструктора;
к анонимному классу невозможно обратиться извне объявляющего его выражения;
анонимные классы не могут быть статическими;
анонимный класс всегда конечен (final);
каждое объявление анонимного класса уникально.
В примере ниже объявлены два анонимный класса, которые являются подклассами PrintManager. Оба анонимных класса являются разными уникальными классами.
PrintManager manager1 = new PrintManager() {
@Override
public void printFile(File file) {
super.printFile(file);
}
};
PrintManager manager2 = new PrintManager() {
@Override
public void printFile(File file) {
super.printFile(file);
}
};
Анонимные методы (лямбда-выражения)
Из-за особенностей реализации лямбда-выражений в Java, дальнейшая информация по поводу лямбда-выражений справедлива только для языка Java.
Лямбда-выражение - это анонимный метод, то есть метод без названия. Такой метод выполняется не самостоятельно, а служит для реализации функционального интерфейса. Таким образом, лямбда-выражение приводит к некоторой форме анонимного класса.
Функциональный интерфейс - интерфейс, который содержит один и только один абстрактный метод. Как правило, такой метод определяет предполагаемое назначение интерфейса. Следовательно, функциональный интерфейс представляет единственное действие. К функциональным интерфейсам можно отнести, например, интерфейс ActionListener.
Лямбда-выражения позволяют объявить метод и сразу же использовать его. Это полезно в случаях однократного вызова метода, так как сокращает время на объявление и написание метода без необходимости создавать отдельный класс. Лямбда-выражение в Java имеет следующий синтаксис:
(аргументы) -> (тело выражения)
Лямбда-выражение вносит новый элемент в синтаксис и оператор в язык Java. Этот новый оператор называется лямбда-оператором, или операцией "стрелка" ->. Он разделяет лямбда-выражение на две части. В левой части указываются любые параметры, требующиеся в лямбда-выражении (если параметры не требуются, то они указываются пустым списком). А в правой части находится тело лямбда-выражения, где указываются действия, выполняемые лямбда-выражением. Операция -> буквально означает "становиться" или "переходить".
В Java определены две разновидности тел лямбда-выражений. Одна из них состоит из единственного выражения (одиночное выражение), а другая - из блока кода (блочное выражение).
Одиночное лямбда-выражение.
Для начала рассмотрим самое простое лямбда-выражение. Это будет выражение, которое не принимает никаких параметров, а возвращает константу. Также объявим метод, который аналогичен лямбда-выражению.
() -> 99
public double foo() {
return 99;
}
Приведем еще один пример уже более полезного выражения
Как вам понятно, лямбда-выражение возвращает псевдослучайное значение, умноженное на 100.
Два первых примера не принимают никаких параметров. Если же вам необходимо передать параметры, они указываются списком в левой части лямбда-оператора.
(a, b) -> a * b
public double foo(int a, int b) {
return a * b;
}
Тип параметров можно указывать явно, но зачастую в этом нет необходимости, потому что тип параметров выводится.
Блочные лямбда-выражения
Предыдущие примеры содержали в теле выражения лишь одно выражение. Мы уже знаем, что такие лямбда-выражения называются одиночными.
Но ничего вам не мешает создавать блочные лямбда-выражения, которые в теле содержат блок выражений. В таком блоке можно создавать циклы, ветвления - все как в обычном блоке выражений.
При создании блочного выражения необходимо явно указать оператор return.
Функциональные интерфейсы. Ссылки на методы и конструкторы.
Лямбда-выражение как реализация интерфейса
Как было упомянуто ранее, в Java 8 было введено понятие функционального интерфейса - интерфейса с одним абстрактным методом. Лямбда-выражение не существует самостоятельно, а реализует абстрактный метод, определенный в функциональном интерфейсе.
Таким образом, лямбда может быть указана в том контексте, в котором определен ее целевой тип. Один из таких контекстов создается в том случае, когда лямбда присваивается ссылке на функциональный интерфейс. К числу других контекстов целевого типа относится инициализация переменных, оператор return и аргументы методов.
Данная тема находится в процессе оформления материала.
Лекция 6
Тема: Принцип абстракции. Дополнительные принципы ООП. Абстрактный класс. Интерфейс. Использование абстрактного класса и интерфейса. Реализация механизма обратного вызова с помощью интерфейса.
Принцип абстракции
Абстракция - упрощенное описание или изложение системы, при котором одни свойства и детали выделяются, а другие опускаются. Хорошей является абстракция, подчеркивающая детали, существенные для данной предметной области, и опускающая несущественные детали. Также абстракция позволяет отличать один объект от другого.
Принцип минимальных обязательств - интерфейс объекта должен описать только существенные аспекты его поведения;
Принцип наименьшего удивления - абстракция должна описывать только поведение объекта, ни больше, ни меньше.
Виды абстракций:
абстракция сущности - объект представляет собой полезную модель некоторой сущности в предметной области ("Студент", "Преподаватель", "Аудитория");
абстракция поведения - объект состоит из обобщенного множества операций ("Менеджер соединения с базой данных");
абстракция виртуальной машины - объект группирует операции, которые вместе используются более высоким уровнем управления;
произвольная абстракция - объект включает в себя набор операций, не имеющих друг с другом ничего общего.
Описывая поведение какого-либо объекта, например, автомобиля, мы строим его модель. Модель не может описать объект полностью, реальные объекты слишком сложны. Приходится отбирать только те характеристики объекта, которые важны для решения поставленной перед нами задачи.
Для описания грузоперевозок важной характеристикой будет грузоподъемность автомобиля, а для описания автомобильных гонок она не существенна. Но для моделирования гонок обязательно надо описать метод набора скорости данным автомобилем, а для грузоперевозок это не столь важно.
Для характеристики спортсмена обязательно надо указать его вес, рост, скорость реакции, спортивные достижения, а для ученого все эти качества несущественны, зато важно его квалификация, ученая степень, количество опубликованных научных работ и так далее.
Мы должны абстрагироваться от некоторых конкретных деталей объекта. Очень важно выбрать правильную степень абстракции. Слишком высокая степень даст только приблизительное описание объекта, не позволит правильно моделировать его поведение. Слишком низкая степень абстракции сделает модель очень сложной, перегруженной деталями, и поэтому непригодной.
При разработке программного обеспечения нам необходимо выбрать уровень абстракции, необходимый для правильного описания реального информационного процесса. Затем следует выделить объекты, принимающие участие в этом процессе, и установить связи между этими объектами.
Как это сделать? Можно воспользоваться следующей методикой: опишите процесс словами и проанализируйте получившиеся фразы. "Завод выпускает автомобили" - здесь два объекта: завод и автомобиль. Производственно-технические характеристики завода составят набор полей объекта "Завод", а процесс выпуска автомобиля будет описан в виде набора методов объекта "Завод".
Пример из другой области - "Преподаватель читает учебный курс". Полями объекта "Преподаватель" будут его фамилия, имя и отчество, научно-педагогический стаж, квалификация, ученая степень, выпущенные им учебники и методические пособия. Методами "Преподавателя" будут такие действия как "проводить лекцию", "повышать квалификацию", "проводить консультацию", "принимать зачет" и так далее.
Полями объекта "Учебный курс" будут его название, программа, количество часов, перечень учебных пособий. Будет ли объект "Учебный курс" обладать какими-то методами или в этом объекта будут только поля? Какие действия выполняет "Учебный курс"? По-видимому, единственным действием объекта "Учебный курс" будет предоставление своих полей другим объектам, значит, нужны методы доступа к полям объекта.
Очень упрощенно можно сказать, что если в словесном описании процесса вам потребовалось сформулировать какое-то понятие, то оно будет кандидатом на оформление его в виде класса. Существительные, описывающие это понятие, будут полями класса, а глаголы - методами будущего класса.
Дополнительные принципы ООП
Кроме основных принципов ООП, некоторые ученые и авторы выделяют дополнительные принципы ООП, соблюдение которых не обязательно для разработки объектно-ориентированного программного обеспечения. Выделим наиболее важные дополнительные свойства объектно-ориентированного программирования.
Модульность
Модульность - свойство системы, которая была разложена на внутренние связные, но слабо связанные между собой модули. Структура каждого модуля должна быть достаточно простой для понимания, допускать независимую реализацию других модулей и не влиять на поведение других модулей, а также позволять легкое изменение проектных решений.
Иерархия
Иерархия - ранжированная или упорядоченная система абстракций. Принцип иерархичности предполагает использование иерархий при разработке программных систем. В ООП используется два вида иерархии:
иерархия "целое/часть" - показывает, что некоторые абстракции являются частями других абстракций. Например, лампа состоит из цоколя, нити накаливания и колбы;
иерархия "общее/частное" - показывает, что некоторая абстракция является частным случаем другой абстракции. Например, "обеденный стол" - конкретный вид стола, а "стол" - конкретный вид мебели. Такая иерархия используется при разработке структуры классов, когда сложные классы строятся на базе более простых классов путем добавления к ним новых характеристик и, возможно, уточнения имеющихся. Реализуется с помощью иерархии наследования.
Типизация
Типизация - это ограничение, накладываемое на свойства объектов и препятствующее взаимозаменяемости абстракций различных типов. Язык Java имеет строгую типизацию, когда для каждого программного объекта (переменной, функции, аргумента и так далее) объявляется тип, который определяет множество операций над соответствующим программным объектом.
Устойчивость
Устойчивость - свойство абстракции существовать во времени (независимо от процесса, породившего данный программный объект) и в пространстве (перемещаясь из адресного пространства, в котором он был создан). Различают:
временные объекты - хранят промежуточные результаты некоторых действий, например, вычислений;
локальные объекты - существуют внутри методов, объект уничтожается после окончания работы метода;
глобальные объекты - существуют, пока программа загружена в память;
сохраняемые объекты - хранятся в файлах внешней памяти между сеансами работы программы.
Интерфейс и абстрактные классы улучшают структуру кода и способствует отделению интерфейса от реализации. В первую очередь необходимо рассмотреть понятие абстрактного класса, который является промежуточной ступенью между обычным классом и интерфейсом.
Абстрактные классы
Рассмотрим пример с базовым классом Shape и его подклассами.
class Shape {
public String draw() {
return null;
}
public String erase() {
return null;
}
}
class Circle extends Shape {
@Override
public String draw() {
return "Рисуем круг";
}
@Override
public String erase() {
return "Стираем круг";
}
}
class Triangle extends Shape {
@Override
public String draw() {
return "Рисуем треугольник";
}
@Override
public String erase() {
return "Стираем треугольник";
}
}
Множество моделей предметов реального мира обладают некоторым набором общих характеристик и правил поведения. Абстрактное понятие "Геометрическая фигура" может содержать описание геометрических параметров и расположения центра тяжести в системе координат, а также возможности определения площади и периметра фигуры. Однако для понятия "Геометрическая фигура" невозможно дать конкретную реализацию. Для конкретного понятия, например, "Квадрат", предоставить реализацию определения площади и параметра не составляет труда. Абстрагирование понятия должно предоставлять абстрактные характеристики предмета реального мира, а не его ожидаемую реализацию. Грамотное выделение абстракций позволяет структурировать код программной системы в целом и повторно использовать абстрактные понятия для конкретных реализаций.
Методы базового класса Shape являются "фиктивными". Попытка вызова метода из класса Shape привела бы к ошибке в программе. Это было связано с тем, что класс Shape был нужен лишь для того, чтобы определить общий интерфейс всех классов, производных от него, а уже производные классы переопределяли эти методы и реализовывали их по-своему.
Класс Shape определял базовую форму, общность всех производных классов. Такие классы как Shape называют абстрактными базовыми классами или просто абстрактными классами.
Если в программе определяется такой абстрактный базовый класс вроде Shape, создание объектов такого класса практически всегда бессмысленно. Абстрактный класс создается для работы с набором классов через общий интерфейс. А если Shape только выражает интерфейс, а создание объектов такого класса не имеет смысла, лучше всего запретить пользователю создавать такие объекты, так как он может ненароком создать объект этого класса и попытаться с ним работать, что приведет к ошибкам в программе.
В языке Java для решения подобных задач используют механизм абстрактных методов. Абстрактный метод является незавершенным; он состоит только из объявления и не имеет тела. Синтаксис объявления абстрактных методов выглядит следующим образом:
abstract Shape draw();
Класс, содержащий хотя бы один абстрактный метод, называется абстрактным классом. Такой класс также должен помечаться ключевым словом abstract (в противном случае, компилятор выдает сообщение об ошибке):
abstract class Shape {
abstract Shape draw();
}
На уровне языка и компилятора создать экземпляр абстрактного класса невозможно
Если вы объявите класс, производный от абстрактного класса, но хотите иметь возможность создания новых объектов, то вы должны переопределить все абстрактные методы базового абстрактного класса. Если это не будет сделано, то производный класс тоже останется абстрактным, и компилятор заставит пометить новый класс ключевым словом abstract.
Объявление класса как abstract не подразумевает, что все его методы должны быть абстрактными.
Абстрактный класс может иметь другие, не абстрактные методы, поля и даже конструкторы.
abstract class Shape {
private Point center;
private Dimension size;
// Геттеры и сеттеры полей
// Нарисовать фигуру
public abstract void drawFigure(Graphics2D graphics2D);
// Вернуть площадь фигуры
public abstract double getArea();
}
class Circle extends Shape {
private int radius;
@Override
public void drawFigure(Graphics2D graphics2D) {
graphics2D.drawOval(getCenter().x, getCenter().y, radius * 2, radius * 2);
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
Механизм абстрактных классов и методов очень полезен, так как он позволяет подчеркнуть абстрактность сущности, снижает риск возникновения ошибок в коде, а также сообщает пользователю и компилятору, как следует с ним обходиться.
Кроме того, абстрактные классы играют полезную роль при рефакторинге, потому что они позволяют легко перемещать общие методы вверх по иерархии наследования.
С помощью абстрактного класса объявляется контракт (требования к функциональности) для его подклассов. Примером может служить уже рассмотренный выше абстрактный класс Shape и его подклассы Circle, Triangle и другие. Класс Shape объявляет контракт на реализацию ряда методов для рисования и определения площади. Реализация этих методов будет различной для каждого из подклассов.
Интерфейс
Ключевое слово interface становится следующим шагом на пути к абстракции. Ключевое слово abstract позволяет создать в классе один или несколько неопределенных методов - разработчик предоставляет часть интерфейса без реализации, которая должна предоставляться производными классами.
Ключевое слово interface используется для создания классов, вообще не имеющих реализации. Создатель интерфейса определяет имена методов, списки аргументов и типы возвращаемы значений, но не тела методов. Интерфейс описывает форму, но не реализацию.
Начиная с Java 8, в язык были добавлены различные механизмы для интерфейса - методы по умолчанию, статические и приватные методы, а также константы.
Это, безусловно, облегчает повседневную жизнь разработчику, но затрудняет изучение Java. Поэтому мы будет рассматривать механизм интерфейсов "в чистом виде", без упоминания различных дополнительных возможностей.
Ключевое слово interface фактически означает - именно так должны выглядеть все классы, которые реализуют данный интерфейс. Поэтому любой код, использующий конкретный интерфейс, знает только то, какие методы вызываются для этого интерфейса, но не более того. Интерфейс определяет своего рода "протокол взаимодействия" между классами.
Кроме этого, в отличие от абстрактного класса, интерфейс позволяет реализовать своего рода, множественное наследование.
Чтобы создать интерфейс, используйте ключевое слово interface вместо class. Как и в случае с классами, перед словом interface указывается модификатор доступа. Интерфейс также может содержать поля, они автоматически являются статическими (static) и неизменяемыми (final).
Обратите внимание, что мы не указываем для методов модификатор. Все объявленные в интерфейсе абстрактные методы автоматически трактуются как public abstract, а все поля - как public static final, даже если они так не объявлены.
Для создания класса, реализующего определенный интерфейс (или группу интерфейсов), используется ключевое слово implements. Фактически это означает "интерфейс определяет форму, а данный класс определяет, как это будет реализовано".
class Circle implements Shape {
private int radius;
private Point center;
private Dimension size;
@Override
public void draw(Graphics2D g) {
// Отрисовка фигуры
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
В классе, который реализует интерфейс, реализуемые методы должны быть объявлены как public.
Неважно, приводите ли вы преобразование к "обычному" классу с именем Shape, к абстрактному классу Shape или к интерфейсу Shape - действие будет одинаковым.
Когда метод работает с классом вместо интерфейса, мы ограничены использованием базового класса и его подклассами. Это исключает возможность использовать метод для класса, который не входит в эту иерархию. Интерфейс, в значительной степени ослабляет это ограничение. В результате код становится более универсальным.
В языке Java существуют три вида интерфейсов: интерфейсы, определяющие функциональность для классов посредством описания методов, но не их реализации; функциональные интерфейсы, определяющие в одном абстрактном методе свое применение и интерфейсы, реализация которых автоматически придает классу определенные свойства (к последним относятся, например, интерфейсы Cloneable, Serializable, Comparable и так далее).
В языке Java существуют
Применение интерфейсных ссылок
В Java можно объявлять переменные ссылочного интерфейсного типа, то есть переменные, хранящие ссылки на интерфейс. Такая переменная может ссылаться на любой объект, реализующий ее интерфейсный тип. При вызове метода для объекта по интерфейсной ссылке выполняется вариант этого метода, реализованный в классе данного объекта. Этот процесс аналогичен применению ссылки на суперкласс для доступа к объекту подкласса.
public static void main(String[] args) {
Shape s1 = new Circle();
s1.draw();
}
Реализация нескольких интерфейсов
Так как интерфейс по определению не имеет реализации, нет ничего что могло бы помешать совмещению нескольких интерфейсов. При объявлении класса, который совмещает несколько интерфейсов, имена интерфейсов перечисляются вслед за ключевым словом implements и разделяются запятыми. Класс обязан предоставить реализацию для всех методов интерфейсов, которые он реализует.
interface Shape {
void draw();
double getArea();
}
interface Movable {
void move(int x_offset, int y_offset);
}
class Circle implements Shape, Movable {
private int radius;
private Point center;
private Dimension size;
@Override
public void draw() {
// Отрисовка фигуры
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public void move(int x_offset, int y_offset) {
// Перемещение фигуры
}
}
Наследование интерфейсов
Многие языки программирования, в том числе Java, позволяют наследовать интерфейс один от другого. Синтаксис наследования интерфейсов аналогичен синтаксису наследования классов. Когда класс реализует интерфейс, он обязан реализовать все методы, определенные по цепочке наследования интерфейсов.
interface Attackable {
void attack(Attackable attackable);
void takeDamage(Attackable attackable);
}
interface Movable extends Attackable {
void move(int x_offset, int y_offset);
}
interface Flyable extends Movable {
void fly(Point destination);
}
class Gargoyle implements Flyable {
@Override
public void attack(Attackable attackable) {}
@Override
public void takeDamage(Attackable attackable) {}
@Override
public void move(int x_offset, int y_offset) {}
@Override
public void fly(Point destination) {}
}
Частичные реализации
Если класс включает в себя интерфейс, но не полностью реализует определенные в нем методы, он должен быть объявлен как abstract
interface Shape {
void draw();
void getArea();
}
abstract class Circle implements Shape {
@Override
public void getArea() {}
}
Реализация механизма обратного вызова с помощью интерфейса
Механизм обратного вызова широко распространен в программировании. При обратном вызове программист задает действия, которые должен выполнять всякий раз, когда происходит некоторое событие. Например, можно задать действие, которое должно быть выполнено после клика на кнопку или при выборе определенного пункта меню.
Приведем небольшой пример. В Java нам доступен класс Timer, который используется для отсчета интервала времени.
Устанавливая таймер, мы задаем интервал времени и указываем, что должно произойти по его истечении. Как указать таймеру, что он должен делать по истечении времени?
public static void main(String[] args) {
// Как нам во втором аргументе указать
// что должен делать таймер?
Timer timer = new Timer(5000, );
}
В ООП для таких случаев существует механизм обратного вызова. Он заключается в том, что программист должен передать таймеру объект некоторого класса. После этого таймер вызывает один из методов данного объекта.
Разумеется, таймер должен знать, какой метод объекта он должен вызвать и должен иметь гарантию, что в классе объекта реализован этот метод. Для этого таймеру нужно указать объект класса, который реализует интерфейс ActionListener. Этот интерфейс выглядит следующим образом
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent e);
}
По истечении заданного интервала времени таймер обращается к объекту, вызывает метод actionPerformed() и передает ему объект класса Event (класс Event описывает событие в Java).
Как мы видим, конструктор класса Timer запрашивает задержку и объект, у которого будет вызван метод actionPerformed. Создадим класс, который будет реализовывать интерфейс ActionListener
class TimerAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null,
"Время истекло!", "Таймер", JOptionPane.WARNING_MESSAGE);
}
}
Как мы видим, данный класс ничего кроме реализации интерфейса не делает. То есть он нужен только для одной цели - он содержит метод, который будет вызван таймером после задержки. Такие классы и их объекты называют слушателем.
Слушатель - это объект, который как бы "слушает" события, которые происходят с другим объектом. Когда это "слушаемое" событие происходит, вызывается указанный в интерфейсе метод этого объекта.
Создадим объект слушателя TimerAction и передадим этот объект таймеру
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setVisible(true);
// Объект слушателя
TimerAction action = new TimerAction();
Timer timer = new Timer(5000, action);
timer.setRepeats(false);
timer.start();
}
Запустим приложение и посмотрим на результат
Обратите внимание, что метод actionPerformed() принимает на вход объект класса ActionPerformed. При вызове метода actionPerformed(), таймер передает в метод объект класса ActionEvent, который содержит различную информацию о событии. Таким образом, мы можем запрограммировать те или иные действия в зависимости от параметров события. Рассмотрим еще один пример, на этот раз будем использовать кнопку.
Создадим объект окна, объект кнопки и добавим кнопку в окно
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setVisible(true);
frame.setLayout(new FlowLayout());
JButton button = new JButton("Нажми меня");
frame.add(button);
}
По умолчанию, при нажатию на кнопку ничего не происходит. По аналогии с таймером нам необходимо передать кнопке слушатель, который реализует определенный интерфейс. Кнопка - гораздо более сложный объект, чем просто таймер, поэтому в связи с кнопкой может произойти очень много событий. Для каждого типа событий кнопка принимает свой слушатель, который реализует свой определенный интерфейс
С помощью такого многообразия интерфейсов мы можем обработать самые разнообразные события, которые могут случиться с кнопкой.
В данном случае, нас интересует метод addActionListener(ActionListener l), который принимает на вход слушатель, который реализует интерфейс ActionListener.
class ButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null,
"На кнопку нажали", "Кнопка", JOptionPane.WARNING_MESSAGE);
}
}
С помощью этого метода мы передаем кнопке объект класса ButtonListener. Когда произойдет какое-то событие, кнопка обратится к переданному объекту и вызовет метод actionPerformed() этого объекта.
Такая реализация в большинстве случаев нас устраивает. Но что, если нужно обработать какое-то специфическое событие и получить детальную информацию. Например, что если мы хотим, чтобы кнопка вела себя по разному при нажатии левой и правой кнопок мыши?
Для этого у кнопки необходимо вызвать метод addMouseListener(MouseListener l) и передать ему слушатель, который реализует интерфейс MouseListener. Таким образом, мы обрабатываем не просто некоторое событие, а событие мыши. Событие мыши - более специфическое событие и поэтому нам доступна большая информация о событии.
Создадим слушатель, который реализует интерфейс MouseListener
class MouseListener implements java.awt.event.MouseListener {
@Override
public void mouseClicked(MouseEvent e) {
String message = "Нажата неизвестная кнопка мыши";
if (e.getButton() == MouseEvent.BUTTON1) {
message = "Нажата первая кнопка мыши";
} else if (e.getButton() == MouseEvent.BUTTON2) {
message = "Нажата вторая кнопка мыши";
}
JOptionPane.showMessageDialog(null,
message, "Кнопка", JOptionPane.WARNING_MESSAGE);
}
@Override
public void mousePressed(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}
Обратим внимание на две особенности:
1) интерфейс MouseListener определяет уже несколько методов, а не один, как ActionListener. Как видите, мы можем запрограммировать реакцию на очень специфические события, например, если курсор мыши вошел в область, которую занимает кнопка. Так как нас интересует только события клика, все остальные методы у нас имеют пустую реализацию. То есть, например, курсор мыши войдет в область кнопки, то кнопка вызовет пустой метод mouseEntered() и ничего не произойдет (поищите информацию о классе MouseAdapter, который упрощает работу с событиями мыши);
2) в методы передается объект класс MouseEvent. Объект класса MouseEvent обладает информацией о событии мыши, что дает нам возможность узнать о событии много подробностей.
Мы видим, что мы можем, например, с помощью метод getButton() узнать - какой кнопкой было произведено нажатие, с помощью методов getX() и getY() узнать координаты нажатия и так далее. Создадим объект слушателя MouseListener и передадим этот объект кнопке
MouseListener mouseListener = new MouseListener();
button.addMouseListener(mouseListener);
Запустим приложение и нажмем кнопку левой кнопкой мыши.
Лекция 12
Тема: Ввод и вывод в Java. Пакеты java.io, java.nio, java.nio2. Сериализация и десериализация.
Материал по данной теме находится в процессе оформления.
Видео
1. Обсуждение первой лабораторной работы, задание на вторую
2. Вторая лекция (объекты, классы)
3. Обсуждение второй лабораторной, задание на третью
4. Третья лекция (инкапсуляция)
5. Задание на 4 лабораторную работу
6. Четвертая лекция (наследование)
7. Задание на 4 лабораторную работу (для группы АИ-205)
8. Четвертая лекция (полиморфизм)
7. Задание на 5 лабораторную работу (для группы АИ-205)
Главная
Лекция 1
Тема: Введение в дисциплину ООП. История и предпосылки возникновения ОО подхода. Основные понятия и задачи ОО подхода. Преимущества и недостатки ОО подхода.
Если бы строители строили здания так же, как программисты пишут программы, первый залетевший дятел разрушил бы цивилизацию.
(с) Второй закон Вейнберга
Любая компьютерная программа, в конечном итоге, сводится к машинному коду - набору двоичных инструкций, которые выполняются центральным процессором.
Чтобы задать эти инструкции, на заре компьютерной эры, в 40-х годах ХХ века, от программистов требовалось физически использовать различные переключатели и менять месторасположение различных перемычек. Это было чудовищно неудобно, так как компьютерную программу необходимо было вводить в компьютер вручную.
ENIAC, 27-тонный клубок из 18000 электронных ламп и диодов, занимавший площадь в 167 квадратных метров, считается первым в мире настоящим компьютером. Машина могла выполнить 5 000 операций в секунду.
В 50-х годах компьютеры начали использовать перфокарты. Перфокарты представляли собой картонные карточки, в которых необходимо было проделать в нужных местах отверстия, чтобы задать требуемые инструкции.
Языки низкого уровня.
Машинный код
На заре компьютерной эры для программирования компьютеров использовался машинный код. Он представлял собой прямой набор инструкций процессора, а также аргументы для той или иной инструкции. Приведем пример программы для вывода надписи "Hello, world!" на машинном коде.
b8 21 0a 00 00 #moving "!\n" into eax
a3 0c 10 00 06 #moving eax into first memory location
b8 6f 72 6c 64 #moving "orld" into eax
a3 08 10 00 06 #moving eax into next memory location
b8 6f 2c 20 57 #moving "o, W" into eax
a3 04 10 00 06 #moving eax into next memory location
b8 48 65 6c 6c #moving "Hell" into eax
a3 00 10 00 06 #moving eax into next memory location
b9 00 10 00 06 #moving pointer to start of memory location into ecx
ba 10 00 00 00 #moving string size into edx
bb 01 00 00 00 #moving "stdout" number to ebx
b8 04 00 00 00 #moving "print out" syscall number to eax
cd 80 #calling the linux kernel to execute our print to stdout
b8 01 00 00 00 #moving "sys_exit" call number to eax
cd 80 #executing it via linux sys_call
Для того, чтобы писать в машинных кодах, требуется досконально знать внутреннее устройство компьютера и подробности работы центрального процессора. Также, различные центральные процессоры различных фирм имели свой набор команд, что приводило к тому, что программа в машинных кодах писалась под конкретную модель центрального процессора.
Язык ассемблера
Годом рождения языка ассемблера можно считать 1949 год. Язык ассемблера представляет собой систему обозначений, которая позволяет облегчить написание программ в машинном коде. Вместо кодов команд используются специальные обозначения (мнемоники), например "MOV" или "ADD". Также, можно было использовать различные системы счисления, а также давать меткам символические имена. Приведем пример программы, которая печатает на экране "Hello, world!" (диалект NASM Linux).
SECTION .data
msg db "Hello, world!",0xa
len equ $ - msg
SECTION .text
global _start
_start: ; Точка входа в программу
mov eax, 4 ; 'write' системный вызов
mov ebx, 1
mov ecx, msg ; Указатель на данные
mov edx, len ; Количество данных
int 0x80 ; Вызов ядра
mov eax, 1 ; '_exit' системный вызов
mov ebx, 0 ; Возвращаем 0 (все хорошо)
int 0x80 ; Вызов ядра
Необходимо отметить, что инструкции на языке ассемблера один в один транслируются в инструкции в машинных кодах (по крайней мере, так было изначально). Язык ассемблера предоставляет более удобную форму записи инструкций для процессора.
Особенностью программирования в машинных кодах и на языке ассемблера было то, что процесс разработки таких программ был очень медленный, отладка и поиск ошибок занимало огромное количество времени, а сами программы представляли собой научные расчеты для военной или аэрокосмической отрасли.
Языки высокого уровня. Неструктурированные языки программирования
FORTRAN и неструктурированные языки программирования
С течением времени, компьютеры становились все более производительными и дешевыми. Компьютерами заинтересовались не только военные и ученые, а и различные коммерческие фирмы, возникла потребность в разработке все более сложных программ, которые занимались не только научными расчетами. Существующие языки (ассемблер и язык машинных команд) не позволяли писать большие и сложные программы, к тому же, стал вопрос об удешевлении стоимости написания программ для бизнеса.
Все это сформировало потребность в развитии Computer Science и появлении принципиально новых языков программирования, которые бы позволили писать сложные программы и тратить меньше времени и денег на их производство, внедрение и поддержку.
Адмирал Грейс Хоппер, разработчик одного из высокоуровневых языков программирования, так описывала необходимость в новых инструментах для разработки компьютерных программ:
Когда-то я была профессором математики. В то время я обнаружила, что есть студенты, которые не могут изучать математику. Затем мне поручили сделать так, чтобы предпринимателям было легко пользоваться нашими компьютерами. Оказалось, что вопрос не в том, смогут ли они изучать математику, а в том, захотят ли они. [ ... ] Многие из них говорили: "Выбросьте эти символы — я не знаю, что они означают, у меня нет времени их изучать’ А тем, кто заявляет, что люди обрабатывающие данные, должны использовать математическую нотацию, я посоветую для начала обучить математической записи вице-президента или полковника или адмирала. Уверяю вас, я уже пробовала.
В конце 1953 года, сотрудник IBM Джон Бэкус предложил более практичную альтернативу языку ассемблера для программирования компьютера IBM 704, которая называлась FORTRAN. Первый компилятор для языка FORTRAN был разработан в апреле 1957 года.
Язык FORTRAN стал невероятно популярным языком высокого уровня и позволил существенно повысить эффективность разработки, внедрения и поддержки компьютерных программ.
Концепция языков высокого уровня позволяет существенно упростить и ускорить процесс разработки программного обеспечения. Основная черта высокоуровневых языков — это абстракция, то есть введение смысловых конструкций, кратко описывающих такие структуры данных и операции над ними, описания которых на машинном коде (или другом низкоуровневом языке программирования) очень длинны и сложны для понимания.
Недостатки неструктурированного программирования
Для того, чтобы объяснить один из главных недостатков языков программирования того времени, приведем пример программы на языке FORTRAN
C AREA OF A TRIANGLE WITH A STANDARD SQUARE ROOT FUNCTION
C INPUT - TAPE READER UNIT 5, INTEGER INPUT
C OUTPUT - LINE PRINTER UNIT 6, REAL OUTPUT
C INPUT ERROR DISPLAY ERROR OUTPUT CODE 1 IN JOB CONTROL LISTING
C READ INPUT TAPE 5, 501, IA, IB, IC
read(*,*) IA, IB, IC
C 501 FORMAT (3I5)
C IA, IB, AND IC MAY NOT BE NEGATIVE OR ZERO
C FURTHERMORE, THE SUM OF TWO SIDES OF A TRIANGLE
C MUST BE GREATER THAN THE THIRD SIDE, SO WE CHECK FOR THAT, TOO
IF (IA) 777, 777, 701
701 IF (IB) 777, 777, 702
702 IF (IC) 777, 777, 703
703 IF (IA+IB-IC) 777, 777, 704
704 IF (IA+IC-IB) 777, 777, 705
705 IF (IB+IC-IA) 777, 777, 799
777 STOP 1
C USING HERON'S FORMULA WE CALCULATE THE
C AREA OF THE TRIANGLE
799 S = FLOAT(IA + IB + IC) / 2.0
AREA = SQRT( S * (S - FLOAT(IA)) * (S - FLOAT(IB)) *
+ (S - FLOAT(IC)))
C WRITE OUTPUT TAPE 6, 601, IA, IB, IC, AREA
write(*, 601) IA, IB, IC, AREA
601 FORMAT (4H A= ,I5,5H B= ,I5,5H C= ,I5,8H AREA= ,F10.2,
+ 13H SQUARE UNITS)
END
Обратите внимание на строку
701 IF (IB) 777, 777, 702
В этой строке проверяется переменная IB. Что означает запись 777,777,702 ? Если переменная меньше или равна 0, то выполняется строка с меткой 777 - программа останавливается с кодом ошибки 1. Если переменная IB больше 0, то выполняется строка с меткой 702 - происходит проверка переменной IC.
Программы того времени представляли собой большое полотно кода, который был плохо структурирован, а ветвления и циклы реализовывались с помощью переходов на определенную строку или метку.
По мере увеличения количества строк кода, такой способ написания программ приводил к такому явлению как "спагетти-код" (spaghetti code) - плохо структурированная и трудная для понимания программа.
Спагетти-код назван так, потому что ход выполнения программы похож на миску спагетти, то есть извилистый и запутанный. Иногда называется «кенгуру-код» (kangaroo code) из-за множества инструкций «jump».
Инструкции перехода jump повсеместно применяются в машинном языке и в языке ассемблера, но в языках высокого уровня их использование приводило к тому, что когда программа разрасталась до определенного размера, ее модификация, поддержка и отладка становилась очень дорогой и медленной.
Для примера, сравните две программы для печати числе от 1 до 10 и их квадратов, реализованных на неструктурированном языке BASIC и в код в стиле структурного программирования
10 i = 0
20 i = i + 1
30 if i <= 10 then goto 70
40 if i > 10 then goto 50
50 print "Программа завершена."
60 end
70 print i; " в квадрате = "; i * i
80 goto 20
for i = 1 to 10
print i; " в квадрате = "; i * i
next i
print "Программа завершена."
Безусловно, неструктурированные языки были огромным шагом вперед по сравнению с языками низкого уровня, но они лишь на время отсрочили необходимость создания более совершенных языков и парадигм программирования, которые смогли бы открыть путь к безболезненной разработке более сложных программ.
Software crisis и структурные языки программирования
Период середины с середины 60-х сегодня известен как "кризис программного обеспечения".
Термин software появился в 1958 году, а уже через 10 лет начали говорить о кризисе программного обеспечения — это словосочетание впервые прозвучало в 1968 году в докладе Питера Наура и Брайана Рэнделла «Программная инженерия», зачитанном на конференции Научного комитета НАТО.
На протяжении десятков лет появился целый ряд публикаций, посвященных Программному кризису 1.0. По оценке Пера Флаттена и его коллег, приведенной в докладе от 1989 года, в среднем на осуществление проекта разработки ПО уходило по 18 месяцев. Это консервативная оценка, если учесть, что в 1988 году авторы статьи в Business Week этот показатель назвали равным трем годам, а в 1982-м аналитики указывали, что на программные проекты уходит по пять лет.
В 1994 году в отчете об исследовании, проведенном специалистами IBM, утверждалось. что 68% всех программных проектов отстают от графика. Там же приводились сведения о том, что превышение бюджетов проектов разработки достигает 65%. Для обозначения программных систем, выпущенных, но неиспользуемых, даже придумали термин shelfware — «ПО на полку».
Эксперты компании Standish Group в докладе CHAOS Manifesto, выпущенном в 2011 году, сетуют на высокую долю провальных проектов, правда, применяемые ими методологию анализа и выводы подвергали сомнению. Программный кризис 1.0, похоже, уже прошел, и благодаря многочисленным инкрементальным усовершенствованиям процесса разработки ПО ситуация все-таки изменилась к лучшему. Практические перемены в конечном итоге привели к тому, что сегодня ПО, как правило, разрабатывается в пределах бюджета и отвечает техническим требованиям. К сожалению, теперь надвигается кризис 2.0, и, как показано на рисунке, его первопричина — неспособность создания ПО, эффективно использующего колоссальные объемы данных, которые появились за последние 50 лет, а также учитывающего возросшие технические характеристики устройств и требования их пользователей.
Чтобы продемонстрировать рост вычислительных мощностей на протяжении 60х годов, приведем один пример. Мейнфрейм IBM 1401, выпущенный в 1959 году, выполнял до 193 300 операций сложения в минуту. Модель IBM 360/91, выпущенная в 1968 году, выполняла до 16 600 000 операций сложения в секунду!
Приведем отрывок из выступления Эдсгера Дейкстры на вручении Премии Тьюринга в 1972 году.
Основная причина кризиса программного обеспечения заключается в том, что компьютеры стали на несколько порядков мощнее! Проще говоря: пока не было машин, программирование вообще не было проблемой; когда у нас было несколько слабых компьютеров, программирование стало небольшой проблемой, а теперь, когда у нас появились гигантские компьютеры, программирование стало столь же гигантской проблемой.
В соответствии с парадигмой, любая программа, которая строится без использования оператора go to, состоит из трёх базовых управляющих конструкций: последовательность, ветвление, цикл; кроме того, используются подпрограммы. При этом разработка программы ведётся пошагово, методом «сверху вниз».
Методология структурного программирования появилась как следствие возрастания сложности решаемых на компьютерах задач, и соответственно, усложнения программного обеспечения. В 1970-е годы объёмы и сложность программ достигли такого уровня, что традиционная (неструктурированная) разработка программ перестала удовлетворять потребностям практики. Программы становились слишком сложными, чтобы их можно было нормально сопровождать. Поэтому потребовалась систематизация процесса разработки и структуры программ.
Методология структурной разработки программного обеспечения была признана «самой сильной формализацией 70-х годов».
Структурное программирование основано на принципе "вызова процедуры", что является еще одним названием "вызова функции". Процедуры также называют функциями, подпрограммами или методами. Процедура содержит последовательность исполняемых инструкций. Любая процедура может быть вызвана в любое время выполнения программа. Процедура может быть вызвана из другой процедуры или даже может вызывать саму себя.
Изначально, все процедуры были доступны в любой части программы, также как и глобальные данные. В небольших программах это не представляло никаких проблем, но чем сложнее и больше становится программа, тем чаще небольшие изменения в одной части программы могли существенно повлиять на другие части. Сложные и большие программы порождали огромное количество запутанных зависимостей и малейшее изменение в одной процедуре могло привести к каскаду ошибок во многих других процедурах, которые зависели от изменяемой процедуры.
Структурное vs объектно-ориентированное программирование
Дискуссии о том, какое программирование "лучше" не имеют смысла. Программирование, в том или ином виде, заключается в решении задачи, вы можете решить любую задачу с помощью любой парадигмы программирования, тем более, что многие языки программирования позволяют писать код в нескольких парадигмах.
Однако, не все парадигмы позволяют решать определенные задачи в равной степени эффективно. Таким образом, дискуссии насчет парадигмы программирования не имеют смысла, до тех пор, пока не определена задача, которую вы пытаетесь решить. Как только вы определили задачу и ее параметры, вы сможете понять, какая парадигма лучше подходит для ее решения.
Сложность программного обеспечения
Одним из основных вызовов при разработке программного обеспечения является сложность программ. Этот вызов иногда называют «кризисом программного обеспечения».
Не всё программное обеспечение является сложным, существует большое количество «простых» программ, которые разрабатываются и поддерживаются одним человеком. Такие программы, как правило, имеют очень ограниченный функционал и используются в течение короткого периода времени. С написанием таких программ не возникает много проблем, поэтому для их написания вы можете использовать практически любые языки программирования, технологии и методы разработки программных продуктов.
Наибольшие проблемы возникают при разработке промышленного программного обеспечения. Такие программы используются довольно долго (годами и десятилетиями), и от их корректной работы зависят тысячи и даже миллионы людей. Это могут быть, например, системы управления воздушным транспортом, железнодорожными перевозками, банковские системы, системы коммунальных платежей, онлайн-игры, популярные веб-сайты и веб-службы и так далее.
Важнейшей особенностью промышленной программы является ее высокая сложность. Одному программисту не под силу решить все проблемы, связанные с проектированием такой системы. Грубо говоря, сложность промышленных программ превышает интеллектуальные возможности отдельного человека.
Со времени возникновения области разработки программного обеспечения, человечество накопило достаточно знаний, чтобы проектировать даже самые сложные системы программного обеспечения, но мы до сих пор сталкиваемся с огромным количеством проблем. В чем же дело?
При анализе сложных систем, мы обнаруживаем много составных частей, которые взаимодействуют друг с другом разными довольно запутанными способами, причем части и способы их взаимодействия могут быть совершенно разными. При проектировании и организации сложных систем разработчику необходимо думать сразу о многом. Например, система управления воздушным транспортом должны одновременно контролировать состояние многих самолетов, учитывая, например, их местоположение, скорость и курс. К сожалению, один человек не может охватить все эти детали одновременно.
Таким образом, у нас возникает проблема сложность – программное обеспечение становится всё более сложным, а способности справиться с этой сложностью остаются ограниченными. Как же решить эту проблему?
Декомпозиция программных систем
Одним из способов справиться со сложностью программных систем, это декомпозиция. При проектировании сложного программного обеспечения, необходимо разделять его на всё меньшие и меньшие части, каждую из которых можно обрабатывать независимо друг от друга. Таким образом, вместо работы над всей программной системой сразу, мы будем работать с ее отдельными частями.
Одним из методов декомпозиции является алгоритмическая декомпозиция. Вы сталкивались с таким видом декомпозиции на первом курсе, когда изучали дисциплину «Алгоритмизация и программирование». Алгоритмическая декомпозиция выполняется методом «сверху вниз», где каждый модуль системы выполняет один из этапов общего процесса. На рисунке 1.1 приведена часть программы, которая обновляет содержимое основного файла.
Другим видом декомпозиции называется объектно-ориентированная декомпозиция, которая вам пока неизвестна.
При использовании этого вида декомпозиции, вместо разделения системы на этапы, например, «Прочитать отформатированное обновление» и «Добавить контрольную сумму», мы определяем такие объекты, как «Основной файл» и «Контрольная сумма», которые создаются при анализе предметной области. Ниже приведен пример ОО-декомпозиции для той же части программы.
В случае ОО-декомпозиции, мир представляет собой совокупность автономных агентов, которые взаимодействуют друг с другом и обеспечивают более сложное поведение системы. Действие «Прочитать отформатированное обновление» больше не является независимым алгоритмом, это действие представляет собой операцию, связанную с объектом «Файл обновлений». В результате выполнения этой операции возникает другой объект – «Обновление карты». Таким образом, каждый объект в такой схеме реализует свое собственное поведение, и каждый из них моделирует некоторый объект реального мира. С такой точки зрения объект является материальной сущностью, обладающей определенным поведением. Получая сообщения, объекты выполняют определенные операции. Такая композиция основана на объектах, поэтому и называется объектно-ориентированной.
Так какой же метод декомпозиции следует использовать? Использовать оба метода одновременно нельзя – сначала следует произвести декомпозицию либо по алгоритмам, либо по объектам.
Многолетний опыт разработчиков программного обеспечения явно показывает, что объектно-ориентированная декомпозиция имеет много чрезвычайно важных преимуществ над алгоритмической. Декомпозицию следует начинать с объектов, поскольку она облегчает упорядочение сложных систем, таких как программное обеспечение, компьютеры, растения, галактики и крупные общественные институты. Преимущества объектно-ориентированной декомпозиции:
уменьшается размер систем за счет повторного использования общих механизмов;
объектно-ориентированные системы являются более гибкими и легче эволюционируют со временем;
снижается риск, возникающий при создании сложной программной системы;
ОО-декомпозиция позволяет лучше справиться со сложностью, характерной для систем программного обеспечения.
Объектно-ориентированная технология основана на использовании так называемой объектной модели проектирования, или просто объектной модели. К основным принципам этой модели относятся абстракция, инкапсуляция, модульность, иерархия, контрольтипов, параллелизм и персистентность.
Языки, которые реализуют объектную модель, называют объектными или объектно-ориентированными. Пример структуры программ, написанных на объектно-ориентированных языках программирования, представлен на рисунке 1.3.
Основным элементом в этих языках является модуль, который представляет собой логическую связанную совокупность объектов и классов (понятие класса будет рассмотрено ниже). Такая структура является графом, а не деревом, как в случае использования алгоритмических языков. Кроме того, в ОО языках исключены глобальные данные. Данные и операции объединяются таким образом, что основными логическими конструктивными элементами ОО систем теперь являются объекты и классы, а не алгоритмы.
Объектная модель допускает масштабирование. В крупных системах образуются целые кластеры, образующие слои. Пример структуры крупных систем приведен на рисунке 1.4.
Объектно-ориентированное программирование
ООП было спроектировано для того, чтобы облегчить проектирование, поддержку и повторное использование кода. Ключевыми концепциями ООП являются инкапсуляция и абстракция, которые используются для помощи в разработки больших и сложных программ. ООП - это инструмент для создания программ в миллионы строк кода таким способом, чтобы программы можно было понимать, управлять и поддерживать.
Корректное использование ООП позволяет делать код проще, безопаснее и проще для понимания и дальнейшей модификации. ООП также помогает повторно использовать функционал в другой части программы или даже в другой программе без серьезной модификации. Это помогает снизить повторное написание одного и того же кода и работу программиста в целом.
ООП также помогает делать код более стабильным и снижает количество багов, потому что когда часть кода корректно протестирована, вы можете быть уверен, что код не приведет к ошибкам при использовании его в другой части программы.
Лекция 3
Тема: Понятие инкапсуляции. Интерфейс и реализация. Инкапсуляция в Java. Геттеры и сеттеры. Инкапсуляция при проектировании классов.
Одним из основных преимуществ использования объектов заключается в том, что объекту не нужно показывать все свои атрибуты и поведения. При хорошем объектно-ориентированном проектировании, объект должен показывать только интерфейсы, необходимые другим объектам для взаимодействия с ним. Детали, не относящиеся к использованию объекта, должны быть скрыты от всех других объектов.
Инкапсуляция определяется тем, что объекты содержат как атрибуты, так и поведения. Скрытие данных является одной из основных частей инкапсуляции. Например, объект, который применяется для вычисления квадратов чисел, должен обеспечивать интерфейс для получения результатов. Однако внутренние атрибуты и алгоритмы, используемые для вычисления квадратов чисел, не нужно делать доступными для запрашивающего объекта.
Инкапсуляция
Инкапсуляция – один из основополагающих принципов ООП. Инкапсуляция – это одна из причин, почему так широко используется ООП.
Инкапсуляцию можно считать защитной оболочкой, которая предохраняет код и данные от произвольного доступа со стороны другого кода, находящегося снаружи оболочки. Доступ к коду и данным, находящимся внутри оболочки, строго контролируется тщательно определенным интерфейсом (набором общедоступных, публичных методов).
Инкапсуляция - основной принцип ООП, позволяющий пользователю не задумываться о сложности используемого программного компонента (что у него внутри?), а взаимодействовать с ним посредством предоставляемого интерфейса (публичных методов и членов), а также объединить и защитить жизненно важные для компонента данные. При этом пользователю предоставляется только спецификация (интерфейс) объекта. Пользователь может взаимодействовать с объектом только через этот интерфейс.
Программистов можно разделить на создателей классов (те, кто создает новые типы данных) и на потребителей классов (они используют уже кем-то ранее созданные классы для своих целей).
Цель потребителей классов – как можно быстрее написать программу, используя уже кем-то ранее созданные классы (как кубики в конструкторе).
Цель создателей классов – построить класс, открывающий только то, что необходимо программисту-клиенту, и скрывающий все остальное.
Почему так? Программист-клиент не сможет получить доступ к скрытым частям, а значит, создатель классов оставляет за собой возможность произвольно их изменять, не опасаясь, что это кому-то повредит. Скрытая часть обычно и самая «хрупкая» часть объекта, которую легко можно испортить неосторожный или несведущий программист-клиент, поэтому сокрытие сокращает количество ошибок в программах.
Создавая классы, вы устанавливаете отношения с программистом-клиентом. Если предоставить доступ ко всем членам класса кому угодно, программист-клиент сможет сделать с классом и нарушить логику его работы. Таким образом, первой причиной ограничения доступа является необходимость уберечь «хрупкие» детали от программиста-клиента – части внутренней кухни, не являющиеся составляющими интерфейса, при помощи которого пользователи решают свои задачи. На самом деле это полезно и пользователям – они сразу увидят, что для них важно, а на что можно не обращать внимания.
Вторая причина ограничения доступа – стремление позволить разработчику классов изменять внутренние механизмы класса, не беспокоясь о том, как это отразится на программисте-клиенте. Например, вы можете реализовать класс «на скорую руку», а затем переписать его, чтобы повысить скорость работы. При правильном разделении скрытой и открытой части, сделать это будет совсем несложно.
Никакая часть сложной системы не должна зависеть от внутреннего устройства какой-либо другой части.
Разумная инкапсуляция должна локализовать проектные решения, которые могут измениться. По мере эволюции системы, ее разработчики могут обнаружить, что какие-то операции выполняются недопустимо долго, а какие-то объекты занимают слишком много памяти. В таких ситуациях внутреннее представление объекта, как правило, изменяется, чтобы реализовать более эффективные алгоритмы или оптимизировать использование памяти, заменяя хранение данных их вычислением.
Инкапсуляция – это процесс отделения друг от друга элементов объекта, определяющих его устройство и поведения; инкапсуляция служит для того, чтобы изолировать контрактные обязательства от их реализации.
Интерфейс и реализация
В любом классе присутствуют две части: интерфейс и реализация.
Интерфейс отражает внешнее поведение объектов этого класса. Внутренняя реализация описывает представления и механизмы достижения желаемого поведения объекта.
В интерфейсе собрано все, что касается взаимодействия данного объекта с другими объектами, а реализация скрывает от других объектов все детали, не имеющие отношения к процессу взаимодействия объектов.
Интерфейс определяет основные средства коммуникации между объектами. При проектировании любого класса предусматриваются интерфейсы для надлежащего создания экземпляров и эксплуатации объектов. Любое поведение, которое обеспечивается объектом, должно вызываться через сообщение, отправляемое с использованием одного из представленных интерфейсов (то есть, любое поведение объекта реализуется через вызов публичного метода).
В случае с интерфейсом должно предусматриваться полное описание того, как пользователи соответствующего класса будут взаимодействовать с этим классом.
Для того чтобы сокрытие данных произошло, все атрибуты должны быть объявлены как private. Поэтому атрибуты никогда не являются частью интерфейсов. Частью интерфейсов классов могут быть только публичные методы. Объявление атрибута как public нарушает концепцию сокрытия данных.
Инкапсуляция позволяет локализовать части реализации системы, которые могут подвергнуться изменениям. По мере развития программы, разработчики могут принять решение изменить внутреннее устройство тех или иных объектов с целью улучшения производительности или экономии памяти. Но интерфейс будет нетронутым и позволит другим объектам таким же способом взаимодействовать с этим объектом. (Пример автомобиля – педали, руль, приборная панель и внутренняя начинка).
Инкапсуляция в Java
Инкапсуляция в Java реализована с помощью использования модификаторов доступа.
Язык Java предоставляет несколько уровней защиты, которые позволяет настраивать область видимости данных и методов. В Java имеется четыре категории видимости элементов класса:
private– члены класса доступны только членам данного класса. Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию;
по умолчанию (package-private) – члены класса доступны классам, которые находятся в этом же пакете;
protected– члены класса доступны классам, находящимся в том же пакете, и подклассам – в других пакетах;
public– члены класса доступны для всех классов в этом и других пакетах.
Модификатор класса указывается перед остальной частью описания типа отдельного члена класса. Это означает, что именно с него должен начинаться оператор объявления класса.
public String errMessage;
private AccountBalance balance;
private boolean isError(byte status) {}
public class Account {}
Когда член класса обозначается модификатором доступа public, он становится доступным для любого другого кода в программе, включая и методы, определенные в других классах.
Когда член класса обозначается модификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену класса.
При отсутствии модификатора доступа, члены класса доступны другим членам класса, который находится в этом же пакете.
Модификатор доступа protected связан с использованием механизма наследования и будет рассмотрен позже.
Модификатор доступа указывается перед остальной частью описания типа отдельного члена класса (то есть, именно с модификатора доступа начинается объявление члена класса).
Член класса (переменная, конструктор, методы), объявленный public, доступен из любого метода вне класса.
Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию.
Доступ к полям через геттеры и сеттеры.
В подавляющем большинстве случаев, поля класса объявляются как private (это не касается статических переменных и констант, там ситуация может быть другая). Должны быть веские основания объявить поле класса общедоступным. Манипулирование данными должно осуществляться только с помощью методов.
Для того чтобы дать возможность получить доступ к переменной или дать возможность изменить ее значение, объявляют специальные методы, которые называются "геттерами" и "сеттерами".
Геттер возвращает значение приватного поля, тогда как сеттер меняет значение приватного поля (новое значение передается в качестве аргумента метода).
Хотя сигнатура и имена геттеров и сеттеров могут быть любыми, приучите себя соблюдать строгий шаблон для объявления геттеров и сеттеров.
Геттер должен иметь префикс get, после которого идет название поля с большой буквы. Геттер, как правило, не имеет входных аргументов.
Сеттер должен иметь префикс set, после которого идет название поля с большой буквы. Сеттер принимает на вход новое значение поля. Возвращаемый тип, как правило, void.
Account.java
public class Account {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
Большинство IDE для Java имеют механизм для генерации геттеров и сеттеров. В IntelliJ IDEA нажмите комбинацию Alt+Insert находясь в окне редактирования java-файла. Откроется контекстное меню Generate, где вы можете выбрать генерацию геттера и сеттера, после чего указать поля, для которых необходимо сгенерировать методы.
Инкапсуляция и сокрытие данных
Начинающие программисты часто путают понятия инкапсуляции и сокрытия данных.
Сокрытие данных означает защиту членов класса от нелегального или неавторизованного доступа.
Таким образом, мы можем сказать, что инкапсуляция автоматически реализует концепцию сокрытия данных, обеспечивая безопасность данных, делая переменную приватной.
Ключевые отличия между сокрытием данных и инкапсуляцией:
инкапсуляция помогает скрыть сложность программы. С другой стороны, сокрытие данных связано с безопасностью данных в программе;
инкапсуляция фокусируется на упаковке (помещении в капсулу) сложных данных, чтобы предоставить пользователю более простое представление объекта. С другой стороны, сокрытие данных направлено на ограничение использования данных с целью обеспечения безопасности данных;
при инкапсуляции данные могут быть общедоступными или приватными, но при сокрытии данных, данные должны быть только приватными;
сокрытие данных - это процесс, а также техника, тогда как инкапсуляция - это подпроцесс при сокрытии данных.
Пример использования инкапсуляции
Представим, что нам необходимо создать класс «Корзина» (Cart), который хранит в себе набор объектов класса «Товар» (Item).
Какие методы «Корзина» должна предоставлять для внешнего использования? Это могут быть, например, методы «Добавить товар», «Убрать последний добавленный товар», «Подсчет суммы цен товаров в корзине», «Повышение цен в корзине на N процентов» и «Снижение цен в корзине на N процентов».
Название метода
Описание
public Cart(int capacity)
Конструктор с 1 параметром – максимальным количеством товаров в корзине.
public boolean addItem(Item item)
Добавление товара в корзину. Возвращает успешность операции.
public Item deleteLastAddedItem()
Удаление последнего добавленного товара в корзину. Возвращает удаленный товар.
public double calculateItemPrices()
Подсчет суммы цен всех товаров в корзине.
public void raiseItemPrices(double percent)
Поднять цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).
public void cutItemPrices(double percent)
Снизить цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).
Как вы можете заметить, это публичные методы, а значит, их можно вызвать через оператор-точку имея ссылку ну объект.
Cart cart = new Cart();
cart.addItem(new Item("Клавиатура", 2000));
Перечень этих публичных методов и составляет интерфейс класса – то есть, с помощью этих методов объект класса будет взаимодействовать с внешним миром.
Эти методы имеют вполне четко определенные входные аргументы и могут возвращать значения четко определенных типов, и никак иначе. По аналогии с этим, поворот колес автомобиля осуществляется четко определенным образом – поворотом руля, и бензин надо заливать в четко определенное отверстие крышки бензобака, а не как-то еще.
То – как будет реализовано хранение товаров в корзине – это внутренняя логика класса и она не должна быть доступна внешнему миру, она должна быть скрыта от внешнего вмешательства. Другие классы, которые будут использовать объекты класса Cartне должны знать и не должны иметь доступ к тому – как там «внутри» реализовано хранение товаров, подсчет цен и изменение цены на определенный процент и так далее, они могут только лишь использовать предоставленные им публичные методы. Давайте реализуем «Корзину» с помощью структуры «стек», которая, в свою очередь, реализована обычным массивом.
Cart.java
public class Cart {
private Item[] stack; // массив для реализации стека
private int topIndex; // указатель на вершину стека
// При создании корзины мы должны
// указать максимальное количество элементов
// в корзине
public Cart(int capacity) {
stack = new Item[capacity];
topIndex = -1;
}
// Добавление нового товара в корзину
public boolean addItem(Item item) {
return push(item);
}
// Приватный метод, который реализует добавление в стек
private boolean push (Item item) {
// Добавляем товар в стек
return true; // или false если не стек переполнен
}
// Удаление последнего добавленного товара в корзину
public Item deleteLastAddedItem() {
return pop();
}
// Приватный метод, который реализует извлечение из стека
private Item pop() {
return new Item(); // Извлеченный из стека товар
}
}
Как мы видим, массив с товарами, указать на вершину стек объявлены как privateчлены класса. Это значит, что мы не можем получить к ним доступ извне – они доступны только внутри данного класса.
Программиста, который будет использовать класс Cart, не должна волновать ситуация с переполнением стека, с попыткой извлечь элемент из пустого стека, он не должен следить за указателем на вершину стека, он даже не должен знать что это стек.
Для него объект класса Cart это некоторый объект, который предоставляет «услугу» в виде корзины товаров и с этой корзиной можно работать с помощью определенных публичных методов.
В дальнейшем мы можем переделать класс Cart и поменять внутреннюю реализацию. Мы можем использовать структуру "очередь", мы можем использовать коллекции, мы можем иначе реализовать операции добавления и удаления элемента в стеке, но если мы сохраним интерфейс класса неизменным, то для внешнего мира эти изменения внутренней логики не будут важны и если мы поменяем внутреннюю логику одного небольшого участка программы, то вся остальная программа будет работать так же.
Инкапсуляция как инструмент защиты инварианта класса
Еще одно важное свойство инкапсуляции - защита инварианта класса.
Что такое инвариант класса? Простыми словами- это утверждение, которое (должно быть) истинно применительно к любому объекту данного класса в любой момент времени (за исключением переходных процессов в методах объекта).
Например, во второй лабораторной работе инвариантом класса TimeSpan должны быть следующие утверждения:
значение hours должно быть 0 или больше;
значение minutes должно быть от 0 до 59 включительно.
Без механизма инкапсуляции мы никак не может обеспечить инвариант класса, потому что любой сторонний код может получить доступ к полю объекта и установить любое значение.
public class Main {
public static void main(String[] args) {
TimeSpan span = new TimeSpan();
span.minutes = 100;
span.hours = -200;
}
}
class TimeSpan {
int hours;
int minutes;
}
Если мы предусмотрим геттеры и сеттеры, это, само по себе, не защитит инвариант класса.
public class Main {
public static void main(String[] args) {
TimeSpan span = new TimeSpan();
span.setMinutes(100);
span.setHours(-200);
}
}
class TimeSpan {
private int hours;
private int minutes;
public int getHours() {
return hours;
}
public void setHours(int hours) {
this.hours = hours;
}
public int getMinutes() {
return minutes;
}
public void setMinutes(int minutes) {
this.minutes = minutes;
}
}
Мы можем обеспечить инвариант класса с помощью проверках в методах setHours() и setMinutes(), чтобы не допустить его нарушения.
В данном случае, грамотным вариантом было бы создание метода setTimeSpan(), в котором мы можем защитить инварианты класса.
public class Main {
public static void main(String[] args) {
TimeSpan span = new TimeSpan();
span.setTimeSpan(1,40);
}
}
class TimeSpan {
private int hours;
private int minutes;
public int getHours() {
return hours;
}
public int getMinutes() {
return minutes;
}
public void setTimeSpan(int hours, int minutes) {
if (hours < 0 || minutes < 0) {
throw new IllegalArgumentException("Hours and(-or) minutes should be zero or bigger!");
}
this.hours = hours + (minutes / 60);
this.minutes = minutes % 60;
}
}
Разберем еще один пример - рассмотрим класс TimeInterval.
class TimeInterval {
Date start;
Date end;
}
Инвариантом класса TimeInterval будет то, что дата start не может быть хронологически позже даты end, то есть начало интервала не может быть позже по времени, чем его окончание.
Даже если мы предусмотрим геттер и сеттер, их наличие не будет автоматически значить наличие инкапсуляции.
TimeInterval.java
class TimeInterval {
private Date start;
private Date end;
public TimeInterval(Date start, Date end) {
this.start = start;
this.end = end;
}
public Date getStart() {
return start;
}
public void setStart(Date start) {
this.start = start;
}
public Date getEnd() {
return end;
}
public void setEnd(Date end) {
this.end = end;
}
}
То есть, любой пользователь может воспользоваться сеттером и установить значения start и end таким образом, что start будет по времени позже чем end.
Грамотным вариантом было бы добавление публичного метода setInterval(), в котором бы осуществлялась проверка на соблюдение инварианта класса.
В нашем коде есть 4 точки, где инвариант может быть нарушен (два метода setInterval() и два конструктора). Обеспечив нужные проверки входных аргументов, мы можем гарантировать инвариант класса.
TimeInterval.java
class TimeInterval {
private Date start;
private Date end;
public TimeInterval(Date start, Date end) {
setInterval(start, end);
}
public TimeInterval(Date start, long durationInMillis) {
setInterval(start, durationInMillis);
}
public Date getStart() {
return start;
}
public void setInterval(Date start, Date end) {
if (start.after(end)) {
throw new IllegalArgumentException("Start date should earlier or equal to end date!");
}
this.start = start;
this.end = end;
}
public void setInterval(Date start, long durationInMillis) {
if (durationInMillis < 0) {
throw new IllegalArgumentException("duration in millis should be 0 or bigger!");
}
this.start = start;
this.end = new Date(start.getTime() + durationInMillis);
}
public Date getEnd() {
return end;
}
}
Инкапсуляция при проектировании классов
При сокрытии информации каждый класс (пакет, метод) характеризуется аспектами проектирования или конструирования, которое он скрывает от остальных классов. Секретом может быть источник вероятных изменений, формат файла, реализация типа данных или область, изоляция которой требуется для сведения к минимуму вреда от возможных ошибок. Класс должен скрывать эту информацию и защищать свое право на "личную жизнь". Небольшие изменения системы могут влиять на несколько методов класса, но не должны распространяться за его интерфейс.
Один из важнейших аспектов проектирования класса - принять решение о том, какие свойства сделать доступными вне класса, а какие оставить секретными.
Класс может включать 25 методов, предоставляя доступ только к пяти из них и используя остальные 20 внутренне. Класс может использовать несколько типов данных, не раскрывая сведений о них. Этот аспект проектирования классов называют "видимостью", так как он определяет, какие свойства класса "видимы" или "доступны" извне.
Интерфейс класса должен сообщать как можно меньше о внутренней работе класса. В этом смысле класс во многом похож на айсберг, большая часть которого скрыта под водой.
Как и любой другой аспект проектирования, разработка интерфейса класса - итеративный процесс. Если приемлемый интерфейс класса не удается создать с первого раза, сделайте еще несколько попыток, пока он не стабилизируется.
Приведем несколько правил использования инкапсуляции при проектировании классов:
Минимизируйте доступность классов и их членов.
Минимизация доступности - одно из нескольких правил, поддерживающих инкапсуляцию. Если вы не можете понять, каким делать конкретный метод: открытым, закрытым или защищенным - некоторые авторы советуют выбирать самый строгий уровень защиты, который работает.
Не делайте данные-члены открытыми.
Предоставление доступа в данным-членам нарушает инкапсуляцию. Например, класс Point (точка), который предоставляет доступ к данным:
class Point {
public float x;
public float y;
public float z;
}
нарушает инкапсуляцию, потому что клиентский код может свободно делать с данными Point что угодно, при этом сам класс может даже не узнать об их изменении. В то же время класс Point, включающий члены:
class Point {
private float x;
private float y;
private float z;
public void setX(float x) {
this.x = x;
}
public void setY(float y) {
this.y = y;
}
public void setZ(float z) {
this.z = z;
}
}
поддерживает прекрасную инкапсуляцию. Вы не имеете понятия о том, реализованы ли данные как floatx, y и z, хранит ли класс Point эти элементы как double, преобразуя их во float, или же он хранит их на Луне и получает через спутник.
Не включайте в интерфейс класса закрытые детали реализации.
Истинная инкапсуляция не позволяла бы узнать детали реализации вообще. Они были бы скрыты и в прямом, и в переносном смыслах.
Не делайте предположений о клиентах класса.
Класс следует спроектировать и реализовать так, чтобы он придерживался контракта, сформулированного посредством интерфейса. Выразив свои требования в интерфейсе, класс не должен делать предположений о том, как этот интерфейс будет или не будет использоваться.
Не делайте метод открытым лишь потому, что он использует только открытые методы.
То, что метод использует только открытые методы, не играет особой роли. Лучше спросите себя, согласуется ли предоставление доступа к данному методу с абстракцией, формируемой интерфейсом.
Лекция 4
Тема: Повторное использование кода. Композиция и наследование. Наследование в Java. Наследование членов суперкласса. Инициализация базового класса. Ключевое слово super. Запрет наследования с помощью ключевого слова final. Класс Object. Сравнение композиции и наследования.
Повторное использование кода
Возможность повторного использования кода принадлежит к числу важнейших преимуществ языков объектно-ориентированного программирования.
В Java, вместо того чтобы создавать новый класс "с чистого листа", вы берете за основу уже существующий класс, который кто-то уже создал и проверил на работоспособность. Существую два пути реализации этой идеи.
композиция (composition) - объекты уже имеющихся классов просто создаются внутри нового класса. Программист просто использует функциональность уже готового кода;
наследование (inheritance) - новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса.
Композиция - использование функционала одних объектов в составе других объектов. Рассмотрим пример класса FileManager, в котором определен метод для сохранения текстовых данных в файл. Класс Document использует функционал класса FileManager, чтобы сохранить текстовый документ на жесткий диск.
class FileManager {
public void saveToFile(String text, String path) {
// тело метода
}
}
class Document {
// класс Document содержит ссылку на объект
// класса FileManager
private FileManager manager;
private StringBuilder contents;
private String path;
public Document(FileManager manager, String path) {
this.manager = manager;
this.contents = new StringBuilder();
this.path = path;
}
public void saveDocument() {
manager.saveToFile(contents.toString(), path);
}
}
Базовые понятия механизма наследования
Наследование - отношение между классами, в котором один класс повторяет структуру и поведение другого класса (или нескольких других классов).
Реализуется наследование путем создания классов на основе уже существующих. При этом члены класса, на основе которого создается новый класс, с некоторыми оговорками, автоматически включается в новый класс. Кроме этого, в новый класс можно добавлять новые члены.
Класс, на основе которого создается новый класс, называется суперклассом (базовым классом, родительским классом). Новый создаваемый класс называется подклассом (дочерним классом, производным классом, классом-наследником и так далее).
Создание подкласса в Java
Создание подкласса практически не отличается от создания обычного класса, кроме необходимости указать суперкласс, на основе которого создается подкласс. В Java для этого существует ключевое слово extends:
// Суперкласс
class Person {
String firstName;
String lastName;
}
// Подкласс
class Student extends Person {
String group;
long id;
}
public class Main {
public static void main(String[] args) {
Student student = new Student();
student.firstName = "Иван";
student.lastName = "Иванов";
student.id = 10000L;
}
}
В Java, в отличие от C++, отсутствует множественное наследование, то есть подкласс может создаваться на основе только одного суперкласса.
// Суперкласс
class Person {
String firstName;
String lastName;
}
class UniversityMember{}
// МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ ЗАПРЕЩЕНО!
// ЭТОТ КОД ВЫЗОВЕТ ОШИБКУ КОМПИЛЯТОРА
class Student extends Person, UniversityMember {
String group;
long id;
}
В Java присутствует многоуровневое наследование: подкласс может быть суперклассом для другого класса. Благодаря этому можно создавать целые цепочки классов, связанные механизмом наследования
class Vehicle {
public void moveTo(Point destination) {
// тело метода
}
}
class Truck extends Vehicle {
public void carryWeight(double weight) {
// тело метода
}
}
class DumpTruck extends Truck {
public void dumpWeight() {
// тело метода
}
}
Наследование членов суперкласса
Если член класса определен как private, то при наследовании доступ к нему со стороны подкласса закрыт. Важно понимать, что приватный член суперкласса в подклассе есть, только он закрыт для прямого доступа. К примеру, данный код не скомпилируется
class Shape2D {
private double width;
private double height;
}
class Rectangle extends Shape2D {
// ОШИБКА НА ЭТАПЕ КОМПИЛЯЦИИ
public double getArea() {
return width * height;
}
}
Закрытыми могут быть как поля класса, так и его методы. Если необходимо открыть поля или методы для доступа к ним со стороны подкласса, при объявлении членов суперкласса используют слово protected либо создают геттеры и сеттеры для доступа к полям. К примеру, данный код скомпилируется и будет работать корректно
class Shape2D {
protected double width;
protected double height;
}
class Rectangle extends Shape2D {
// Данный код корректен
public double getArea() {
return width * height;
}
}
Данный пример демонстрирует доступ к полям с помощью геттеров
class Shape2D {
private double width;
private double height;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
}
class Rectangle extends Shape2D {
// Данный код корректен
public double getArea() {
return getWidth() * getHeight();
}
}
Инициализация базового класса
Так как в наследовании участвуют два класса, базовый и производный, не сразу понятно, какой же объект получится в результате. Внешне все выглядит так, словно новый класс имеет тот же интерфейс, что и базовый класс, плюс еще несколько дополнительных полей и методов.
Для упрощения можете представить, что когда вы создаете объект производного класса, внутри него якобы "содержится" объект базового класса. Этот объект "выглядит" точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса "упакован" объект базового класса.
Данная аналогия не соответствует тому, как наследование реализуется "внутри" языка Java, но пока будем мыслить таким образом.
Чтобы объект базового класса был правильно инициализирован, при вызове конструктора подкласса, сначала вызывается конструктор базового класса, у которого есть необходимые знания и привилегии для проведения инициализации базового класса.
При использовании конструкторов без параметров, у компилятора не возникает проблем с вызовом таких конструкторов, так как нет нужды передавать аргументы. В этом случае Java автоматически вставляет вызовы конструктора базового класса в конструктор производного класса.
class Animal {
public Animal() {
System.out.println("Конструктор класса Animal");
}
}
class Mammal extends Animal {
public Mammal() {
System.out.println("Конструктор класса Mammal");
}
}
class Cat extends Mammal {
public Cat() {
System.out.println("Конструктор класса Cat");
}
}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
}
}
Результат работы такого приложения будет следующим
Конструктор класса Animal
Конструктор класса Mammal
Конструктор класса Cat
Как видно из данного примера, цепочка вызовов конструкторов начинается с самого базового класса. Таким образом, подобъект базового класса инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса Cat не будет определен, Java сгенерирует конструктор по умолчанию, в котором также будет вызван конструктор базового класса.
Если в классе не определен конструктор без параметров, то вызов конструктора базового класса надо будет оформлять явно. К примеру, такой код вызовет ошибку на этапе компиляции
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100);
}
}
class Box {
public double width;
public double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
}
class Box3D extends Box {
public double depth;
// НЕТ ЯВНОГО ВЫЗОВА КОНСТРУКТОРА СУПЕРКЛАССА !
public Box3D(double depth) {
this.depth = depth;
}
}
Для явного вызова конструктора суперкласса используется ключевое слово super. Более подробно мы рассмотрим его ниже, а сейчас приведем пример корректного вызова конструктора суперкласса
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100, 200, 300);
}
}
class Box {
public double width;
public double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
}
class Box3D extends Box {
public double depth;
public Box3D(double width, double height, double depth) {
super(width, height); // <-- ВЫЗОВ КОНСТРУКТОРА СУПЕРКЛАССА
this.depth = depth;
}
}
Ключевое слово super
Как было сказано ранее, наследование в Java реализуется следующим образом - в объект производного класса добавляется скрытый объект базового класса, который и обеспечивает вызов методов суперкласса.
Ключевое слово super как раз и ссылается на этот скрытый объект суперкласса. Используя это ключевое слово, можно получить доступ к членам суперкласса (если позволяет их модификатор доступа)
class Box {
private double width;
private double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
// Площадь прямоугольника
public double getArea() {
return width * height;
}
}
class Box3D extends Box {
private double depth;
public Box3D(double width, double height, double depth) {
super(width, height); // <-- ВЫЗОВ КОНСТРУКТОРА СУПЕРКЛАССА
this.depth = depth;
}
// Мы используем метод суперкласса, чтобы
// посчитать площадь трехмерной коробки
public double get3DArea() {
double area2D = super.getArea(); // <---- Вызов метода суперкласса
return area2D * depth;
}
}
Как видно из примера, ключевое слово super имеет что-то общее с ключевым словом this.
Переопределение методов
При использовании механизма наследования возникает проблема с использованием методов суперкласса. Часто метод суперкласса не отражает изменения и нововведения, внесенные в подклассе и вызов таких методов дает некорректную информацию об объекте. Рассмотрим следующий пример
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.getInfo());
}
}
class Box {
private double width;
private double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
public String getInfo() {
return "Объект Box {" +
"ширина = " + width +
", высота = " + height +
'}';
}
}
class Box3D extends Box {
private double depth;
public Box3D(double width, double height, double depth) {
super(width, height);
this.depth = depth;
}
}
Результат работы такого приложения будет следующим
Объект Box {ширина = 100.0, высота = 200.0}
Как вы уже понимаете, в данной части кода
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.getInfo());
был вызван метод getInfo() суперкласса, который выводит информацию только о двух параметрах коробки, тогда как класс Box3D содержит три параметра. Таким образом, метод getInfo() для класса Box3D становится как бы некорректным, неправильным, он выдает неполную информацию об объекте. Как решить эту проблему?
Первый вариант - создать в подклассе Box3D свой метод для вывода информации
class Box {
private double width;
private double height;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Box(double width, double height) {
this.width = width;
this.height = height;
}
public String getInfo() {
return "Объект Box {" +
"ширина = " + width +
", высота = " + height +
'}';
}
}
Тогда мы будем вызывать метод get3DInfo()
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.get3DInfo());
}
}
что даст нам корректный результат
Объект Box3D {ширина = 100.0, высота = 200.0, глубина = 300.0}
Такой вариант является интуитивно понятным, но все-таки не совсем корректным. Теперь у объекта Box3D существует аж целых два метода для получения информации об объекте, один из которых является некорректным, что вносит путаницу для нас и для тех программистов, которые будут использовать наш код.
Самым правильным вариантом будет как бы "переписать" метод getInfo(), предоставить версию метода getInfo() для класса Box3D. Таким образом и класс Box и класс Box3D будет иметь метод getInfo(), просто в классе Box3D он будет иначе реализован, с учетом появления третьего параметра. Кроме того, объект класса Box3D теперь будет содержать только один метод для получения информации об объекте и этот метод будет корректно работать.
Для реализации своеобразного "переписывания" метода в подклассе, в Java существует механизм переопределения метода (method overriding).
Воспользуемся механизмом переопределения метода, чтобы корректно решить проблему с классами Box и Box3D (в примере для наглядности опущены некоторые члены классов)
public class Main {
public static void main(String[] args) {
Box box = new Box(600,600);
System.out.println(box.getInfo());
Box3D box3D = new Box3D(100, 200, 300);
System.out.println(box3D.getInfo());
}
}
class Box {
// Поля, конструктор и геттеры\сеттеры
public Box(double width, double height) {
this.width = width;
this.height = height;
}
public String getInfo() {
return "Объект Box {" +
"ширина = " + width +
", высота = " + height +
'}';
}
}
class Box3D extends Box {
// Поля, конструктор и геттеры\сеттеры
@Override
public String getInfo() {
return "Объект Box3D {" +
"ширина = " + super.getWidth() +
", высота = " + super.getHeight() +
", глубина = " + depth +
'}';
}
}
Обратите внимание что сигнатуры двух методов getInfo() полностью совпадают - это обязательное условие для срабатывания механизма переопределения метода. Метод в суперклассе называется переопределенным.
Также обратите внимание на строку 33, где записано @Override. Такая запись называется аннотацией.
Аннотация - это дополнительное пояснение для компилятора и для различных утилит, которые работают с кодом (анализаторы, генераторы документации и так далее). Указание аннотации не является обязательным, код будет работать и без аннотации, но указание аннотации является одним из важных правил грамотного написания кода.
Результат работы нашего примера будет следующим
Объект Box {ширина = 600.0, высота = 600.0}
Объект Box3D {ширина = 100.0, высота = 200.0, глубина = 300.0}
Обратите внимание, что и для класса Box и для класса Box3D мы вызываем метод с одним и тем же названием и с одной и той же сигнатурой
Box box = new Box(600, 600);
System.out.println(box.getInfo());
Box3D box3D = new Box3D(100, 200, 300);
System.out.println(box3D.getInfo());
Но в зависимости от того, для какого класса мы вызываем этот метод, в первом случае отрабатывает метод в классе Box, а во втором случае - метод в классе Box3D.
Следует отличать механизм перегрузки метода от механизма переопределения метода.
Метод может быть как перегруженным, так и переопределенным.
Запрет наследования с помощью ключевого слова final
Иногда бывает необходимо запретить наследоваться от какого-то класса либо запретить переопределять метод. В этом случае, в объявлении класса или метода укажите ключевое слово final
final class A {}
class B extends A {
// ВЫЗОВЕТ ОШИБКУ КОМПИЛЯЦИИ !
}
class C {
final public void foo() {}
}
class D extends C {
@Override
public void foo() {} // <-- Ошибка компиляции !
}
Класс Object
В Java определен специальный класс Object, который является суперклассом для всех классов Java. Иными словами, все классы в языке Java являются подклассами, производными от класса Object.
В классе Object определены перечисленные ниже методы, которые доступны в любом объекте
Метод
Описание
Object clone()
Создает новый объект, аналогичный клонируемому объекту
boolean equals(Object объект)
Определяет равнозначность объектов
void finalize()
Вызывается перед тем, как неиспользуемый объект будет удален "сборщиком мусора"
Class<?> getClass()
Определяет класс объекта во время выполнения
int hashCode()
Возвращает хеш-код, связанный с вызывающим объектом
void notify()
Возобновляет работу потока, ожидающего уведомления от вызывающего объекта
void notifyAll()
Возобновляет работу всех потоков, ожидающих уведомления от вызывающего объекта
String toString()
Возвращает символьную строку, описывающую объект
void wait()
void wait(long мсек)
void wait(long мсек, int наносек)
Ожидает исполнения другого потока
С некоторыми методами класса Object мы встретимся позже, а сейчас нам интересен только метод toString().
Метод toString()
Метод toString() призван возвращать строковое представление объекта (список значений полей). Рассмотрим пример
public class Main {
public static void main(String[] args) {
Box box = new Box();
System.out.println(box.toString());
}
}
class Box{}
Обратите внимание, что класс Box не содержит никаких членов, однако, так как Box наследуется от класса Object, ему доступны методы суперкласса и метод toString() в частности.
Результатом работы данного примера является следующая строка
com.company.Box@1540e19d
Метод toString() в классе Object выводит полное название класса и 16-ричное представление хеш-кода объекта.
Если вам необходимо вывести значения полей объекта в виде строки, используйте метод toString(), он для этого и был предназначен, его использование является общепринятым правилом.
Метод getInfo() в примере выше был использован только в демонстрационных целях!
Чтобы метод toString() выводил нужную информацию, его необходимо пепреопределить как в следующем примере
public class Main {
public static void main(String[] args) {
Box box = new Box(100, 200);
System.out.println(box.toString());
}
}
class Box {
private double width;
private double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public String toString() {
return "Box{" +
"width=" + width +
", height=" + height +
'}';
}
}
Результат будет следующим
Box{width=100.0, height=200.0}
Также метод toString() имеет одну примечательную особенность - его можно явно не вызывать, а просто указывать ссылочную переменную, Java сама вызовет метод toString(). Например, следующий код
public class Main {
public static void main(String[] args) {
Box box = new Box(100, 200);
System.out.println(box.toString());
System.out.println(box);
}
}
Как вы видите, результат работы строк 5 и 6 является идентичным.
Сравнение композиции и наследования
И композиция, и наследование позволяют вам помещать подобъекты внутрь вашего нового класса (при композиции это происходит явно, а в наследовании - опосредовано). В чем же разница между ними и когда следует выбирать одно, а когда другое?
Композиция в основном используется, когда в новом классе необходимо задействовать функциональность уже существующего класса, но не его интерфейс. То есть вы встраиваете объект, чтобы использовать его возможности в новом классе, а пользователь класса видит только определенный вами интерфейс, но не замечает встроенных объектов. Для этого внедряемые объекты объявляются со спецификатором private.
При использовании наследования, вы берете уже существующий класс и создаете eго специализированную версию. В основном это значит, что класс общего назначения адаптируется для конкретной задачи.
Лекция 2
Тема: Базовые понятия ООП. Создание объектов. Примитивные и ссылочные типы. Конструктор. Ключевое слово this.
Объектно-ориентированное программирование – это методология программирования, основанная на представлении программы в виде совокупности взаимодействующих объектов, каждый из которых является экземпляром определенного класса, а классы являются членами определенной иерархии наследования.
Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Фактически, объект является основным строительным блоком объектно-ориентированных программ.
Класс – шаблон для объектов. Каждый объект является экземпляром (instance) какого-либо класса («безклассовых» объектов не существует). В рамках класса задается общий шаблон, структура, на основании которой создаются объекты. Данные, относящиеся к классу, называются полями класса, а программный код для их обработки называется методами класса. Поля и методы иногда называют общим термином – члены класса.
Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом.
Объект состоит из следующих частей:
имя объекта;
состояние (переменные состояния). Данные, содержащиеся в объекте, представляют его состояние. В терминологии ООП эти данные называются атрибутами. Например, атрибутами работника могут быть: имя, фамилия, пол, дата рождения, номер телефона. В разных объектах атрибуты имеют разное значение. Фактически, в объектах определяются конкретные значения тех переменных (полей класса), которые были заявлены при описании класса;
методы (операции) – применяются для выполнения операций с данными, а также для совершения других действий. Методы определяют, как объект взаимодействует с окружающим миром.
Объекты могут отправлять друг другу сообщения. Сообщение (message) - это практически то же самое, что и вызов функции в обычном программировании. В ООП обычно употребляется выражение "послать сообщение" какому-либо объекту. Понятие "сообщение" в ООП можно объяснить с точки зрения ООП: мы не можем напрямую изменить состояние объекта и должны как бы послать сообщение объекту, что мы хотим как-то изменить его состояние. Очень важно понять, что объект сам меняет свое состояние, а мы можем только попросить его об этом с помощью отсылки сообщения.
В объектно-ориентированной программе весь код должен находиться внутри классов!
В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.
От языка С к объектно-ориентированному программированию
Давайте попробуем понять, что такое объект и класс с помощью языка программирования C.
Представим себе, что мы пишем программу для книжной лавки на языке C. В какой-то момент мы сталкиваемся с необходимостью хранить информацию о множестве книг: название книги, кто автор книги, год издания и стоимость книги. Как нам это запрограммировать?
Мы можем воспользоваться массивами и хранить данные о книгах в нескольких массивах.
int book_year[100];
int book_pages[100];
char book_author[100][50];
char book_title[100][100];
float book_price[100];
Теперь мы можем обратиться к i-му номеру каждого массива для получения информации об i-ой книге.
Какими недостатками обладает данный способ работы с данными? Такой подход может приводить к многочисленным ошибкам (например, ошибки при работе с индексами массивов), такие данные тяжело модифицировать (удаление книги приводит к необходимости смещать влево часть элементов массивов), такой код неудобно читать, поддерживать и модифицировать.
Но самое главное - мы мыслим в контексте структуры компьютера, а не решаемой задачи. Для нас книга - это некий единый объект, который имеет некоторые параметры (атрибуты): название, количество страниц и так далее. Мы же представляем атрибуты этого объекта в виде отдельных записей в разных массивах, потому что на языке C мы вынуждены мыслить в терминах имеющихся структур данных (массивов, очередей, деревьев), а не в терминах отдельных объектов и их взаимоотношений. Это затрудняет понимание решаемой задачи (управление книжной лавкой и продажей книг), увеличивает количество ошибок в программе и на некотором этапе мы вообще перестаем понимать, что происходит в программе.
Для хотя бы какого-то решения этой проблемы и облегчения труда программиста, на некотором этапе в язык C было введено понятие структуры (ключевое слово struct), которое вы должны были изучать в курсе "Алгоритмизация и программирование".
Структура в языке C - это тип данных, создаваемый программистом, предназначенный для объединения данных различных типов в единое целое. Таким образом, мы можем сгруппировать данные, которые относятся к одной книге, в одну структуру.
Сначала мы должны описать структуру (новый тип данных).
struct Book {
char author[50];
char title[100];
int year;
int pages;
float price;
};
Таким образом, некоторое понятие реального мира (то есть, книга вообще, как понятие) в программе описан структурой Book. Экземпляр этого понятия (какая-то конкретная книга) в программе будет представлен экземпляром структуры - переменной типа Book. Мы объявляем экземпляр структуры, после чего мы сможет заполнить поля структуры и работать с ней дальше в программе.
int main() {
// Объявляем экземпляр структуры
struct Book book1;
// Заполняем поля экземпляра
strcpy(book1.author,"Иванов А.А");
strcpy(book1.title,"Программирование на Java");
book1.pages = 255;
book1.price = 350.25;
book1.year = 2018;
// Теперь мы можем работать с созданным экземпляром структуры
printf("%s - %s, %d страниц", book1.author, book1.title, book1.pages);
return 0;
}
Использование структур решает проблему лишь частично. Мы сгруппировали данные, но функции для работы с нашей структурой находятся вне структуры.
Объект реального мира обладает не только атрибутами (автор книги, название книги, количество страниц и так далее), но и определенным поведением (книгу можно продать, купить, переместить на склад и так далее). При разработке сложных программных систем необходимо группировать не только данные, но и функции, которые работают с этими данными.
Пример использования объектного подхода
Рассмотрим простой пример объектно-ориентированной программы. Представим, что мы программируем графический редактор, который может рисовать различные фигуры. Подумав над задачей, мы приходим к выводу, что в нашей программе должны быть объекты, которые представляют различные фигуры. Итак приступим.
Сначала мы должны создать классы и описать внутри этих классов данные, которые относятся к фигурам и поведение фигур в виде набора методов (функций).
Опишем класс для фигуры «Треугольник». Какие характеристики могут быть у класса «Треугольник»? От чего зависит набор характеристик? В принципе, любой объект является бесконечно сложным и для его описания понадобится бесконечно большое количество характеристик. Но для нашего простого графического редактора важными будут следующие параметры: координаты трех точек и цвет фигуры.
Здесь вы должны понять очень важную вещь: набор данных в классе зависит от той программы, которую мы собираемся написать. Например, если бы мы писали более мощный графический редактор, в списке полей класса мы добавили бы отдельно цвет заливки и цвет линий фигуры, сам цвет мог быть не только сплошным, но и в виде какого-то узора и так далее.
Для указания класса в Java используется ключевое слово class, после чего идет название класса, далее мы ставим фигурные скобки и всё, что будет написано внутри фигурных скобок (переменные и функции) будет относиться к этому классу.
class Triangle {
}
Какие переменные и типы данных будут моделировать координаты точек и цвет? Для моделирования цвета воспользуемся обычным целым числом (очень часто цвета моделируются обычным целочисленным значением).
class Triangle {
// Цвет треугольника
int color;
}
Теперь попробуем подумать, а как нам смоделировать координаты трех точек? Если бы мы программировали на языке C, мы бы написали что-то вроде
class Triangle {
// Цвет треугольника
int color;
// Координаты точек треугольника
int x1,y1,x2,y2,x3,y3;
}
Но давайте подумаем - координаты каждой точки логически связаны между собой и когда мы будем, например, перемещать треугольник или будем менять размер треугольника, будут одновременно меняться две переменные, которые моделируют одну точку. С точки зрения объектной модели, лучшим вариантом будет предусмотреть отдельную сущность, отдельный класс, который моделирует точку на двумерной плоскости. Таким образом, мы сгруппируем данные и код, который будет эти данные изменять (например, менять значение X и Y).
class Point2D {
int x;
int y;
}
Теперь вернемся к классу Triangle и укажем, что в качестве координат у нас будут выступать три объекта класса Point2D.
class Triangle {
// Цвет треугольника
int color;
// Координаты трех точек
Point2D point1;
Point2D point2;
Point2D point3;
}
Теперь давайте определимся с методами классов – то есть, с тем поведением, которое будут осуществлять объекты этих классов.
Какое поведение может быть у объекта класса «точка»? Совершенно точно это будет метод «изменить координаты». То есть, наша точка как отдельный объект может вести себя следующим образом – менять свои координаты. Давайте запрограммируем этот метод.
class Point2D {
int x;
int y;
void changeCoordinates(int new_x, int new_y) {
x = new_x;
y = new_y;
}
}
Теперь давайте подумаем, какое поведение будет у треугольника? Что с ним можно сделать? Ну, например, можно его перекрасить, можно его передвинуть в другое место на плоскости, можно его нарисовать на каком-то полотне и так далее. Давайте опишем некоторые из этих методов.
class Triangle {
int color; // Цвет треугольника
// Координаты трех точек
Point2D point1;
Point2D point2;
Point2D point3;
// Меняем цвет фигуры
void changeColor(int new_color) {
color = new_color;
}
// Меняем расположение фигуры
// Для наглядности будем передавать входные параметры
// в виде 6 целых чисел
void move(int x1, int y1, int x2, int y2, int x3, int y3) {
point1.changeCoordinates(x1, y1);
point2.changeCoordinates(x2, y2);
point3.changeCoordinates(x3, y3);
}
}
Итак, мы описали треугольник, но мы определили только понятие «треугольник», у нас нет их физически, код класса - это просто описание. Если вы создали java-проект, у вас, по умолчанию, будет присутствовать класс Mainи функция main(). Пока что мы не будем объяснять, зачем нужен класс Main, а обратим внимание на метод main(). Этот метод является «точкой входа» в программу, с вызова этого метода начинается работа программы. Когда мы дойдем и выполним последнюю инструкцию внутри метода main(), приложение завершится.
Создадим внутри метода main() несколько объектов класса Triangle. Создание объектов мы будем рассматривать подробно в следующих лекциях, на данный момент мы просто скажем, что для создания объекта используется ключевое словоnew. После создания объекта, мы можем обращаться к объекту и вызывать методы у объектов (как мы вызывали функции в C).
public class Main {
public static void main(String[] args) {
// Создали один объект треугольника
Triangle triangle1 = new Triangle();
// Создали еще один объект треугольника
Triangle triangle2 = new Triangle();
// Для первого треугольника поменяли цвет
triangle1.changeColor(10000);
// Поменяли цвет для второго треугольника
triangle2.changeColor(20000);
// Создали объект первой точки треугольника
// и назначили точке какие-то координаты
triangle1.point1 = new Point2D();
triangle1.point1.changeCoordinates(140,180);
// Сделаем это же для первой точки второго треугольника
triangle2.point1 = new Point2D();
triangle2.point1.changeCoordinates(50,80);
}
}
Важно! Вы должны понять, что у разных объектов значения переменных будут разные!
То есть, где-то в оперативной памяти у нас создано два разных объекта. Внутри первого есть объект класса Point2D, внутри этого объекта есть две переменные типа int, у которых будут значения 140 и 180. Внутри же второго треугольника будет свой объект Point2D, внутри которого будут совершенно другие две переменные типа int, у которых будут значения 50 и 80. Это же будет касаться переменных color в двух разных объектах (рис.1.5).
Таким образом, когда мы описываем классы, мы заявляем, что объекты этих классов будут содержать определенные поля (набор переменных) и будут иметь набор методов (функций), которые можно вызвать у объекта. Каждый объект содержит свои переменные, но методы у них общие.
Базовые понятия ООП
Понятие объекта
Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Это позволят разграничить область применения методов. Объект – это строительный блок объектно-ориентированных программ. Объектно-ориентированная программа является, по сути, набором объектов. Объект состоит из трех частей:
имя объекта;
состояние (данные объекта, переменные состояния). Состояние объекта характеризуется перечнем всех свойств данного объекта и текущими значениями каждого из этих свойств;
методы (операции).
Данные объектов. Данные, содержащиеся в объекте, представляют его состояние. В терминологии ООП, эти данные называются атрибутами. Например, атрибутами работника могут быть имя, фамилия, пол, дата рождения, номер телефона и так далее. В разных объектах атрибуты имеют разное значение.
Поведение объектов. Поведение объекта – то, что он может сделать (в структурном программировании это реализовывалось функциями, процедурами, подпрограммами).
Сообщения – механизм коммуникации между объектами. Например, когда объект А вызывает метод объекта B, объект A отправляет сообщение объекту B. Ответ объекта B определяется его возвращаемым значением. Только «открытые» методы могут вызываться другим объектом.
Понятие класса
Каждый объект определяется общим шаблоном, который называется классом. В рамках класса задается общий шаблон, структура, на основе которой затем создаются объекты. Данные, относящиеся к классу, называются полями класса, а программный код для их обработки – методами класса. Поля и методы иногда называют общим термином – члены класса.
В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.
Согласно конвенции кода и правилам языка Java:
каждый класс должен содержаться в своем отдельном файле с расширением .java;
название файла должно совпадать с названием класса;
класс должен быть именем существительным;
имя класса должно его описывать;
имя класса начинается с большой буквы;
если имя состоит из нескольких слов, то каждое слово начинается с большой буквы.
Рассмотрим разницу между объектом и классом на примере. Определим класс Catи Dog. Описание класса производится через указание полей (данных) и методов класса. Для класса Catв качестве полей укажем name(кличку кота) и color(окрас). Для класса Dogзадаем поля name(кличка), color(окрас) и breed(порода).
Помимо полей, определим методы для этих классов. Методы – это то, что может делать объект класса (или что можно делать с объектом). Коты будут мяукать, и ловить мышей, а собаки лаять и вилять хвостом.
Таким образом, мы определили шаблоны, на основании которых впоследствии будут создаваться экземпляры классов или объекты. Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом. При создании объекта класса задаются конкретные значения для полей. Когда мы говорим о собаке или кошке вообще, как понятии, мы имеем в виду домашних животных, у которых есть имя, окрас и прочие характеристики. Это абстрактные понятия, которые соответствуют классу. А вот если речь идет о конкретном Шарике или Мурзике, то это уже объекты, экземпляры класса.
Синтаксис классов
Рассмотрим синтаксис описания классов в Java. Описание класса начинается с ключевого слова class. После этого следует имя класса и в фигурных скобках тело класса. Тело класса состоит из описания членов класса – полей и методов.
Для объявления класса служит ключевое слово class. Упрощенная форма определения класса имеет вид:
class имя_класса {
тип переменная_экземпляра_1;
тип переменная_экземпляра_2;
тип переменная_экземпляра_3;
...
тип переменная_экземпляра_N;
тип имя_метода_1(список параметров) {...}
тип имя_метода_2(список параметров) {...}
...
тип имя_метода_N(список параметров) {...}
}
Данные или переменные, определенные в классе, называются переменными экземпляра, поскольку каждый экземпляр класса (объект) содержит собственные копии этих переменных. Таким образом, данные одного объекта отделены и отличаются от данных другого объекта.
Код содержится в теле методов. В большинстве классов действия над переменными и доступ к ним осуществляют методы этого класса. Таким образом, методы определяют порядок использования данных класса.
Пример простого класса
Создадим класс Box, который описывает контейнер, допустим, на каком-то складе.
Box.java
public class Box {
double width;
double height;
double depth;
}
Класс Box определяет три переменные экземпляра: width (ширина), height (высота) и depth (глубина). В настоящий момент класс Box не содержит никаких методов.
Как мы уже говорили, класс определяет новый тип данных. В данном случае новый тип данных называется Box. Это имя будет использоваться для объявления объектов типа Box. Не следует забывать, что объявление class создает только шаблон, но не конкретный объект. Таким образом, приведенный выше код не приводит к появлению каких-нибудь объектов типа Box.
Чтобы действительно создать объект класса Box, нужно воспользоваться оператором new
Main.java
public class Main {
public static void main(String[] args) {
Box myBox = new Box();
}
}
После выполнения этого оператора объект myBox станет экземпляром класса Box. Таким образом, он обретет "физическое" существование.
Также следует напомнить, что каждый объект содержит собственную копию переменной экземпляра, которая определена в классе. Каждый объект типа Boxбудет содержать собственные копии переменных width, height и depth(рис. 4.2).
Изменения в переменных экземпляра одного объекта не влияют на переменные экземпляра другого объекта. Таким образом, каждый объект класса Box будет содержать собственные копии переменных width, height и depth. Для доступа к этим переменным служит оператор-точка (.). Эта операция связывает имя объекта с именем переменной экземпляра. Например, чтобы присвоить переменной width экземпляра myBox значение 100, нужно выполнить следующий оператор:
myBox.width = 100;
Этот оператор предписывает компилятору, что копии переменной width, хранящейся в объектe myBox, требуется присвоить значение 100. В общем, операция-точка служит для доступа как к переменным экземпляра, так и к методам в пределах объекта.
Ниже приведет пример программы, в которой используется класс Box
public class Main {
public static void main(String[] args) {
// Создаем объект типа Box
Box myBox = new Box();
// Присваиваем значения переменным экземпляра myBox
myBox.width = 10;
myBox.height = 20;
myBox.depth = 15;
// Рассчитываем объем коробки
double volume = myBox.width * myBox.height * myBox.depth;
System.out.println("Объем равен: " + volume);
}
}
public class Box {
double width;
double height;
double depth;
}
Как пояснялось ранее, каждый объект содержит собственные копии переменных экземпляра. Это означает, что при наличии двух объектов класса Box каждый из них будет содержать собственные копии переменных width, height и depth. Следует, однако, иметь ввиду, что изменения в переменных экземпляра одного объекта не влияют на переменные экземпляра другого. Например, в следующей программе объявлены два объекта класса Box:
public class Main {
public static void main(String[] args) {
Box myBox1 = new Box();
Box myBox2 = new Box();
// Присваиваем значения для mybox1
myBox1.width = 10;
myBox1.height = 20;
myBox1.depth = 15;
// Присваиваем значения для mybox2
myBox2.width = 3;
myBox2.height = 6;
myBox2.depth = 9;
double volume;
// объем первой коробки
volume = myBox1.width * myBox1.height * myBox1.depth;
// будет выведено 3000
System.out.println("Объем равен: " + volume);
// объем второй коробки
volume = myBox2.width * myBox2.height * myBox2.depth;
// будет выведено 162
System.out.println("Объем равен: " + volume);
}
}
Программа выводит следующий результат:
Объем равен: 3000.0
Объем равен: 162.0
Как видите, данные из объекта myBox1 полностью изолированы от данных, содержащихся в объекте myBox2.
Методы класса
Как упоминалось ранее, классы состоят из двух компонентов: переменных экземпляра и методов. Общая форма метода выглядит следующим образом:
[возвращаемый тип] имя ([список параметров]) {
[тело метода]
}
где возвращаемый тип означает конкретный тип данных, возвращаемый методом. Он может быть любым допустимым типом данных, в том числе и типом созданного класса. Если метод не возвращает значение, то его возвращаемым типом должен быть void. В качестве имени методов может быть любой допустимый идентификатор, кроме тех, которые уже используются другими элементами кода в текущей области действия. А список параметров обозначает последовательность пар "тип-идентификатор", разделенных запятыми. По существу, параметры - это переменные, которые принимают значения аргументов, передаваемых методу во время его вызова. Если у метода отсутствуют параметры, то список параметров оказывается пустым. Методы, возвращаемый тип которых отличается от void, возвращают значение вызывающей части программы с помощью оператора return.
Вернемся к нашему примеру с классом Box. Было бы логично, если бы расчет объема коробки выполнялся в классе Box, поскольку объем коробки зависит от ее размеров. Для этого добавим в класс Box метод getVolume()
В первой строке вызывается метод volume() для объекта myBox1. Следовательно, метод volume() вызывается по отношению к объекту myBox1, для чего было указано имя объекта, а вслед за ним - операция-точка. Таким образом, в результате вызова метода myBox1.volume() выводится объем коробки, определяемого объектом myBox1, а в результате вызова метода myBox2.volume() - объем коробки, определяемого объектом myBox2.
При вызове метода myBox1.volume() исполняющая система Jаvа передает управление коду, определенному в теле метода volume(). По окончании выполнения всех операторов в теле метода управление возвращается вызывающей части программы и далее ее выполнение продолжается со строки кода, следующей за вызовом метода. В самом общем смысле метод - это способ реализации подпрограмм в Java.
В методе volume() следует обратить внимание на еще одну очень важную особенность: ссылка на переменные экземпляра width, height и depth делается непосредственно без указания перед ними имени объекта или операции-точки. Когда в методе используется переменная экземпляра, определенная в его же классе, это делается непосредственно, без указания явной ссылки на объект и применения операции-точки . Это становится понятным, если немного подумать. Метод всегда вызывается по отношению к какому-то объекту его класса. Как только этот вызов сделан, объект известен. Таким образом, в теле метода вторичное указание объекта совершенно излишне. Это означает, что переменные экземпляра width, height и depth неявно ссылаются на копии этих переменных, хранящиеся в объекте, который вызывает метод volume().
Подведем краткие итоги. Когда доступ к переменной экземпляра выполняется из кода, не входящего в класс, где определена переменная экземпляра, следует непременно указать объект с помощью операции-точки. Но когда такой доступ осуществляется из кода, входящего в класс, где определена переменная экземпляра, ссылка на переменную может делаться непосредственно. Эти же правила относятся и к методам.
Следует обратить внимание, что метод getVolume() возвращает значение 3000, и это значение рассчитанного объема сохраняется в переменной vol. При обращении с возвращаемыми значениями следует принимать во внимание два важных обстоятельства:
тип данных, возвращаемых методом, должен быть совместим с возвращаемым типом, указанным в методе. Так, если какой-нибудь метод должен возвращать логический тип boolean, то возвратить из него целочисленное значение нельзя;
переменная, принимающая возвращаемое методом значение (например, vol), также должна быть совместима с возвращаемым типом, указанным для метода.
Создание объектов
Когда мы объявляем новый класс, то мы фактически создаем новый тип данных, который можно использовать для объявления объектов данного типа. Создание объектов класса представляет собой двухэтапный процесс. Сначала следует объявить переменную типа класса.
Box myBox;
Эта переменная является ссылочной, то есть она не содержит объект, а ссылается на него (примерно как указатель в C не содержит значение, а содержит адрес, то есть переменная ссылается на значение в памяти).
Затем нужно создать конкретный физический объект и получить на него ссылку. Эти операции выполняются с помощью оператора new. Этот оператор динамически (то есть, во время выполнения программы) резервирует память для объекта, инициирует процесс создания объекта и возвращает ссылку на него (ссылка представляет собой адрес объекта в памяти). Далее нам необходимо сохранить ссылку в переменной.
Box myBox;
mybox = new Box();
В первой строке переменная mybox объявляется как ссылка на объект типа Box. В данный момент mybox пока еще не ссылается на конкретный объект, значение переменной равно null. В следующей строке кода выделяется память для конкретного объекта, а переменной mybox присваивается ссылка на этот объект.
После выполнения второй строки кода переменную mybox можно использовать так, как если бы она была объектом типа Box. Но в действительности переменная mybox просто содержит адрес памяти конкретного объекта типа Box. Результат выполнения этих двух строк кода показан на рисунке.
Присвоение переменным ссылок на объекты
Какие действия выполняет приведенный ниже фрагмент кода?
Box b1 = new Box();
Box b2 = b1;
На первый взгляд, переменной b2 присваивается ссылка на копию объекта, на которую ссылается переменная b1. Таким образом, может показаться, что переменные b1 и b2 ссылаются на совершенно разные объекты, но это не так. После выполнения данного фрагмента кода обе переменные, b1 и b2, будут ссылаться на один и тот же объект. Таким образом, любые изменения, внесенные в объекте по ссылке в переменную b2, окажут влияние на объект, на который ссылается переменная b1, поскольку это один и тот же объект.
Передача аргументов подпрограмме
В общем случае, для передачи аргументов подпрограмме (в данном случае, методу) в языках программирования имеются два способа.
Первым способом является передача по значению. В этом случае значение аргумента копируется в параметр метода. Следовательно, изменения, вносимые в параметр метода, не оказывают никакого влияния на аргумент.
Вторым способом является передача по ссылке. В этом случае параметру передается ссылка на значение аргумента. Изменения, вносимые в параметр метода, будет оказывать влияние на аргумент, используемый при вызове.
Все аргументы в Java передаются по значению, но конкретный результат зависит от того, какой именно тип данных передается: примитивный или ссылочный.
Когда методу передается аргумент примитивного типа, его передача происходит по значению. В итоге создается копия аргумента, и все, что происходит с параметром, принимающим этот аргумент, не оказывает никакого влияния за пределами вызываемого метода.
int x = 10; // x равен 10
foo(x); // в метод копируется значение х
// работа метода не повлияет на переменную x
// она все равно будет равна 10
void foo(int x) {
x = x * 2;
}
При передаче объекта в качестве аргумента методу ситуация меняется коренным образом, поскольку объекты, по существу, передаются при вызове по ссылке. Не следует, однако , забывать, что при объявлении переменной типа класса создается лишь ссылка на объект этого класса. Таким образом, при передаче этой ссылки методу принимающий ее параметр будет ссылаться на тот же самый объект, на который ссылается и аргумент. По существу, это означает, что объекты действуют так, как будто они передаются методам по ссылке. Но изменения объекта в теле метода оказывают влияние на объект, указываемый в качестве аргумента.
Box mybox = new Box();
mybox.width = 100;
foo(mybox);
// переменная width объекта mybox будет равна 200
void foo(Box mybox) {
// В этом методе мы работаем с одним и тем же объектом
mybox.width = mybox.width * 2;
}
Конструктор
Основной причиной чрезмерных затрат в программировании является "небезопасное" программирование.
Основные проблемы с безопасностью относятся к инициализации и завершению. Очень многие ошибки при программировании на языке C обусловлены неверной инициализацией переменных. Это особенно часто происходит при работе с библиотеками, когда пользователи не знают, как нужно инициализировать компонент библиотеки или забывают это сделать.
В языке C++ впервые появляется понятие конструктора - специального метода, который вызывается при создании нового объекта.
В Java разработчик класса может в обязательном порядке выполнить инициализацию каждого объекта при помощи специального метода, называемого конструктором. Если у класса имеется конструктор, Java автоматически вызывает его при создании объекта, перед тем как пользователи смогут обратиться к этому объекту. Таким образом, инициализация объекта гарантирована.
Конструктор – это специальный метод, который вызывается при создании нового объекта.
Синтаксис конструктора отличается от синтаксиса обычного метода. Его имя совпадает с именем класса, в котором он находится, и он не имеет возвращаемого типа.
Как было сказано выше, оператор newдинамически выделяет оперативную память для создания объекта. Общая форма использования оператора newвыглядит следующим образом
переменная_типа_класса = new имя_класса();
Имя класса, за которым следуют круглые скобки, обозначает конструктор данного класса. Конструкторы являются важной частью всех классов и обладают множеством важных свойств
В большинстве классов, используемых в реальных программах, явно объявляются свои конструкторы в пределах определения класса
Инициализация всех переменных класса при каждом создании объекта – занятие довольно утомительное. В связи с этим, в Java разрешается выполнять собственную инициализацию при создании объектов. Такая инициализация осуществляется с помощью конструктора.
public class Box {
double width;
double height;
double depth;
public Box(double wd, double ht, double dt) {
width = wd;
height = ht;
depth = dt;
}
}
...
Box mybox = new Box(100, 200, 300);
Еще раз обратите внимание, что имя конструктора совпадает с именем класса, в котором он находится, а синтаксис аналогичен синтаксису метода. Также конструктор не имеет возвращаемого типа - даже типа void.
Большинство IDE для Java имеют механизм для генерации конструкторов. В IntelliJ IDEA нажмите комбинацию Alt+Insert находясь в окне редактирования java-файла. Откроется контекстное меню Generate, где вы можете выбрать генерацию конструктора, после чего указать поля для инициализации.
Теперь нам должно быть понятно, почему при создании нового объекта, после имени класса требуется указывать круглые скобки. В действительности оператор newвызывает конструктор класса.
Оператор newвызывает конструктор Box(). Но мы ранее не создавали этот конструктор, почему компилятор не выдал ошибку, когда мы запускали приложение?
Если в классе не определен конструктор, то в Java будет автоматически предоставлен конструктор по умолчанию.
Конструктор не получающий аргументов, называется конструктором по умолчанию (в документации Java он называется конструктор без аргументов).
Конструктор по умолчанию инициализирует все переменные экземпляра устанавливаемыми по умолчанию значениями, которые могут быть нулевыми, пустыми (null) и логическими (false) для числовых, ссылочных и логических типов соответственно. Зачастую конструктора по умолчанию оказывается достаточно для простых классов. Если же вы определите в классе хотя бы один конструктор, то конструктор по умолчанию создан не будет. Именно поэтому, следующий код выдаст ошибку.
Ключевое слово this
Представим, что у нас есть два объекта одного класса и для этих двух объектов вызывается один и тот же метод:
public class Box {
double width;
double height;
double depth;
public double getArea() {
return width * height * depth;
}
}
...
Box box1 = new Box();
Box box2 = new Box();
box1.getArea();
box2.getArea();
Если существует один метод getArea(), как метод узнает, для какого объекта он вызывается – для box_1 или дляbox_2?
Оказывается, при вызове метода getArea() (как и при вызове любого другого метода) передается скрытый первый аргумент – ссылка на используемый объект. Таким образом, вызовы методов на самом деле выглядят так:
Box.getArea(box1);
Box.getArea(box2);
Передача дополнительного аргумента относится к внутреннему синтаксису. При попытке явно воспользоваться ею компилятор выдаст сообщение об ошибке.
Предположим, во время выполнения метода нам необходимо получить ссылку на текущий объект. Так как эта ссылка передается компилятором скрытно, идентификатора для нее не существует. Но для решения этой задачи существует ключевое слово this.
Ключевое слово this может использоваться только внутри не-статического метода и предоставляет ссылку на объект, для которого был вызван метод.
Обращаться с ней можно точно так же, как и с любой другой ссылкой на объект. Для вызова метода класса из другого метода этого же класса, использовать ключевое слово thisне нужно.
Ключевое слово this чаще всего используется в ситуации, когда локальная переменная скрывает поле класса. В Java не допускается объявление двух локальных переменных с одним и тем же именем в той же самой области действия. Однако, мы можем объявить локальные переменные, имена которых совпадают с именами полей класса.
public class Box {
double width;
double height;
double depth;
// Пример сокрытия поля класса
void foo(double width) {
double height = 100;
// В консоль будет выведено значение локальных переменных
System.out.println(width);
System.out.println(height);
}
}
Когда имя локальной переменной совпадает с именем переменной экземпляра, локальная переменная скрывает поле класса.
Можно решить эту ситуацию путем изменения имен локальных переменных, но это некорректно с точки зрения хорошего стиля написания кода. Грамотным решением является использование ключевого слова this. Это позволит переменным иметь одинаковые названия, а к переменным экземпляра можно будет обратиться с помощью этого ключевого слова.
public class Box {
double width;
double height;
double depth;
// Пример сокрытия поля класса
void foo(double width) {
double height = 100;
// В консоль будет выведено значение локальных переменных
System.out.println(width);
System.out.println(height);
// Теперь мы обращаемся к переменным экземпляра
System.out.println(this.width);
System.out.println(this.height);
}
}
Иногда, во время выполнения метода необходимо получить ссылку на текущий объект, для которого был вызван метод. Так как ссылка на него передается скрытно, идентификатора для нее нет. Но для этого существует специальное ключевое слово – this. Ключевое слово thisпредоставляет ссылку на объект, для которого был вызван метод. Обращаться с ней можно как и с любой другой ссылкой на объект.
Лекция 5
Тема: Принцип полиморфизма. Статическое и динамическое связывание. Переопределение методов. Перегрузка методов. Upcasting и downcasting. Ключевое слово instanceof.
Полиморфизм является второй важной особенностью объектно-ориентированных языков.
Он представляет еще одну степень отделения интерфейса от реализации, разъединения что от как. Полиморфизм улучшает организацию кода и его читаемость, а также способствует созданию расширяемых программ, который могут "расти" не только в процессе начальной разработки проекта, но и при добавлении новых возможностей.
Преобразование типов в Java
Чтобы начать рассматривать принцип полиморфизма, рассмотрим механизм преобразования типов в Java.
Преобразование примитивных типов
Преобразование типов позволяет преобразовывать данные одного типа в другой тип. Можно выделить следующие типы преобразования:
расширяющее преобразование (widening) - значение одного типа преобразовывается в более широкий тип, с большим диапазоном допустимых значений, то есть не происходит потери данных.
сужающее преобразование (narrowing) - значение одного типа преобразовывается в более узкий тип, с меньшим диапазоном допустимых значений, в этом случае возможна потеря данных.
упаковка\распаковка данных (boxing\unboxing) - позволяет преобразовывать данные примитивных типов в соответствующие им объекты типов-оболочек.
Про упаковку и распаковку данных мы будем говорить позже, а пока рассмотрим примеры расширяющие и сужающего преобразования.
Результат работы программы будет следующим
Как мы видим из приведенного фрагмента кода, расширяющее преобразование происходит неявно, автоматически. Конечно, нам никто не запрещает указать явное преобразование
но Java этого не требует и такая форма записи при расширяющие преобразовании не используется.
Сужающее преобразование можно привести к потере данных и поэтому Java требует явного преобразования типов, запись типа
вызовет ошибку компиляции.
Преобразование ссылочных типов
Наряду с преобразованием данных примитивных типов, Java поддерживает преобразование ссылочных типов.
Преобразование примитивных типов базировалось на количестве байт, который использовал тот или иной тип данных и на возможность хранить больший диапазон допустимых значений. Преобразование ссылочных базируется на иерархии классов.
Класс-родитель считается базовым типом, потому что все объекты класса-потомка входят во множество объектов базового класса. Как мы уже говорили раньше, все объекты Student являются объектами Person (все студенты являются людьми), все объекты Dog являются объектами Animal (все собаки являются животными).
Каждый класс Java расширяет (наследуется) другой класс, который называется суперклассом. Класс наследует поля и методы суперкласса и определяет свои дополнительные поля и методы. Существует специальный класс Object, который является корнем классовой иерархии. Он не наследуется от каких-либо классов, но все другие классы Java либо напрямую расширяют класс Object, либо наследуются от одного из потомков Object.
Рассмотрим классы String и Point. Мы можем сказать, что все объекты String также являются объектами Object. Также мы можем сказать, что все объекты Point являются объектами Object. Обратное утверждение неверно (не все объекты Object являются объектами Point или String).
Опишем правила преобразования ссылочных типов в Java:
1.Объект не может быть преобразован в несовместимый (unrelated) тип. Речь может идти только о классах с отношением "родитель" - "потомок". К примеру, Java не сможет преобразовать объект типа String в объект типа Point. Даже если вы будете использовать явное преобразование, то следующий код
приведет к ошибке компиляции
2. Объект может быть преобразован в тип суперкласса. Это расширяющее преобразование, поэтому Java не требует явного преобразования. Например, ссылка на объект типа String может быть присвоена переменной типа Object, так как String наследуется от класса Object, напрямую или опосредовано. Ссылка на объект типа Point также может быть присвоена переменной типа Object. Хотя класс Point напрямую наследуется от Point2D, класс Object все равно является суперклассом для Point, хотя и опосредовано. К примеру, следующий код вполне корректен и не вызовет ошибок
Важнейший момент - при преобразовании ссылочных типов, сам физический объект ни во что не преобразовывается. Просто теперь мы можем работать с ним как будто он объект типа Object.
Это замечание - важнейший момент, который вам необходимо усвоить. В примере выше переменные o1 и o2 по факту будут ссылаться на объекты типа String и Point соответственно. При любых преобразованиях типов, сам объект никак не модифицируется, просто Java теперь считает его объектом другого типа. К чему это приводит - рассмотримпозже.
3. Объект может быть преобразован в тип подкласса. Это является сужающим преобразованием и требует явного приведения типов.
Опасность сужающего преобразования ссылочных типов заключается в том, что компилятор Java не может определить корректность преобразования во время компиляции, эта проверка происходит во время выполнения (в runtime, в рантайме) приложения. Это может привести к ошибке во время выполнения и закрытию приложения, что недопустимо при разработке качественного ПО.
Сужающее преобразование будет корректным только в одном случае - если объект по факту является объектом этого класса!
Нисходящее преобразование является корректным только когда это операция, обратная восходящему преобразованию.
Например, данный код
приведет к ошибке во время выполнения
Ошибка возникает потому что ссылочная переменная person ссылается на объект, который имеет фактический тип Person. При попытке преобразовать его в объект типа Student произойдет ошибка преобразования типов.
Но следующий код
будет работать корректно и преобразование будет корректным.
В примере выше мы сначала сделали восходящее преобразование
и переменная person ссылается на объект, который имеет фактический тип Student. Реализуя сужающее преобразование
мы ставим все на свои места - ссылочная переменная типа Student ссылается на объект фактического типа Student.
Еще раз повторим, нисходящее преобразование используется, чтобы восстановить тип объекта после восходящего преобразования.
4. Все типы массивов являются несовместимыми типами, поэтому массив одного типа не может быть преобразован в массив другого типа, даже если элементы массивов могут быть преобразованы. Например, следующий код
приведет к ошибке во время выполнения, хотя отдельные элементы типа int можно преобразовать в double.
5. Массивы не имеют иерархию типов, но все массивы считаются экземплярами типа Object, поэтому любой массив может быть преобразован в тип Object с помощью расширяющего преобразования и обратно с помощью сужающего преобразования.
Восходящее преобразование (upcasting)
Для начала, вернемся к наследованию. Самая важная особенность наследования заключается в том, что наследование выражает отношение между новым и базовым классом. Это отношение можно выразить как "Новый класс является разновидностью базового класса", это же справедливо и для объектов этих классов. Например, класс Student является разновидностью Person, класс Bear является разновидностью Animal и так далее.
Данное отношение поддерживается языком программирования. Например, рассмотрим базовый класс Instrument, который представляет музыкальный инструмент, и класс Guitar, который представляет гитару.
Так как наследование означает, что все методы базового класса также доступны в производном классе, любое сообщение, которое мы можем отправить базовому классу, можно отправить и производному классу. Если в классе Instrument есть метод play(), то он будет присутствовать и в классе Guitar. Рассмотрим следующий код
В результате получим следующие сообщения в консоли
Обратите внимание, что на вход методу tune() можно подать как объект типа Instrument, так и объект типа Guitar.
Таким образом, метод tune() можно применять для объектов типа Instrument и объектов любых классов, производных от Instrument.
Как уже было сказано выше, мы имеет дело с расширяющим преобразованием ссылочных типов (преобразование ссылки на объект Guitar в ссылку на объект Instrument). Расширяющее преобразование ссылочных типов называется восходящим преобразованием типов (upcasting).
Рассмотрим еще раз пример с классом Instrument
Метод tune() получает ссылку на объект класса Instrument, но мы можем передать объект любого класса, производного от Instrument. В методe main() ссылка на объект класса Guitar передается методу tune() без явных преобразований. Это нормально - интерфейс класса Instrument должен существовать и в классе Guitar, так как класс Guitar был унаследован от класса Instrument.
Казалось бы, зачем нам указывать, что метод tune() принимает ссылку на объект класса Instrument? Если мы хотим, чтобы метод tune() принимал ссылку на объект класса Guitar, почему просто не написать
В таком случае, для каждого класса, производного от Instrument придется писать свой метод tune(). Предположим, что в новой версии программы мы добавили классы Saxophone и Violin. В таком случае у нас получился бы следующий код
Программа будет работать корректно, но у нее есть огромный недостаток - для каждого нового подкласса Instrument необходимо будет писать свой новый метод tune(), который будет принимать объект этого производного класса.
Если же указать в методe tune() ссылку на базовый класс, тогда необходимо будет написать всего один единственный метод tune().
Таким образом, код метода tune() будет, своего рода, обобщенным кодом, который будет выполняться не только для объектов базового класса, но и для объектов производного класса. В этом и заключается главное преимущество полиморфизма.
Важно понять, что в зависимости от разных объектов разных производных классов, метод tune() может работать по-разному, так как в каждом подклассе мы переопределяем метод play().
В данном случае, методы play() различных подклассов возвращают различную строку, поэтому, хотя и вызывается один и тот же метод tune(), но результат его работы каждый раз разный, в зависимости от того - объект какого подкласса мы ему передали.
Перегрузка методов
В Java разрешается в одном и том же классе определять два или более метода с одинаковым именем, если только объявления их параметров отличаются. В этом случае методы называются перегружаемыми, а сам процесс – перегрузкой метода (method overloading).
Если у методов одинаковые имена, как Java узнает, какой именно из них вызывается? Ответ прост: перегружаемые методы должны отличаться по типу и/или количеству входных параметров. Даже разного порядка аргументов достаточно для того, чтобы методы считались разными (хотя это не рекомендуется).
Перегрузка по возвращаемым значениям
Логично спросить, почему при перегрузке используются только имена классов и списки аргументов? Почему не идентифицировать методы по их возвращаемым значениям?
Идентифицировать их нельзя, потому что Java в этом случае не может определить, какая версия метода должна выполняться.
При вызове перегружаемого метода для определения нужного варианта в Java используется тип и\или количество аргументов метода. Следовательно, перегружаемые методы должны отличаться по типу и\или количеству их параметров. Возвращаемые типы перегружаемых методов могут отличаться, но самого возвращаемого метода недостаточно, чтобы отличить два разных варианта метода. Когда в исполняющей среде Java встречается вызов перегружаемого метода, в ней просто выполняется тот вариант, параметры которого соответствуют аргументам, указанным в вызове.
Перегрузка методов позволяет поддерживать принцип «один интерфейс, несколько методов».
В языках программирования без перегрузки методов, каждому методу должно быть присвоено однозначное имя. Но зачастую требуется реализовать, по существу, один и тот же метод для разных типов данных.
В таком случае, в языках программирования без перегрузки реализуют несколько методов, которые немного отличаются названиями.
Перегрузка методов ценна тем, что позволяет обращаться к похожим методам по общему имени. Следовательно, имя представляет общее действие, которое должно выполняться. Выбор подходящего варианта метода для конкретной ситуации входит в обязанности компилятора.
Ничто не запрещает вам реализовать несколько перегруженных методов, каждый из которых будет работать совершенно по-разному. Но на практике крайне рекомендуется, чтобы перегруженные методы реализовывали одну и ту же общую операцию.
Перегрузка конструкторов
Наряду с перегрузкой обычных методов можно также выполнять перегрузку конструкторов. Перегружаемые конструкторы – это норма и часто используемый прием.
Соответствующий перегружаемый конструктор вызывается в зависимости от параметров, указываемых при выполнении оператора new.
Если вы пишете для класса несколько конструкторов, иногда бывает удобно вызвать один конструктор из другого, чтобы избежать дублирования кода. Такая операция проводится с использованием ключевого слова this.
Связывание "метод-вызов"
Давайте еще раз взглянем на метод tune()
Мы уже разобрались, что в зависимости от ссылки на объект того или иного подкласса, Java вызовет тот или иной переопределенный метод play(). Но откуда компилятор знает - какой из методов play() необходимо будет вызвать, ведь в качестве входного аргумента у нас указана ссылка на объект класса Instrument? Ответ заключается в том, что компилятор этого не знает.
Присоединение вызова метода к телу метода называется связыванием. Если связывание производится перед запуском программы (на этапе компиляции или компоновки), оно называется ранним (early) или статическим (static)связыванием (binding).
Неоднозначность в работе метода tune() связана именно с ранним связыванием: компилятор не может знать заранее, какой вариант метода play() нужно будет вызвать, когда у него есть только ссылка на объект класса Instrument.
Данная проблема решается благодаря позднему связыванию, то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее (late) связывание (binding) также называют динамическим (dynamic) или связыванием на этапе выполнения программы (runtime binding).
В реализации Java существует механизм для фактического определения типа объекта во время работы программы для вызова подходящего метода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова метода определяет фактический тип объекта и вызывает соответствующее тело метода.
Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как final (приватные методы являются final по умолчанию).
Итак, подведем итоги:
статическое связывание в Java происходит на этапе компиляции, тогда как динамическое связывание происходит во время выполнения программы (в runtime);
для private, final и static методов, а также для полей используется статическое связывание, тогда как для остальных методов (такие методы в некоторых языках программированию называются виртуальными (virtual)) используется динамическое связывание;
в статическом связывании используется тип ссылки, тогда как в динамическом связывании используется фактический тип объекта;
перегруженные методы используют статическое связывание, тогда как переопределенные методы используют динамическое связывание.
Пример.
Рассмотрим популярный пример с геометрическими фигурами. Создадим базовый класс Shape и различные производные классы: Circle, Square и Triangle.
Создадим объект класса Circle используя принцип восходящего преобразования
В данном коде создается объект типа Circle, после чего ссылка на объект присваивается переменной типа Shape. На первый взгляд это ошибка, мы не можем присвоить ссылочной переменной одного типа ссылку на объект другого типа. Но на самом деле, все правильно, потому что тип Circle является типом Shape посредством наследования.
Если мы вызовем у объекта метод draw()
то можно подумать, что вызовется метод draw() из класса Shape, так как ссылочная переменная имеет тип Shape. Но на самом деле будет вызван правильный метод Circle.draw(), так как в программе используется позднее связывание (полиморфизм).
Создадим объекты других типов
и посмотрим на результат
Базовый класс Shape устанавливает для всех классов, производных от Shape, общий интерфейс - набор публичных методов (действий, которые может совершить внешний код над объектом). То есть любую фигуру можно нарисовать (draw()) и стереть (erase()). Производные классы переопределяют этот набор методов, чтобы реализовать свое уникальное поведение для этой фигуры.
Благодаря полиморфизму, вы можете добавлять сколько угодно новых типов, внося в программу минимальные изменения или не внося изменений вовсе. В хорошо спроектированной программе, большая часть методов (или даже все методы) переопределяют интерфейс базового класса. Такая программа будет являться расширяемой, поскольку в нее можно добавлять дополнительную функциональность, создавая новые типы данных из общего базового класса.
Лекция 9
Тема: Коллекции. Интерфейс List и его производные. ArrayList и LinkedList. Интерфейс Queue и его производные. PriorityQueue и интерфейс Deque. Интерфейс Set и его производные. HashSet и TreeSet. Интерефейс Map и его производные. HashMap и TreeMap.
В Java имеется несколько способов хранения объектов (или, точнее, ссылок на объекты). Самый простой вариант хранения объектов - массивы. Массивы обеспечивают самый эффективный способ хранения групп объектов. Однако массив имеет фиксированный размер, а, в общем случае, во время написания программы разработчик не знает точное количество объектов. К тому же массивы неэффективны при большой объеме элементов. Для таких случаев, в Java и в других языках реализован механизм коллекций объектов.
Коллекция - это хранилище, поддерживающее различные способы накопления и упорядочения объектов с целью обеспечения возможностей эффективного доступа к ним. Они представляют собой реализацию абстрактных типов (структур) данных, поддерживающих три основные операции:
добавление нового элемента в коллекцию;
удаление элемента из коллекции;
изменение элемента в коллекции.
Коллекции в Java объединены в библиотеке java.util и представляют собой контейнеры для хранения и манипулирования объектами. Основные интерфейсы коллекций:
Map<K, V> - карата отображения вида "ключ-значение" (такие структуры называют словарями или ассоциативными массивами);
Collection<E> - вершине иерархии, базовый интерфейс для коллекций;
List<E> - специализирует коллекции для обработки списков;
Set<E> - специализирует коллекции для обработки множеств, содержащих уникальные элементы.
Ниже представлены основные интерфейсы и классы коллекций.
Интерфейс Collection
Этот интерфейс служит основанием, на котором построен весь каркас коллекций, поскольку он должен быть реализован почти всеми классами коллекций (кроме коллекций, реализующих интерфейс Map). Интерфейс Collection является обобщенным, ссылочная переменная типа Collection объявляется следующим образом
Интерфейс Collection расширяет интерфейс Iterable. Это означает, что все коллекции можно перебирать, организовав цикл foreach. В интерфейсе Collection определяются основные методы, которые должны иметь все коллекции. Перечислим некоторые основные методы
Объекты вводятся в коллекции методом add(). Следует, однако, иметь ввиду, что метод add() принимает аргумент типа E. Следовательно, добавляемые в коллекцию объекты должны быть совместимы с предполагаемым типом данных в коллекции. Вызвав метод addAll(), можно ввести все содержимое одной коллекции в другую.
Вызвав метод remove(), можно удалить из коллекции отдельный объект. Чтобы из коллекции удалить группу объектов, достаточно вызвать метод removeAll(). А для того чтобы удалить из коллекции все элементы, кроме указанных, следует вызвать метод retainAll(). Вызвав метод removeIf(), можно удалить из коллекции элемент, если он удовлетворяет условию, которое задается в качестве параметра predicate. И наконец, для полной очистки коллекции достаточно вызвать метод clear().
Имеется также возможность определить, содержит ли коллекция определенный объект, вызвав метод contains(). Чтобы определить, содержит ли одна коллекция все члены другой, следует вызвать метод containsAll(). А определить, пуста ли коллекция, можно с помощью метода isEmpty(). Количество элементов, содержащихся в данный момент в коллекции, возвращает метод size().
Оба метода toArray() возвращают массив, который содержит элементы, хранящиеся в коллекции. Первый из них возвращает массив класса Object, а второй - массив элементов того же типа, что и массив, указанный в качестве параметра этого метода. Обычно второй метод более предпочтителен, поскольку он возвращает массив элементов нужного типа. Эти методы оказываются важнее, чем может показаться не первый взгляд. Ведь обрабатывать содержимое коллекции, используя синтаксис массивов, иногда оказывается очень выгодно. Обеспечив связь коллекции с массивом, можно извлечь выгоду из обоих языковых средств Java.
Две коллекции можно сравнить на равенство, вызвав метод equals(). Точный смысл равенства может зависеть от конкретной коллекции. Например, метод equals() можно реализовать таким способом, чтобы он сравнивал значения элементов, хранимых в коллекции. В качестве альтернативы методу equals() можно сравнивать ссылки на эти элементы.
Еще один очень важный метод iterator() возвращает итератор, а метод spliterator() - итератор разделитесь для коллекции. Итераторы очень часто используются для обращения с коллекциями. И наконец, методы stream() и parallelStream() возвращают поток данных типа Stream, использующий коллекцию для своих элементов.
Интерфейс List
Список (List) представляет собой динамический массив - упорядоченный набор элементов и может содержать повторяющиеся элементы. Вы можете получить доступ к любому элементу по индексу. Список является одним из наиболее используемых типов коллекций.
Основные свойства коллекций, реализующих интерфейс List:
список может включать одинаковые элементы;
элементы в списке хранятся в том порядке, в котором они помещались;
можно получить доступ к любому элементу по его порядковому номеру (индексу) внутри списка.
Начиная с версии Java 9, в интерфейс List внедрен фабричный метод of(), у которого имеется целый ряд перегруженных вариантов, возвращающих неизменяемую коллекцию на основе значений, составленную из переданных аргументов.
Особое назначение фабричного метода of() - предоставить удобный, эффективный способ для создания небольшой коллекции типа List. Во всех перегружаемых вариантах данного метода не допускается указывать пустые (null) элементы создаваемого списка. И в любом случае конкретная реализация интерфейса List не указывается.
Существуют два основных класса, реализующих List.
Класс ArrayList
Класс ArrayList с превосходной скоростью произвольного доступа к элементам, но относительно медленными операциями вставки и удаления элементов в середине. Пожалуй, самая часто используемая коллекция. ArrayList инкапсулирует в себе обычный массив, длина которого автоматически увеличивается при добавлении новых элементов.
Так как ArrayList использует массив, то время доступа к элементу по индексу выполняется за константное время (в отличие от класса LinkedList). При удалении произвольного элемента из списка, все элементы находящиеся "правее", смещаются на одну ячейку влево, при этом реальный размер массива (его емкость, capacity) не изменяется. Если при добавлении элемента массив будет полностью заполнен, будет создан новый массив размером n * 3 / 2 + 1, в него будут помещены все элементы из старого массива плюс новый, добавляемый элемент.
Несмотря на то, что емкость объектов типа ArrayList наращивается автоматически, ее можно увеличивать и вручную. вызывая метод ensureCapacity(). Это может потребоваться в том случае, если заранее известно, что в коллекции предполагается сохранить намного больше элементов, чем она содержит в данный момент. Увеличив емкость списочного массива в самом начале его обработки, можно избежать дорогостоящей операции постепенного наращивания списка.
С другой стороны, если потребуется уменьшить размер базового массива, на основе которого строится объект типа ArrayList до текущего количества хранящихся объектов, следует вызвать метод trimToSize().
Класс LinkedList
Связанный список LinkedList представляет собой коллекцию с оптимальным последовательным доступом и низкозатратными операциями вставки и удаления в середине списка. Операция произвольного доступа LinkedList выполняет относительно медленно, но обладает более широкой функциональностью, чем ArrayList.
LinkedList состоит из узлов, каждый из которых содержит как собственно данные, так и две ссылки ("связки") на следующий и предыдущий узел списка. Доступ к произвольному элементу осуществляется за линейное время (но доступ к первому и последнему элементу списка всегда осуществляется за константное время - ссылки постоянно хранятся на первый и последний элементы, так что добавление элемента в конец списка вовсе не значит, что придется перебирать весь список в поисках последнего элемента).
В целом, в абсолютных величинах LinkedList проигрывает ArrayList и по потребляемой памяти и по скорости выполнения операций.
В общем случае, следует использовать ArrayList. Коллекцию LinkedList имеет смысл использовать в случае, если происходит интенсивная вставка\удаление в середину списка либо необходимо гарантированное, заранее известное время добавления элемента в список.
Рассмотрим основные методы интерфейса List
Рассмотрим работу основных методов, заявленных в интерфейсе List
Обратите внимание, что класс ArrayList преобразуется в List посредством восходящего преобразования. В идеале, большая часть кода должна взаимодействовать с этими интерфейсами (хотя это не всегда возможно), а точный тип указывается только в точке создания контейнера.
Интерфейс Queue
Интерфейс Queue (очередь) расширяет интерфейс Collection и определяет поведение очереди, которая действует как список по принципу "первым вошел - первый вышел". Иначе говоря, объекты помещаются в один "конец" очереди, а извлекаются из другого "конца". Таким образом, порядок занесения объектов в контейнер будет совпадать с порядком его извлечения оттуда. Очереди также играют важную роль в параллельном программировании, потому что они обеспечивают безопасную передачу объектов между задачами.
Существуют разные виды очередей, порядок организации в которых основывается на некотором критерии. Интерфейс Queue является обобщенным, ссылочная переменная типа Queue объявляется следующим образом
Перечислим основные методы, определенные в интерфейсе Queue
Класс LinkedList
Класс LinkedList, который мы рассматривали в связи с интерфейсом List, также реализует интерфейс Deque и поддерживает поведение очередей. Восходящее преобразование LinkedList в Queue позволяет использовать объект как очередь.
Класс PriorityQueue
Класс PriorityQueue расширяет класс AbstractQueue и реализует интерфейс Queue. Он служит для сздания очереди по приоритетам на основании компаратора очереди.
В приоритетной очереди следующим извлекается элемент, обладающий наивысшим приоритетом.
Например, в аэропорту клиент может быть обслужен вне очереди, если его самолет готовится к вылету. В системе передачи сообщений некоторые сообщения могут содержать более важную информацию; они должны быть срочно обработаны независимо от времени поступления.
Если при построении очереди компаратор не указан, то применяется компаратор, выбираемый по умолчанию для того типа данных, который сохраняется в очереди. Таким образом, в начале (голове) очереди окажется элемент с наименьшим значением.
С помощью компаратора можно задать другую схему сортировки элементов в очереди. Например, когда в очереди сохраняются элементы, содержащие метку времени, для этой очереди можно задать приоритеты таким образом, чтобы самые давние элементы располагались в начале очереди.
Класс PriorityQueue имеет одну особенность. Так как очередь с приоритетами реализована с помощью кучи (бинарное дерево с определенными свойствами), то попытка вывести элементы с помощью цикла foreach, скорее всего, приведет к некорректному выводу элементов.
Интерфейс Set
В интерфейсе Set определяется множество. Он расширяет интерфейс Collection и определяет поведение коллекций, не допускающих дублирования элементов. Таким образом, метод add() возвращает false при попытке ввести в множество дублирующий элемент. В этом интерфейсе не определяется никаких дополнительных методов.
Интерфейс Set объявляется следующим образом
Интерфейс Set не добавляет никакой функциональности по сравнению с интерфейсом Collection, поэтому в Set нет дополнительный функциональности. Вместо этого Set представляет собой разновидность Collection.
Класс HashSet
Хеш-таблица хранит информацию, используя так называемый механизм хеширования, в котором содержимое ключа используется для определения уникального значения, называемого хеш-кодом. Хеш-код затем применяется в качестве индекса, с которым ассоциируются данные, доступные по этому ключу. Преобразование ключа в хеш-код выполняется автоматически - вы никогда не узнаете самого хеш-кода. Также ваш код не может напрямую индексировать хеш-таблицу.
Выгода от хеширования состоит в том, что оно обеспечивает константное время выполнения методов add(), contains(), remove() и size(), даже для больших объемов данных.
Если вы хотите использовать HashSet для хранения объектов собственных пользовательских классов, то вы ДОЛЖНЫ переопределить методы hashCode() и equals(), иначае два логически-одинаковых объекта будут считаться разными, так как при добавлении элемента в коллекцию будет вызываться метод hashCode() класса Object (который вернет разный хеш-код для двух логически одинаковых объектов).
Важно отметить, что класс HashSet не гарантирует упорядоченности элементов, поскольку процесс хеширования сам по себе обычно не порождает сортированных наборов. Если вам нужны сортированные наборы, то лучшим выбором может быть класс TreeSet.
Класс TreeSet
TreeSet - коллекция, которая хранит свои элементы в виде упорядоченного дерева. TreeSet использует сбалансированное красно-черное дерево для хранения элементов.
Для опреаций add(), remove(), contains() потребуется гарантированное время log(n).
Интерфейс Map
Интерфейс Map описывает коллекцию, состоящую из пар "ключ-значение". У каждого ключа только одно значение, что соответствует математическому понятию однозначной функции или отображения. Такую коллекцию часто называют словарем (dictionary) или ассоциативным массивом (associative array). Несмотря на то, что интерфейс Map входит в список коллекций Java, он не расширяет интерфейс Collection.
Интерфейс Map соотносит уникальные ключи со значениями. Ключ - это объект, который вы используете для последующего извлечения данных. Задавая ключ и значение, вы можете помещать значения в объект Map. После того как это значение сохранено, вы можете получить его по ключу. Интерфейс Map - это обобщенный интерфейс, объявленный так, как показано ниже:
Здесь в качестве K указывается тип ключа, а в качестве V - тип значения.
Рассмотрим основные методы, объявленные в интерфейсе Map
Обращение с отображениями опирается на две основные операции, выполняемые методами get() и put(). Чтобы ввести значение в отображение, следует вызвать put(), указав ключ и значение, а для того чтобы получить значение из отображения - вызвать метод get(), передав ему ключ в качестве аргумента. По этому ключу будет возвращено связанное с ним значение.
Как упоминалось ранее, Map не реализует интерфейс Collection, хотя является частью каркаса коллекций. Тем не менее, можно получить представление отображения в виде коллекции. Для этого можно воспользоваться методом entrySet(), возвращающим множество, содержащее элементы отображения (для получения множества ключей можно использовать метод keySet(), для получения множества значений можно использовать метод values()).
Интерфейс Map.Entry
Этот интерфейс позволяет обращаться с отдельными записями в отображении. Напомним, что метод entrySet(), объявляемый в интерфейсе Map, возвращает множество типа Set, содержащее записи из отображения. Каждый элемент этого множества представляет собой объект типа Map.Entry. Интерфейс Map.Entry является обобщенным и объявляется следующим образом:
где K обозначает тип ключей, а V - тип хранимых в отображении значений.
Класс HashMap
Этот класс расширяет класс AbstractMap и реализует интерфейс Map. В нем используется хеш-таблица для хранения отображений, и благодаря этому обеспечивается постоянное время выполнения методов get() и put(), даже в случае отображения с большим количеством элементов. Класс HashMap является обобщенным и объявляется приведенным ниже образом, где K обозначает тип ключей, а V - тип хранимых в отображении значений.
Класс TreeMap
Класс TreeMap расширяет класс AbstractMap и реализует интерфейс NavigableMap. В нем создается отображение, размещаемое в древовидной структуре. В классе TreeMap предоставляются эффективные средства для хранения пар "ключ-значение" в отсортированном порядке и обеспечивается их быстрое извлечение. Следует заметить, что в отличие от HashMap, древовидное отображение гарантирует, что его элементы будут отсортированы по порядку возрастания ключей. Класс TreeMap является обобщенным и объявляется следующим образом
где K обозначает тип ключей, а V - тип хранимых в отображении значений.
Классы WeakHashMap, LinkedHashMap и интерфейс NavigableMap выносятся на самостоятельное изучение.
Вспомогательный класс Collections
В фреймворке коллекций определяется ряд алгоритмов, которые можно применять к коллекциям и отображениям. Эти алгоритмы определены в виде статических методов класса Collections. По сути, класс Collections предоставляет огромный набор методов для различных операций с коллекциями.
Полный список методов класса Collections можно найти здесь.
Приведем список наиболее полезных методов класса Collections
В рамках данного курсе мы не будем подробно рассматривать методы этого класса, важно запомнить одно правило - если вы хотите произвести какие-то манипуляции с коллекцией - сначала попробуйте найти нужный функционал в классе Collections. Скорее всего, он там будет присутствовать в виде готового метода.
Вспомогательный класс Arrays
Также, в библиотеке java.util присутствует класс Arrays. Класс Arrays предоставляет различные удобные методы для работы с массивами. Эти методы помогает восполнить функциональный пробел между коллекциями и массивами. Например, метод asList() возвращает список, исходя из указанного массива, а метод binarySearch() использует алгоритм двоичного поиска для обнаружения заданного значения.
Полный список методов класса Arrays можно найти здесь.
Приведем список наиболее полезных методов класса Arrays
Применительно к классу Arrays действует то же правило - если вы хотите произвести какие-то манипуляции с массивом - сначала попробуйте найти нужный функционал в классе Arrays. Скорее всего, он там будет присутствовать в виде готового метода.
Сравнительная таблица временной сложности основных операций с коллекциями
Ниже приведена таблица, которая позволит вам облегчить процесс выбора нужной коллекции.
Лабораторные работы
Образец протокола лабораторной работы:
в формате pdf
Критерии оценки лабораторной работы (начиная с 3 лабораторной работы)
Корректно работающая лабораторная работа оценивается в 60 баллов, кроме оценки D за протокол лабораторной работы или в случае списывания лабораторной работы.
Студент получает дополнительно:
до 20 баллов за стилизацию исходного кода;
до 10 баллов за оформление протокола лабораторной работы;
Обобщение означает параметризированный тип. Обобщения позволяют создавать классы, интерфейсы и методы, в которых тип данных указывается в виде параметра. С помощью обобщений можно создавать класс, который будет автоматически работать с разными типами данных. Такие классы, интерфейсы и методы называются обобщенными, как например, обобщенный класс или обобщенный метод.
Обобщенный код будет автоматически работать с типом данных, переданным ему в качестве параметра. Многие алгоритмы выполняются одинаково, независимо от того, к данным какого типа они будут применяться. Например, сортировка не зависит от типа данных, будь то String, Student или любой другой пользовательский класс, объекты которого можно сравнить между собой. Используя обобщения, можно реализовать алгоритм один раз, а затем применять его без дополнительных усилий к любому типу данных.
Рассмотрим небольшой пример. Рассмотрим класс Box, который может содержать в себе один объект.
В данном случае наша коробка хранит объекты класса String. Но что, если нам в нашей программе необходима коробка, которая хранила бы не только объекты класса String, но и любого другого класса, как стандартного из библиотек Java, так и нашего созданного? Как мы можем реализовать это с помощью уже известных механизмов ООП?
Мы можем использовать механизм полиморфизма. Например, мы можем использовать класс Object, который, как известно, является суперклассом для всех классов Java.
Второй вариант - объявить интерфейс BoxItem и использовать его в качеcтве типа ссылочной переменной. Тогда в коробку можно будет "положить" объект класса, который реализует этот интерфейс.
Метод стал чуть более общим и может использоваться в большем количестве мест. Однако, огромный недостаток такого подхода - необходимость нисходящего преобразования извлеченного из коробки объекта. Если же мы не знаем заранее, объект какого класса будет помещен в коробку, то и нисходящее преобразование реализовать будет достаточно затруднительно.
Какой выход может быть из данной ситуации? В Java и других ОО-языках программирования существует механизм обобщенных типов. Этот механизм позволяет нам создавать классы, методы и интерфейсы, которые будут автоматически работать с типами данных, которые будут переданы позднее, при создании объекта этого класса.
Обобщенные классы
Реализуем класс Box с помощью механизма обобщенных типов
Такой класс называется обобщенным классом. Под термином обобщение следует понимать "применимость к большой группе классов". Создадим объект класса Box
После создания объекта обобщенного класса, вместо T будет подставлен тип Item
То есть, нам нет необходимости заниматься нисходящим преобразованием и приведением типов - объект класса Box будет работать с классом Item.
Механизм обобщения работает только с объектами
Когда объявляется экземпляр обобщенного типа, аргумент передаваемый параметру типа должен быть ссылочным типом. Использовать для этого примитивные типы, например, int или char, нельзя.
Для преодоления этой проблемы в Java предусмотрен механизм автоупаковки (autoboxing) и автораспаковки (unboxing), который позволяет использовать классы-оболочки типов данных (их еще называют "обертки", wrapper).
Механизм автоупаковки и автораспаковки
В пятой версии Java были добавлены два очень полезных механизма - автоупаковка и автораспаковка, которые существенно упрощающих и ускоряющих создание кода, в котором приходится преобразовывать простые типа данных в объекты и наоборот.
Как мы знаем, в Java предусмотрены 8 примитивных типов данных. Примитивные типы данных имеют преимущества по сравнению с объектами, но механизм обобщенных типов и многие структуры данных в Java предполагают работу с объектами и поэтому в них нельзя хранить данные простых типов.
Для решения этой проблемы в Java предусмотрены классы-обертки (wrappers). Классы-обертки реализуются в классах Double, Float, Long, Integer, Short, Byte, Character и Boolean. Все обертки числовых типов данных являются производными от абстрактного класса Number.
Процесс преобразования значения примитивного типа в объект соответствующего класса-обертки называется упаковкой (boxing).
Процесс извлечения значения примитивного типа из объекта-обертки называется распаковкой (unboxing).
Автоупаковка - процесс автоматической упаковки (инкапсуляции) простого типа данных в объектную обертку без необходимости явного создания объекта.
Автораспаковка - это обратный процесс автоматической распаковки (извлечения) значения, упакованного в объектную оболочку.
Передача нескольких параметризированных типов
Количество типов, которые можно передать в обобщенный класс, не ограничено. Чтобы передать несколько типов, нужно перечислить из через запятую. Синтаксис объявления ссылки и создания такого объекта аналогичен созданию объекта с одним параметров.
Ограничения типов
Очень часто при использовании обобщений, необходимо ограничить типы, которые могу быть переданы классу. Например, при написании обобщенного класса, который выполняет операции с числами, необходимо указать, что в качестве типа можно передать тип Number.
Для этого необходимо после placeholder написать ключевое слово extends и указать имя класса (в этом случае будут доступны объекты этого типа или типов-наследников) или интерфейса (объекты типов, которые реализуют интерфейс).
Если класс принимает несколько типов, то мы можем указать один тип в качестве ограничителя второго. Таким образом, можно гарантировать, что оба класса совместимы друг с другом.
Шаблон аргумента (wildcard)
Для того, чтобы иметь возможность использовать обобщенный класс в качестве аргумента метода, необходимо использовать так называемый "шаблон аргумента" (wildcard).
Предположим, нужно реализовать метод, который сравнивает два объекта класса NumericValue и возвращает true, если оба объекта равны.
Необходимо сообщить компилятору, что входным аргументом является объект обобщенного класса. Написать NumericValue<T> не получится, так как это не объявление класса, а записать какое-то конкретное значение мы не можем, так как мы хотим подать объект обобщенного типа с любым допустимым типом данных. Для такого случая используется специальный символ ?, который и является шаблоном аргумента.
Такая сигнатура метода означает, что метод isEquals() принимает на вход аргументы обобщенного типа NumericValue, где параметр типа может быть любым допустимым для типа NumericValue.
Ограничение снизу при использовании wildcard
Кроме "ограничения сверху", мы можем устанавливать "ограничение снизу", то есть установить в качестве корректного типа этот тип и суперклассы выше по цепочке наследования. Это реализуется с помощью ключевого слова super.
Обобщенные методы
Методы в обобщенных классах могут использовать параметр типа своего класса, а следовательно, автоматически становятся обобщенными относительно параметра класса. Однако можно объявить обобщенный метод, который сам по себе использует параметр типа. Более того, такой метод может быть объявлен в обычном, а не обобщенном классе.
К примеру, реализуем методы, который будет сравнивать два массива обобщенных типов. Сначала объявим два класса
Далее объявим метод compareArrays()
Выражение T extends Comparable<T> означает, что тип T должен реализовывать обобщенный интерфейс Comparable<T>, то есть чтобы мы могли сравнить объект типа T с другим объектом типа T. Выражение V extends T означает, что тип V должен быть или типом T или производным от T типом.
Создадим два массива и попытаемся вызвать метод compareArrays().
Такой код вызовет ошибку компиляции, так как тип Student не реализует интерфейс Comparable<Student>.
Исправим данную ошибку
Если мы перепишем класс PostGradStudent так, чтобы он не является наследником типа Student, то при исполнении кода получим следующую ошибку
Обобщенный конструктор
Так как конструктор является методом, то мы также можем объявлять обобщенные конструкторы. Обобщенный конструктор можно объявить даже тогда, когда сам класс не является обобщенным.
Обобщенный интерфейс
Обобщенный интерфейс объявляется также как и обобщенный класс
Если класс реализует обобщенный интерфейс, то он также должен быть обобщенным. Не обобщенным он может быть только в том случае, если вы явно указываете тип при объявлении класса.
В интерфейсе можно указать ограничение с помощью ключевого слова extends. Класс, который реализует обобщенный интерфейс с ограничением, также должен соблюдать эти ограничения. При объявлении класса ограничение следует прописать после названия класса, но после ключевого слова implements его дублировать не надо.
Соглашение по правилам названия параметров типа
Согласно общепринятым правилам и конвенции кода, параметры типов записываются в виде одного символа алфавита в верхнем регистре. Рекомендуется использовать следующие символы алфавита:
E (означает Element, часто используется в коллекциях);
K (означает Key);
N (означает Number);
T (означает Type);
V (означает Value);
S, U - обычно вторые, третьи, четвертые параметры типа.
Лекция 11
Тема 11: Статические поля и методы. Внутренние и статические вложенные классы. Исключения в Java. Принципы обработки исключений. Checked и unchecked исключения. Блок finally. Конструкция try-with-resources.
Обычно при создании класса вы описываете, как объекты этого класса ведут себя и как они выглядят. Объект появляется только после того, как он будет создан ключевым словом new, и только начиная с этого момента для него выделяется память и появляется возможность вызова методов.
Но есть ситуации, в которых такой подход недостаточен. Первая ситуация - это когда некоторые данные должны храниться "в единственном числе" независимо от того, сколько было создано объектов класса. Вторая - когда вам потребуется метод, не привязанный ни к какому конкретному объекту класса (то есть метод, который можно вызвать даже при полном отсутствии объектов класса).
Такой эффект достигается путем использования ключевого слова static, делающего элемент класса статическим. Поля или методы, которые объявлены как static, не привязаны к определенному экземпляру этого класса. Поэтому даже если в программе еще не создавался ни один объект класса, вы можете вызывать статический метод или получить доступ к статическим полям. С обычным объектом вам необходимо сначала создать его и использовать для вызова метода или доступа к полям, так как нестатические данные должны точно знать объект, с которым работают.
Некоторые ОО языки используют термин данные уровня класса и методы уровня класса, подразумевая, что данные и методы существуют только на уровне класса в целом, а не для отдельных объектов этого класса.
Понятие статического поля
Чтобы сделать данные или метод статическими, просто поместите ключевое слово static перед их определением. Например, следующий код создает статическое поле класса и инициализирует его:
Теперь, даже при создании двух объектов StaticTest, для элемента StaticTest.val выделяется единственный блок памяти. Оба объекта совместно используют одно значение val. Пример:
В данном примере как st1.val, так и st2.val имеют одинаковые значения, равные 924, потому что они ссылаются на один блок памяти.
Существует два способа обратиться к статической переменной. Как было видно выше, на нее можно ссылаться по имени объекта, например st2.val. Также возможно обратиться к ней прямо через имя класса; для нестатических членов класса такая возможность отсутствует.
После выполнения операции, значения st1.val и st2.val будут равны 48.
Синтаксис с именем класса является предпочтительным, потому что он не только подчеркивает, что переменная является статической, но и в некоторых случаях предоставляет компилятору больше возможностей для оптимизации.
Если для инициализации статических переменных требуется произвести вычисления, то для этой цели достаточно объявить статический блок, который будет выполняться только один раз при первой загрузке класса. Пример:
Как только загружается класс NetworkConnector, выполняются все действия в статическом блоке.
Понятие статического метода
Та же логика верна и для статических методов. Вы можете обратиться к такому методу или через объект, как это делается для всех методов, или в специальном синтаксисе ИмяКласса.метод(). Статические методы определяются по аналогии со статическими данными:
Нетрудно заметить, что метод increment() класса Incrementable увеличивает значение статического поля val. Метод можно вызвать стандартно, через объект:
Или, поскольку метод increment() является статическим, можно вызвать его с прямым указанием класса:
Если применительно к полям ключевое слово static радикально меняет способ определения данных (статические данные существуют на уровне класса, в то время как нестатические данные существуют на уровне объектов), то в отношении методов изменения не столь принципиальны. Одним из важных применений static является определение методов, которые могут вызываться без объектов.
На статические методы налагаются следующие ограничения:
они могут непосредственно вызывать только другие статические методы;
им непосредственно доступны только статические переменные;
им недоступны ключевые слова this или super.
Статический импорт
В языке Java имеется языковое средство, расширяющее возможности ключевого слова import и называемое статическим импортом. Оператор import, предваряемый ключевым словом static, можно применять для импорта статических членов класса или интерфейса. Благодаря статическому импорту появляется возможность ссылаться на статические члены непосредственно по именам, не используя имя класса. Это упрощает и сокращает синтаксис, требующийся для работы со статическими членами.
Рассмотрим пример без использования статического импорта. Представим себе, что мы вычисляем гипотенузу прямоугольного треугольника. Мы будем часто использовать методы Math.pow() и Math.sqrt()
Как мы видим, строка
получается слишком громоздкой. Подобных неудобств можно избежать, если воспользоваться статическим импортом. Рассмотрим пример ниже
После использования статического импорта нет нужды использовать имя класс Math для вызова статических методов pow() и sqrt().
Если предполагается применять много статических методов или полей, определенных в классе, то можно импортировать класс Math полностью
Каким бы удобным ни был статический импорт, очень важно не злоупотреблять им. Не следует забывать, что библиотечные классы Java объединяются в пакеты для того, чтобы избежать конфликтов пространств имен и непреднамеренного сокрытия прочих имен. Если статический член используется в программе один или два раза, то его лучше не импортировать. Статический импорт следует оставить на тот случай, если статический члены применяются многократно, в частности при выполнении математических вычислений.
В Java определены вложенные классы. Вложенным называется такой класс, который объявляется в другом классе.
Вложенный класс не может существовать независимо от класса, в который он вложен. Следовательно, область действия вложенного класса ограничена его внешним классом. Если вложенный класс объявлен в пределах области действия внешнего класса, то он становится членом последнего. Имеется также возможность объявить вложенный класс, который станет локальным в пределах блока
Существуют два типа вложенных классов. Одни вложенные классы объявляются с помощью модификатора доступа static, а другие - без него. Нестатический вариант вложенных классов называются внутренними.
Внутренние классы позволяют вам группировать классы, логически принадлежащие друг другу, и управлять доступом к ним. Однако важно понимать, что внутренние классы заметно отличаются от композиции.
Внутренний класс может взаимодействовать со своим внешним классом, а код, написанный с использованием внутренних классов, получается более элегантным и понятным.
Создание внутренних классов
Создать внутренний класс несложно - достаточно разместить определение класса внутри окружающего класса.
Внутренний класс имеет доступ ко всем переменным и методам внешнего класса, в который он вложен, и может обращаться к ним непосредственно, как и все остальные нестатические члены внешнего класса.
Иногда внутренний класс используется для предоставления ряда услуг внешнему классу, в котором он содержится.
Класс можно вложить в область действия блока. В итоге получается локальный класс, недоступный за пределами блока.
Также внутренний класс можно быть анонимным классом (анонимные классы уже были рассмотрены ранее).
Исключения
Исключение (exception) - это ненормальная ситуация (термин "исключение" здесь следует понимать как "исключительная ситуация"), возникающая во время выполнения программного кода. Иными словами, исключение - это ошибка, возникающая во время выполнения программы (в runtime).
Исключение - это способ системы Java (в частности, JVM - виртуальной машины Java) сообщить вашей программе, что в коде произошла ошибка. К примеру, это может быть деление на ноль, попытка обратиться к массиву по несуществующему индексу, очень распространенная ошибка нулевого указателя (NullPointerException) - когда вы обращаетесь к ссылочной переменной, у которой значение равно null и так далее.
В любом случае, с формальной точки зрения, Java не может продолжать выполнение программы.
Обработка исключений (exception handling) - название объектно-ориентированной техники, которая пытается разрешить эти ошибки.
Программа в Java может сгенерировать различные исключения, например:
программа может пытаться прочитать файл из диска, но файл не существует;
программа может попытаться записать файл на диск, но диск заполнен или не отформатирован;
программа может попросить пользователя ввести данные, но пользователь ввел данные неверного типа;
программа может попытаться осуществить деление на ноль;
программа может попытаться обратиться к массиву по несуществующему индексу.
Используя подсистему обработки исключений Java, можно управлять реакцией программы на появление ошибок во время выполнения. Средства обработки исключений в том или ином виде имеются практически во всех современных языках программирования. В Java подобные инструменты отличаются большей гибкостью, понятнее и удобнее в применении по сравнению с большинством других языков программирования.
Преимущество обработки исключений заключается в том, что она предусматривает автоматическую реакцию на многие ошибки, избавляя от необходимости писать вручную соответствующий код.
Иерархия исключений
В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable имеются два непосредственных подкласса: Exception и Error.
Исключения типа Error относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в приложении, как правило, не предусматривается. В связи с этим исключения данного типа не будут рассматриваться в книге.
Ошибки, связанные с работой программы, представлены отдельными подклассами, производными от класса Exception. В частности, к этой категории относятся ошибки деления на нуль, выхода за пределы массива и обращения к файлам. Подобные ошибки следует обрабатывать в самой программе. Важным подклассом, производным от Exception, является класс RuntimeException, который служит для представления различных видов ошибок, часто возникающих во время выполнения программ.
Каждой исключительной ситуации поставлен в соответствие некоторый класс. Если подходящего класса не существует, то он может быть создан разработчиком.
Так как в Java ВСЁ ЯВЛЯЕТСЯ ОБЪЕКТОМ, то исключение тоже является объектом некоторого класса, который описывает исключительную ситуацию, возникающую в определенной части программного кода.
«Обработка исключений» работает следующим образом:
когда возникает исключительная ситуация, JVM генерирует (говорят, что JVM ВЫБРАСЫВАЕТ исключение, для описания этого процесса используется ключевое слово throw) объект исключения и передает его в метод, в котором произошло исключение;
вы можете перехватить исключение (используется ключевое слово catch), чтобы его каким-то образом обработать. Для этого, необходимо определить специальный блок кода, который называется обработчикомисключений, этот блок будет выполнен при возникновении исключения, код должен содержать реакцию на исключительную ситуацию;
таким образом, если возникнет ошибка, все необходимые действия по ее обработке выполнит обработчик исключений.
Если вы не предусмотрите обработчик исключений, то исключение будет перехвачено стандартным обработчиком Java. Стандартный обработчик прекратит выполнение программы и выведет сообщение об ошибке.
Рассмотрим пример исключения и реакцию стандартного обработчика Java.
Мы видим, что стандартный обработчик вывел в консоль сообщение об ошибке. Давайте разберемся с содержимым этого сообщения:
Строка
сообщает нам тип исключения, а именно класс ArithmeticException (про классы исключений мы будем говорить позже), после чего сообщает, какая именно ошибка произошла. В нашем случае это деление на ноль.
Далее сообщается
в каком классе, методе и строке произошло исключение. Используя эту информацию, мы можем найти ту строчку кода, которая привела к исключительной ситуации, и предпринять какие-то действия. Строки
называются «трассировкой стека» (stack tracing). О каком стеке идет речь? Речь идет о стеке вызовов (call stack). Соответственно, эти строки означают последовательность вызванных методов, начиная от метода, в котором произошло исключение, заканчивая самым первым вызванным методом.
Для вызова методов в программе используется инструкция «call». Когда вы вызываете метод в программе, важно сохранить адрес следующей инструкции, чтобы, когда вызванный метод отработал, программа продолжила работу со следующей инструкции. Этот адрес нужно где-то хранить в памяти. Также перед вызовом необходимо сохранить аргументы функции, которые тоже необходимо где-то хранить.
Вся эта информация хранится в специальной структуре – стеке вызовов. Каждая запись в стеке вызовов называется кадром или фреймом (stack frame).
Таким образом, зная, какая строка привела к возникновению исключения, вы можете изменить код либо предусмотреть обработчик событий.
Классы исключений
Как уже было сказано выше, исключение это объект некоторого класса. В Java существует разветвленная иерархия классов исключений.
ВJava, класс исключения служит для описания типа исключения. Например, класс NullPointerException описывает исключение нулевого указателя, а FileNotFoundException означает исключение, когда файл, с которым пытается работать приложение, не найден. Рассмотрим иерархию классов исключений:
На самом верхнем уровне расположен класс Throwable, который является базовым для всех исключений (как мы помним, JVM «выбрасывает» исключение», поэтому класс Throwable означает – то, что может «выбросить» JVM).
От класса Throwable наследуются классы Error и Exception. Среди подклассов Exception отдельно выделен класс RuntimeException, который играет важную роль в иерархии исключений.
Виды исключений
В Java существует некоторая неопределенность насчет того – существует ли два или три вида исключений.
Если делить исключения на два вида, то это:
контролируемые исключения (checked exceptions) – подклассы класса Exception, кроме подкласса RuntimeException и его производных;
неконтролируемые исключения (unchecked exceptions) – класс Error с подклассами, а также класс RuntimeException и его производные;
В некоторых источниках класс Error и его подклассы выделяют в отдельный вид исключений - ошибки (errors).
КлассError
Далее мы видим класс Error. Классы этой ветки составляют вид исключений, который можно обозначить как «ошибки» (errors). Ошибки представляют собой серьезные проблемы, которые не следует пытаться обработать в собственной программе, поскольку они связаны с проблемами уровняJVM.
На самом деле, вы конечно можете предпринять некоторые действия при возникновении ошибок, например, вывести сообщение для пользователя в удобном формате, выслать трассировку стека себе на почту, чтобы понять – что вообще произошло.
Но, по факту, вы ничего не можете предпринять в вашей программе, чтобы эту ошибку исправить, и ваша программа, как правило, при возникновении такой ошибки дальше работать не может.
В качестве примеров «ошибок» можно привести: переполнение стека вызова (класс StackOverflowError); нехватка памяти в куче (класс OutOfMemoryError), вследствие чего JVM не может выделить память под новый объект и сборщик мусора не помогает; ошибка виртуальной машины, вследствие которой она не может работать дальше (класс VirtualMachineError) и так далее.
Несмотря на то, что в нашей программе мы никак не можем помочь этой проблеме, и приложение не может работать дальше (ну как может работать приложение, если стек вызовов переполнен или JVM не может дальше выполнять код?!); знание природы этих ошибок поможет вам предпринять некоторые действия, чтобы избежать этих ошибок в дальнейшем. Например, ошибки типа StackOverflowError и OutOfMemoryError могут быть следствием вашего некорректного кода.
Например, попробуем спровоцировать ошибку StackOverflowError
Получим такое сообщение об ошибке
Ошибка OutOfMemoryError может быть вызвана тем, что ваш код, вследствие ошибки при программировании, создает очень большое количество массивных объектов, которые очень быстро заполняют кучу и свободного места не остается.
Ошибка VirtualMachineError может означать, что следует переустановить библиотеки Java.
В любом случае, следует относиться к типу Error не как к неизбежному злу и «воле богов», а просто как к сигналу к тому, что в вашем приложении что-то не так, или что-то не так с программным или аппаратным обеспечением, которое вы используете.
КлассException
Класс Exception описывает исключения, связанные непосредственно с работой программы. Такого рода исключения «решаемы» и их грамотная обработка позволит программе работать дальше в нормальном режиме.
В классе Exception описаны исключения двух видов: контролируемые исключения (checked exceptions) и неконтролируемые исключения (unchecked exceptions).
Неконтролируемые исключения содержатся в подклассе RuntimeException и его наследниках. Контролируемые исключения содержатся в остальных подклассах Exception.
В чем разница между контролируемыми и неконтролируемыми исключениями, мы узнаем позже, а теперь рассмотрим вопрос – а как же именно нам обрабатывать исключения?
Обработка исключений
Обработка исключений в методе может выполняться двумя способами:
с помощью связки try-catch;
с помощью ключевого слова throws в сигнатуре метода.
Рассмотрим оба метода поподробнее:
Способ 1. Связкаtry-catch
Этот способ кратко можно описать следующим образом.
Код, который теоретически может вызвать исключение, записывается в блоке try{}. Сразу за блоком try идет блок код catch{}, в котором содержится код, который будет выполнен в случае генерации исключения. В блоке finally{} содержится код, который будет выполнен в любом случае – произошло ли исключение или нет.
Теперь разберемся с этим способом более подробно. Рассмотрим следующий пример – программу, которая складывает два числа, введенные пользователем из консоли
Первое, что нам нужно определить – и что является главным при работе с исключениями, КАКАЯ ИНСТРУКЦИЯ МОЖЕТ ПРИВЕСТИ К ВОЗНИКНОВЕНИЮ ИСКЛЮЧЕНИЯ?
То есть, мы должны понять – где потенциально у нас может возникнуть исключение? Понятно, что речь идет не об операции сложения и не об операции чтения данных из консоли. Потенциально опасными строчками кода здесь являются строчки
в которых происходит преобразование ввода пользователя в целое число (метод parseInt() преобразует цифры в строке в число).
Почему здесь может возникнуть исключение? Потому что пользователь может ввести не число, а просто какой-то текст и тогда непонятно – что записывать в переменную a или b. И да, действительно, если пользователь введет некорректное значение, возникнет исключение в методе Integer.parseInt().
Итак, что мы можем сделать. «Опасный код» нужно поместить в блок try{}
Обратите внимание на синтаксис блока try. В самом простом случае это просто ключевое слово try, после которого идут парные фигурные скобки. Внутри этих скобок и заключается «опасный» код, который может вызвать исключение. Сразу после блока try должен идти блок catch().
Обратите внимание на синтаксис блока catch. После ключевого слова, в скобках описывается аргумент с именем e типа NumberFormatException.
Когда произойдет исключение, то система Java прервет выполнение инструкций в блоке try и передаст управление блоку catch и запишет в этот аргумент объект исключения, который сгенерировала Java-машина.
То есть, как только в блоке try возникнет исключение, то дальше инструкции в блоке try выполняться не будут! А сразу же начнут выполняться действия в блоке catch.
Обработчик исключения находится в блоке catch, в котором мы можем отреагировать на возникновение исключения. Также, в этом блоке нам будет доступен объект исключения, от которого мы можем получить дополнительные сведения об исключении.
Блок catch сработает только в том случае, если указанный в скобках тип объекта исключения будет суперклассом или будет того же типа, что и объект исключения, который сгенерировала Java.
Например, если в нашем примере мы напишем код, который потенциально может выбросить исключение типа IOException, но не изменим блок catch
тогда обработчик не будет вызван и исключение будет обработано стандартным обработчиком Java.
Способ 2. Использование ключевого словаthrows
Второй способ позволяет передать обязанность обработки исключения тому методу, который вызывает данный метод (а тот, в свою очередь может передать эту обязанность выше и т.д.).
Изменим наш пример и выделим в отдельный метод код, который будет запрашивать у пользователя число и возвращать его как результат работы метода
Мы понимаем, что в данном методе может произойти исключение, но мы не хотим или не можем его обработать. Причины могут быть разными, например:
обработка исключений может происходить централизованно однотипным способом (например, показ окошка с сообщением и с определенным текстом);
это не входит в нашу компетенцию как программиста – обработкой исключений занимается другой программист;
мы пишем только некоторую часть программы и непонятно – как будет обрабатывать исключение другой программист, который потом будет использовать наш код (например, мы пишем просто какую-то библиотеку, которая производит вычисления, и как будет выглядеть обработка – это не наше дело).
В любом случае, мы знаем, что в этом коде может быть исключение, но мы не хотим его обрабатывать, а хотим просто предупредить другой метод, который будет вызывать наш код, что выполнение кода может привести к исключению. В этом случае, используется ключевое слово throws, которое указывается в сигнатуре метода
Обратите внимание на расположение сигнатуру метода. Мы привыкли, что при объявлении метода сразу после скобок входных аргументов мы открываем фигурную скобку и записываем тело метода. Здесь же, после входных аргументов, мы пишем ключевое слово throws и потом указываем тип исключения, которое может быть сгенерировано в нашем методе. Если метод может выбрасывать несколько типов исключений, они записываются через запятую
Тогда, в методе main мы должны написать примерно следующее
Основное преимущество этого подхода – мы передаем обязанность по обработке исключений другому, вышестоящему методу.
Отличия между контролируемыми и неконтролируемыми исключениями
Если вы вызываете метод, который выбрасывает checked исключение, то вы ОБЯЗАНЫ предусмотреть обработку возможного исключения, то есть связку try-catch.
Яркий пример checked исключения – класс IOException и его подклассы.
Рассмотрим пример – попробуем прочитать файл и построчно вывести его содержимое на экран консоли:
Как мы видим, компилятор не хочет компилировать наш код. Чем же он недоволен? У нас в коде происходит вызов двух методов – статического метода Files.newBufferedReader() и обычного метода BufferedReader.readLine().
Если посмотреть на сигнатуры этих методов то можно увидеть, что оба этих метода выбрасывают исключения типа IOException. Этот тип исключения относится к checked-исключению и поэтому, если вы вызываете эти методы, компилятор ТРЕБУЕТ от вас предусмотреть блок catch, либо в самом вашем методе указать throws IOException и, таким образом, передать обязанность обрабатывать исключение другому методу, который будет вызывать ваш.
Таким образом, «оборачиваем» наш код в блок try и пишем блок catch.
Еще один способ - указать в сигнатуре метода, что он выбрасывает исключение типа IOException и переложить обязанность обработать ошибку в вызывающем коде
Eсли метод выбрасывает checked-исключение, то проверка на наличие catch-блока происходит на этапе компиляции. И вы обязаны предусмотреть обработку исключения для checked-исключения.
Что касается unchecked-исключения, то обязательной обработки исключения нет – вы можете оставить подобные ситуации без обработки.
Зачем необходимо наличие двух видов исключений?
В большинстве языков существует всего лишь один тип исключений – unchecked. Некоторые языки, например, C#, в свое время отказались от checked-исключений.
Во-первых, мы не можем сделать все исключения checked, т.к. очень многие операции могут генерировать исключения, и если каждый такой участок кода «оборачивать» в блок try-catch, то код получится слишком громоздким и нечитабельным.
С другой стороны, зачем нужно делать некоторые типы исключений checked? Почему просто не сделать все исключения unchecked и оставить решения об обработке исключений целиком на совести программиста?
В официальной документации написано, что unchecked-исключения – это те исключения, от которых программа «не может восстановиться», тогда как checked-исключения позволяют откатить некоторую операцию и повторить ее снова.
На самом деле, если вы посмотрите на различные типы unchecked-исключений, то вы увидите, что большинство их связаны с ошибками самого программиста. Выход за пределы массива, исключение нулевого указателя, деление на ноль – большинство из подобного рода исключений целиком лежат на совести программистов. Тогда мы можем сказать, что лучше программист пишет более хороший код, чем везде вставляет проверки на исключения.
Контролируемые исключения, как правило, представляют те ошибки, которые возникают не из-за программиста и предусмотреть которые программист не может. Например, это отсутствующие файлы, работа с сокетами, подключение к базе данных, сетевые соединения, некорректный пользовательский ввод.
Вы можете написать идеальный код, но потом вы отдадите приложение пользователю, а он введет название файла, которого нет или напишет неправильный IP для сокет-соединения. Таким образом, мы заранее должны быть готовыми к неверным действиям пользователя или к программным или аппаратным проблемам на его стороне и в обязательном порядке предусмотреть обработку возможных исключений.
Дополнительно об исключениях
Рассмотрим детально различные возможности механизма исключений, которые позволяют программисту максимально эффективно противодействовать исключениям:
Несколько блоков catch
Java позволяет вам для одного блока try предусмотреть несколько блоков catch, каждый из которых должен обрабатывать свой тип исключения
Важно помнить, что Java обрабатывает исключения последовательно. Java просматривает блок catch сверху вниз и выполняет первый подходящий блок, который может обработать данное исключение.
Так как вы можете указать как точный класс, так и суперкласс, то если первым блоком будет блок для суперкласса – выполнится он. Например, исключение FileNotFoundException является подклассом IOException. И поэтому если вы первым поставите блок с IOException – он будет вызываться для всех подтипов исключений, в том числе и для FileNotFoundException и блок c FileNotFoundException никогда не выполнится.
Один блок для обработки нескольких типов исключений
Начиная с версии Java 7, вы можете использовать один блок catch для обработки исключений нескольких, не связанных друг с другом типов. Приведем пример
Как мы видим, один блок catch используется для обработки и типа IOException и NullPointerException и NumberFormaException.
Вложенные блоки try
Вы можете использовать вложенные блоки try, которые могут помещаться в других блоках try. После вложенного блока try обязательно идет блок catch
Выбрасывание исключения с помощью ключевого словаthrow
С помощью ключевого слова throw вы можете преднамеренно «выбросить» определенный тип исключения.
Блокfinally
Кроме блока try и catch существует специальный блок finally. Его отличительная особенность – он гарантированно отработает, вне зависимости от того, будет выброшено исключение в блоке try или нет. Как правило, блок finally используется для того, чтобы выполнить некоторые "завершающие" операции, которые могли быть инициированы в блоке try.
При любом развитии события в блоке try, код в блоке finally отработает в любом случае.
Блок finally отработает, даже если в try-catch присутствует оператор return.
Как правило, блок finally используется, когда мы в блоке try работаем с ресурсами (файлы, базы данных, сокеты и т.д.), когда по окончании блока try-catch мы освобождаем ресурсы. Например, допустим, в процессе работы программы возникло исключение, требующее ее преждевременного закрытия. Но в программе открыт файл или установлено сетевое соединение, а, следовательно, файл нужно закрыть, а соединение – разорвать. Для этого удобно использовать блок finally.
Блок try-with-resources
Блок try-with-resources является модификацией блока try. Данный блок позволяет автоматически закрывать ресурс после окончания работы блока try и является удобной альтернативой блоку finally.
Внутри скобок блока try объявляется один или несколько ресурсов, которые после отработки блока try-catch будут автоматически освобождены. Для этого объект ресурса должен реализовывать интерфейс java.lang.AutoCloseable.
Создание собственных подклассов исключений
Встроенные в Java исключения позволяют обрабатывать большинство распространенных ошибок. Тем не менее, вы можете создавать и обрабатывать собственные типы исключений. Для того, чтобы создать класс собственного исключения, достаточно определить как его произвольный от Exception или от RuntimeException (в зависимости от того, хотите ли вы использовать checked или unchecked – исключения).
Насчет создания рекомендуется придерживаться двух правил:
определитесь, исключения какого типа вы хотите использовать для собственных исключений (checked или unchecked) и старайтесь создавать исключения только этого типа;
старайтесь максимально использовать стандартные типы исключений и создавать свои типы только в том случае, если существующие типы исключений не отражают суть того исключения, которое вы хотите добавить.
Плохие практики при обработке исключений
Ниже представлены действия по обработке ошибок, которые характерны для плохого программиста. Ни в коем случае не рекомендуется их повторять!
Указание в блоке catch объекта исключения типа Exception. Существует очень большой соблазн при создании блока catch указать тип исключения Exception и, таким образом, перехватывать все исключения, которые относятся к этому классу (а это все исключения, кроме системных ошибок). Делать так крайне не рекомендуется, т.к. вместо того чтобы решать проблему с исключениями, мы фактически игнорируем ее и просто реализуем некоторую «заглушку», чтобы приложение продолжило работу дальше. Кроме того, каждый тип исключения должен быть обработан своим определенным образом.
Помещение в блок try всего тела метода. Следующий плохой прием используется, когда программист не хочет разбираться с кодом, который вызывает исключение и просто, опять же, реализует «заглушку». Этот прием очень "хорошо" сочетается с первым приемом. В блок try должен помещаться только тот код, который потенциально может вызвать исключение, а не всё подряд, т.к. лень обрабатывать исключения нормально.
Игнорирование исключения. Следующий плохой прием состоит в том, что мы просто игнорируем исключение и оставляем блок catch пустым. Программа должна реагировать на исключения и должна информировать пользователя и разработчика о том, что что-то пошло не так. Безусловно, исключение это не повод тут же закрывать приложение, а попытаться повторить то действие, которое привело к исключению (например, повторно указать название файла, попытаться открыть базу данных через время и т.д.). В любом случае, когда приложение в ответ на ошибку никак не реагирует – не выдает сообщение, но и не делает того, чего от нее ожидали – это самый плохой вариант.
Лекция 14
Тема: Принцип Separation of Concerns. Контроллер и представление. Паттерн MVС. Паттерн Observer. Разработка приложения с соблюдением архитектуры MVC.
Отделение графического интерфейса от бизнес-логики приложения. Язык FXML.
До этого момента мы создавали графический интерфейс напрямую в коде приложения, явно создавая объекты графического интерфейса, меняли их свойства, назначали слушатели событий и так далее.
Такой подход обладает рядом недостатков:
такой программный код превращается в череду созданий объектов и вызовов методов для их настройки, что затрудняет чтение кода и приводит к тому, что он выглядит не лучшим образом;
такой код тяжело отлаживать и исправлять ошибки при реализации графического интерфейса. Необходимо постоянно компилировать код и смотреть полученный результат в работающем приложении;
графический интерфейс, как правило, проектируется отдельным специалистом в области UI/UX, который может не знать языка, на котором этот интерфейс проектируется (в нашем случае, это Java). Кроме того, это приводит к тому, что java-программист, кроме программирования логики приложения, должен проектировать GUI, что приводит к дополнительным материальным затратам;
в случае необходимости изменения графического интерфейса (добавления нового функционала, изменения внешнего вида, анимации, эффектов), необходимо «перелопачивать» код и вносить туда изменения, что приводит к многочисленным ошибкам и тормозит процесс разработки программного обеспечения.
Основополагающим принципом разработки программного обеспечения является принцип separation of concerns
Исходя из этого, следует запомнить первое правило, которому должен следовать любой программист при разработке даже самого простого приложения: внешний вид (пользовательский интерфейс) приложения всегда должен быть максимально отделен от бизнес-логики работы приложения. Этот принцип еще кратко называют принципом отделения представления от содержания (эти принципы называются «Separation of concerns» или «Separation of presentation and content»).
В общем случае, принцип separation of concerns формулируется следующим образом - программа должна быть разделена на функциональные блоки, как можно меньше перекрывающие функции друг друга.
Повторим еще раз, что пользовательский интерфейс и внутренняя логика работы программы должны быть максимально независимы друг от друга. В идеале, вашей программе должно быть все равно – в каком виде она представлена пользователю – в виде GUI, консольного приложения, в окне браузера или еще как-то. В идеале, при изменении GUI, остальным классы программы остаются нетронутыми и изменения в них вносить не надо (в реальности, конечно, изменения вноситься будут, но они должны быть минимальны). Это помогает сделать программу более гибкой, а также помогает добиться разделения труда: программист пишет код, а дизайнер (верстальщик, специалист по UI/UX) занимается проектированием графического интерфейса.
Подавляющее большинство современных графических библиотек и фреймворков обладают тем или иным механизмом отделения представления от содержания.
Давайте разберемся, как это реализовано в JavaFX. Рассмотрим простой пример. Создадим пустое JavaFX-приложение
В примере определен объект Stage, объект Scene и корневой элемент графа сцены. Очевидным следующим шагом было бы создание новых объектов графического интерфейса, установка свойств объектов и формирование графа сцены.
Но в нашем случае мы будем использовать специальный язык для проектирования графического интерфейса, который называется FXML.
Выполним несколько шагов, чтобы настроить работу IntelliJ IDEA для использования языка FXML:
зайдем в настройки IntelliJ IDEA (Ctrl+Alt+S или File -> Settings) и в пункте JavaFX укажем путь к exe-файлу установленного SceneBuilder;
перезапустим IntelliJ IDEA.
Откроем наш проект с JavaFX-приложением и создадим в проекте новый FXML файл sample.fxml.
Давайте разберемся, зачем мы создали этот файл и зачем нам нужен язык FXML. В JavaFX пользовательский интерфейс можно описать с помощью специального языка разметки, который называется FXML. Язык FXML является подмножеством языка XML и немного напоминает HTML.
Таким образом, вместо того, чтобы создавать объекты пользовательского интерфейса на языке Java в исходном коде программы, мы описываем граф сцены в виде fxml-файла, который выглядит определенным образом. Благодаря тому, что XML позволяет описать любые данные, мы можем очень точно описать объекты и их свойства, а древовидная структура XML идеально описывает граф сцены, который тоже имеет структуру дерева.
Откроем файл sample.fxml и посмотрим его содержимое
Как мы видим, текст файла напоминает формат XML, в котором присутствуют некоторые элементы языка Java (в частности, подключение библиотек с помощью import).
Опишем граф сцены из нескольких элементов, после чего разберем, что происходит в получившемся коде.
Итак, давайте попробуем разобраться в этом коде:
строка 1 – в ней содержится так называемая XML-декларация. Она описывает версию XML и кодировку;
строки 3 – 6 отдаленно напоминают команды импорта в java, которые заключены между символами <? и ?>. Такая конструкция называется инструкцией обработки (Processing Instruction, PI), фактически, это инструкция тому приложению, которое будет «читать» XML-файл. Фактически, это означает инструкцию – подключить те или иные библиотеки;
строки 8-10 определяет корневой элемент графа сцены – элемент HBox. В качестве атрибутов xmlns указаны пространства имен для тэгов FXML. Все остальные элементы размещаются внутри HBox, что в точности соответствует структуре дерева;
в строке 11 указан элемента <children>. Потомки того или иного элемента содержатся внутри элемента <children>, что напоминает метод getChildren(), который мы используем для добавления потомков элемента;
свойства элементов (высота, ширина, текст надписи и так далее) описываются в виде атрибутов тех или иных элементов.
Таким образом, данное описание довольно понятно и легко читается человеком, который хоть сколько-нибудь знаком с форматом XML и с элементами JavaFX.
Граф сцены, описанный в файле sample.fxml с помощью традиционного способа создания объектов Java в исходном коде выглядит следующим образом:
Визуальные редакторы UI
Использование специального языка описания FXML дает нам возможность использовать специальные визуальные редакторы, которые позволяют нам проектировать граф сцены в визуальном режиме.
В качестве визуального редактора мы будем использовать редактор SceneBuilder, который мы установили и подключили выше.
При работе с fmxl-файлами вы можете легко переключаться между текстовым и визуальным представлением файла, редактировать код, который сгенерировал визуальный редактор и видеть результат без необходимости каждый раз компилировать и запускать приложение.
Использование редактора очень сильно облегчает и ускоряет разработку графических приложений. Кроме того, при работе с fxml очень удобно подключать css-стили и даже вызывать скрипты на различных языках программирования, например, например, Groovy, Clojure и даже JavaScript (данный вопрос в рамках этого курса не рассматривается).
Используя визуальный редактор, создадим граф сцены, который визуально будет выглядеть следующим образом
В формате XML этот код будет выглядеть следующим образом:
У нас есть готовый граф сцены, но что дальше? Как известно, в Java всё является объектами некоторых классов, в том числе и элементы графического интерфейса.
До этого мы просто создавали в исходном коде объекты нужных нам классов, после чего вызывали их методы для формирования нужного нам графа сцены. Но у нас только текстовое описание графа сцены в формате FXML, как его преобразовать в набор объектов, вызвать нужные методы этих объектов и сформировать граф сцены?
Генерация объектов графа сцены. ЗагрузчикFXML
Для решения вышеуказанной проблемы было придумано много различных методик и подходов, которые были реализованы в различных графических библиотеках в различных языках программирования.
Фреймворк JavaFX использует распространенный подход, который состоит в генерации объектов. Его суть состоит в следующем: в JavaFX существует специальный класс FXMLLoader (загрузчик FXML), который содержит статический метод load(). Этот метод реализует следующий функционал:
метод считывает fxml-файл, URL которого вы должны указать;
метод «парсит» fxml-файл (разбивает файл на отдельные элементы с атрибутами);
используя результаты парсинга и механизм рефлексии (механизм рефлексии в данном курсе не рассматривается), метод создает объекты, устанавливает их свойства и помещает один объект в состав других объектов и таким образом формирует нужный нам граф сцены;
в качестве возвращаемого значения, метод load() возвращает ссылку на корневую вершину (root node), которая прямо или опосредованно содержит в себе весь сгенерированный граф сцены.
Вернемся к нашему исходному коду. Используя полеченные знания о работе класса FXML, выполним следующие шаги:
ссылку на корневой элемент графа сцены передадим в созданный объект сцены.
Полученный код выглядит следующим образом.
Запустим приложение и убедимся, что сгенерированный граф сцены соответствует тому, что мы визуально спроектировали в SceneBuilder.
Таким образом, мы визуально спроектировали граф сцены для нашего окна, после чего класс FXMLLoader сгенерировал нужный граф сцены в объектном виде.
Но сразу же возникает следующая проблема, каким образом получить ссылки на эти сгенерированные объекты? Если мы хотим указать обработчик событий для элементов графического интерфейса или динамически изменить свойства некоторых элементов – нам нужна ссылка на объекты элементов графического интерфейса.
Когда мы создавали объекты самостоятельно в исходном коде классов, мы хранили ссылку на них и могли к ним обратиться. Но в нашем случае, объекты были сгенерированы в недрах класса FXMLLoader, упакованы один в другой и всё, что у нас есть – ссылка на корневой элемент графа сцены.
Мы могли бы попытаться получить ссылку на нужный элемент через объект корневого элемента графа сцены, но это сильно затруднит написание обработчиков событий и бизнес-логики приложения в целом.
Контроллер графа цены. Получения ссылок на объекты графа сцены.
Получение ссылки на сгенерированный элемент графического интерфейса – это одна из проблем, которая возникает, когда объекты графического интерфейса создаются не вручную. В различных графических библиотеках и в разных языках программирования эта проблема решается по-разному. Давайте рассмотрим, как эта проблема решается в JavaFX.
При описании интерфейса, в fxml-файле вы можете указать т.н. контроллер – специальный класс, который будет «управлять» объектами, которые описаны вfxml-файле. Почему этот класс называется контроллером и что такое вообще «контроллер» (вы, наверняка, слышали этот термин и даже что-то читали по этому поводу), мы рассмотрим при изучении архитектурного паттерна MVC.
Класс-контроллер это обычный java-класс, который должен реализовывать интерфейс javafx.fxml.Initializable, который определяет всего один метод initialize(). Создадим класс SampleController и пока оставим его пустым.
Дальнейший шаг – в fxml-файле необходимо указать, что для данного графа сцены будет использоваться этот класс контроллера. Эта информация будет считана FXMLLoader при парсинге fxml-файла.
Откроем файл sample.fxml и в корневом узле графа сцены укажем атрибут fx:controller = ”ua.opu.SampleController”.
Таким образом, мы заявляем, что для данного fxml-файла будет использоваться указанный класс контроллера. Этот атрибут можно использовать только в корневом узле fxml-файла.
Чтобы отличить один объект элемента UI от других, необходимо присвоить элементам UI различные идентификаторы, используя которые мы сможем обратиться к тому или иному элементу. Для того чтобы установить идентификатор для элемента, необходимо указать атрибут fx:id в коде либо в свойствах в визуальном редакторе.
Идем дальше. Теперь я хочу каким-то образом в классе контроллера получить ссылку на первые две кнопки и как-то ними дальше манипулировать. Как мне это сделать? Чтобы получить доступ к каким-то элементам, мне нужно этим элементам присвоить определенный id. Вы можете сделать это либо в визуальном редакторе (выделите нужный элемент, зайдите во вкладку Code: и установите значение в поле fx:id).
Для данного примера присвоим идентификаторы для двух кнопок: button1 и button2.
Нам необходимо получить ссылки на объекты этих кнопок. Для этого зайдем в класс контроллера и создадим два поля типа javafx.scene.control.Button, с названиями, которые точно совпадают с идентификаторами этих кнопок вfxml-файле. После чего, укажем перед каждым полем аннотацию @FXML. В итоге, класс контроллера будет иметь следующий вид
Еще раз обратите внимание, что fx:id и имя переменной должно совпадать точно и перед каждым объявлением должно быть указано @FXML.
Давайте разберемся, что происходит при указании контроллера и полей с аннотацией @FXML:
класс FXMLLoader парсит fxml-файл;
загрузчик «считывает» атрибут fx:controller и создает объект этого класса;
загрузчик парсит аннотации @FXML и внедряет в эти поля ссылки на созданные объекты (используется механизм рефлексии);
загрузчик вызывает метод initialize() (в этом методе мы прописываем все наши манипуляции с нужными элементами GUI).
Таким образом, если мы правильно указали fx:id и не ошиблись с классами и названиями полей, то в момент вызова метода initialize(), наши поля button1 и button2 будут содержать ссылки на наши две кнопки.
Добавим обработчик нажатия на кнопку 1
Всё прошло успешно, мы имеем ссылки на нужные нам элементы, и мы можем делать с ними что захотим.
Для второй кнопки реализуем слушатель иначе. В классе-контроллере создадим метод handleButton2() и тоже пометим его аннотацией @FXML.
То есть, мы видим, что мы можем помечать этой аннотацией не только поля, но и методы. Далее, зайдем в fxml-файл, в режим визуального редактора, выделите вторую кнопку, зайдите во вкладку Code: и в поле OnAction из выпадающего списка выберите нужный метод
В текстовом виде это выглядит следующим образом
Компилируем приложение и смотрим результат
Как вы уже, наверное, поняли, фактически это означает следующее «если для кнопки2 произошло событие Action – вызови метод handleButton2(). Таким образом, если у вас много слушателей, то удобнее просто закодировать методы и расставить нужные методы для нужных событий в нужных элементах, и не париться лишний раз с полями.
Паттерн MVC
Паттерн MVC можно описать следующим образом
Шаблон проектирования MVC предполагает разделение данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: Модель, Представление и Контроллер – таким образом, что модификация каждого компонента может осуществляться независимо.
В данном определении под компонентом следует понимать часть кода (как правило, это отдельный класс), каждая из которых играет одну из ролей Контроллера, Модели или Представления, где Модель служит для извлечения и манипуляций данными приложения, Представление отвечает за видимое пользователю отображение этих данных, а Контроллер управляет всем этим оркестром
Архитектурная концепция (архитектурный паттерн) MVC позволяет разделить программу на три отдельных компонента, которые могут быть реализованы следующим образом:
1. Пользователь взаимодействует с представлением. Представление – «окно», через которое пользователь воспринимает модель. Когда вы делаете что-то с представлением (скажем, щелкаете на кнопке воспроизведения), представление сообщает контроллеру, какая операция была выполнена. Контроллер должен обработать это действие.
2. Контроллер обращается к модели с запросами об изменении состояния. Контроллер получает действия пользователя и интерпретирует их. Если вы щелкаете на кнопке, контроллер должен разобраться, что это значит и какие операции с моделью должны быть выполнены при данном действии.
3. Контроллер также может обратиться к представлению с запросами об изменении. Когда контроллер получает действие от представления, в результате его обработки он может обратиться к представлению с запросом на изменение (скажем, заблокировать некоторые кнопки или команды меню).
4. Модель оповещает представление об изменении состояния. Когда в модели что-то изменяется (вследствие действий пользователя или других внутренних изменений – скажем, перехода к следующей песне в списке), модель оповещает представление об изменении состояния.
5. Представление запрашивает у модели информацию состояния. Представление получает отображаемую информацию состояния непосредственно от модели. Например, когда модель оповещает представление о начале воспроизведения новой песни, представление запрашивает название песни и отображает его. Представление также может запросить у модели информацию состояния в результате запроса на изменение состояния со стороны контроллера.
Модель (Model) – содержит бизнес-логику приложения и включает методы выборки, обработки и предоставления конкретных данных, что зачастую делает ее очень «толстой», что вполне нормально. Модель не должна напрямую взаимодействовать с пользователем. Модель:
предоставляет (представлению и контроллеру):
данные;
методы работы с данными:
запросы к базам данных;
валидация данных;
бизнес-логика (если модель «активна»).
нуждается в следующем:
в представлении (не может самостоятельно демонстрировать данных и результаты их обработки);
в контроллере (не имеет точек взаимодействия с пользователем).
может иметь множество различных представлений и контроллеров;
отвечает на запросы изменением состояния. При этом, в модель может быть встроено автоматическое оповещение «наблюдателей».
Представление (View) – используется для задания внешнего вида отображения данных, полученных из контроллера и модели. Представление не должно обращаться к базе данных, этим должны заниматься модели. Также, представление не должно работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер. Представление:
отвечает за получение необходимых данных от модели и отправку их пользователю;
не обрабатывает введенные данные пользователя;
может влиять на состояние модели через отправку ей сообщений (вызовы методов).
Контроллер (Controller) – связующее звено, соединяющее модели, представления и другие компоненты в рабочее приложение. Контроллер отвечает за обработку действий пользователя. В хорошо спроектированном MVC-приложении контроллеры обычно очень «тонкие» и содержат только несколько десятков строк кода. Логика контроллера довольно типична и большая ее часть выносится в базовые классы. Модели, наоборот, очень «толстые» и содержат большую часть кода, связанную с обработкой данных, так как структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения. Контроллер:
обеспечивает «связь» между пользователем и системой, контролирует и направляет:
данные от пользователя к системе;
реакцию системы – пользователю.
использует модель и представление для необходимого действия;
в случае «пассивной» модели – реализует бизнес-логику.
Рассмотрим пример реализации паттерна MVC на примере просто приложения. Приложение выводит на экран список студентов, добавляет нового студента и удаляет выделенного студента из списка.
Для начала, создадим новый JavaFX проект. Шаблон JavaFX проекта содержит три файла. Давайте определимся, к какой категории относится тот или иной файл.
Файл sample.fxml, очевидно, относится к представлению (View). Он задает внешний вид приложения.
Класс Main (файл Main.java) относится к контроллеру. Этот класс можно назвать контроллером приложения. Он содержит метод Application.launch(), который запускает JavaFX приложение, настраивается окно, создается объект сцены, устанавливается нужный fxml файл для генерации графа сцены для данного окна.
Класс Controller (файл Controller.java) очевидно является контроллером. Это контроллер окна, который содержит код для обработки событий, связанный с данным окном.
Создадим соответствующие пакеты и распределим файлы по своим пакетам. Не забудьте изменить путь к файлу fxml.
Теперь создадим класс Student, который будет содержать данные и логику для объекта студента. Очевидно, что этот класс относится к классу модели.
Спроектируем UI для нашего приложения. Для этого отредактируем файл sample.fxml. За вывод списка студентов будет отвечать элемент ListView. Кроме него предусмотрены две кнопки для добавления и удаления студента.
Добавим обработчики нажатий на кнопки в контроллер окна Controller.java.
Теперь вернемся к классам модели. Очевидно, что в данном приложении объекты класса Student должны быть организованы в некоторую коллекцию. Кроме того, мы должны предоставить метод добавления и удаления студентов из коллекции.
Так как «сборка» отдельных частей приложения осуществляется в контроллере – организаторе и посреднике между различными компонентами приложения, самым простым вариантом было бы просто создать пустую коллекцию студентов, в которую можно было бы напрямую добавлять или удалять студентов.
Так как мы оставляем возможности для будущего расширения программы, в которой могут появиться новые окна (например, отдельное окно для добавления студента), коллекция со студентами должна быть доступна во всем приложении.
Таким образом, первый кандидат на хранении коллекции со студентами – контроллер приложения, класс Main. Код для такого варианта будет примерно следующим:
Методы addStudent() и deleteStudent() будут вызываться из контроллера окна вместе с объектом для добавления или удаления.
Такой подход оправдан только, если мы точно знаем, что программа дальше не будет расширяться и наша модель будет, фактически, состоять из одного класса Student. Но мы хотим оставить задел на будущее, когда у нас может появиться класс студенческой группы, класс с оценками, класс учебной дисциплины, данные могут записываться и считываться из файлов, передаваться по сети и так далее.
В случае расширения программы в контроллер приложения будут добавляться все новые и новые ссылки на различные объекты и коллекции, а также появляться новые методы для управления этими объектами и коллекциями.
Поэтому, лучшим вариантом будет применение паттерна проектирования «Фасад», который позволит перенести весь будущий функционал в слой модели.
Принцип работы паттерна «Фасад» можно объяснить следующим образом: фасад – это объект некоторого класса, который предоставляет простой (но урезанный) интерфейс работы со сложной системой объектов. Таким образом, если наша модель будет усложняться, и будут добавляться новые классы, то будет усложняться внутреннее строение фасада, а внешне он будет предоставлять те же самые простые методы для работы со сложной моделью.
Создадим класс StudentsFacade. Класс будет содержать коллекцию со студентами, а также предоставлять публичные методы добавления и удаления студента.
Теперь создадим объект фасада в контроллере приложения и создадим методы, которые будут вызывать методы фасада.
Следующий шаг – необходимо написать обработчик кнопки «Добавить». При нажатии на кнопку необходимо создать объект класса Student и вызвать метод Main.addStudent(), которому необходимо передать созданный объект.
Так как обработчик нажатия на кнопку находится в контроллере окна, мы должны из контроллера окна (Controller.java) вызвать метод контроллера окна (Main.java). Чтобы это сделать, мы должны передать контроллеру окна ссылку на контроллер приложения.
Давайте разберемся, как это сделать. Для начала, откроем контроллер приложения и немного изменим загрузку fxml файла.
Если ранее мы использовали статический метод FXMLLoader.load(), то теперь мы создадим объект класса FXMLLoader, укажем в конструкторе ресурс fxml файла и вызовем метод load() у созданного объекта.
Эти манипуляции нужны нам для того, чтобы вызвать у объекта метод getController(), который вернет нам ссылку на созданный объект класса Controller. Таким образом, мы получим ссылку на объект контроллера окна.
Далее, в контроллере окна необходимо добавить поле типа Main и публичный метод, который будет записывать в поле переданную ссылку на объект контроллера приложения.
Таким образом, контроллер окна будет иметь ссылку на объект контроллера приложения. Теперь мы можем приступать к написанию кода для обработчика кнопки «Добавить». Для упрощения кода, будем генерировать значения полей для объекта студента.
Итак, на данный момент мы реализовали этапы 1 – 3 схемы работы паттерна MVC: пользователь взаимодействует с представлением (нажимает на кнопку «Добавить»), контроллер обращается к модели с запросами об изменении состояния. Но нам необходимо оповестить представление о том, что модель изменилась, чтобы представление обновилось (на экране появилась новая запись о студенте).
Для начала, реализуем взаимодействие модели и представления без использования встроенных средств JavaFX. Для этого нам необходимо разобраться с паттерном «Наблюдатель» (Observer).
Паттерн «Наблюдатель» (Observer)
Паттерн Observer определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически.
Паттерн определяет объект Subject, хранящий данные (модель) и объекты Observer, которые реализуют функциональность представлений. При создании наблюдатели Observer регистрируются у объекта Subject. Когда объект Subject изменяется, он извещает об этом всех зарегистрированных наблюдателей. После этого каждый обозреватель запрашивает у объекта Subject ту часть состояния, которая необходима для отображения данных.
Итак, реализуем этот паттерн в нашем исходном коде. В качестве Subject у нас выступает объект класса StudentsFacade, который хранит нужные данные. Укажем в исходном коде, что класс StudentsFacade наследуется от класса java.util.Observable. Класс Observable содержит готовый функционал для регистрации наблюдателей и для их оповещения об изменении модели. В классе StudentsFacade есть два метода, которые меняют состояние объекта – метод addStudent() добавляет новый объект студента в коллекцию, метод deleteStudent() извлекает из коллекции объект студента. Таким образом, при вызове этих методов, мы должны оповестить всех наблюдателей о том, что произошло изменение данных и оповестить их об этом и передать нужные данные для отображения.
Далее необходимо указать, что используемый для вывода студентов элемент ListView реализует интерфейс java.util.Observer, а также необходимо запрограммировать реакцию на извещение об изменении модели.
Когда в классе StudentsFacade будет выполняться метод addStudent() или deleteStudent() и выполнится команда notifyObservers(), у объекта класса ListViewObservers будет вызван метод update(), в котором будет передано два параметра – ссылка на объект, который вызвал метод и переданные данные, если они есть (иногда необходимо передать в Observer сам факт изменений, тогда вызывается метод notifyObservers() без параметров).
Мы проверяем – от кого произошло событие, и обновляем список с помощью пришедших данных (очищаем список от существующих элементов и добавляем новые).
Теперь необходимо заменить класс ListView на ListViewObserver. Регистрация Observer будет происходить в классе контроллера приложения
Нам осталось только реализовать функционал для кнопки «Удалить» и наше небольшое приложение теперь полностью готово.
Тестируем работу приложения.
Мы реализовали паттерн «Наблюдатель» вручную, но Java FX уже имеет готовый функционал для реализации этого паттерна. Для этого в JavaFX предусмотрен целый набор классов Observable*, которые предоставляют различные значения и коллекции уже со встроенной поддержкой паттерна Observer. Давайте реализуем 4-5 этап паттерна MVC с помощью встроенных классов Observable.
Для начала перейдем в класс StudentsFacade и изменим класс коллекции с List на ObservableList. Классу StudentsFacade теперь нет нужды наследоваться от класса java.util.Observable, так как этот функционал теперь реализует непосредственно коллекция со студентами. Кроме того, добавим геттер для коллекции (хотя это не очень согласуется с принципом инкапсуляции).
В классе Main заменим метод bindObserverToFacade() на setItemsForListView(). Обратите внимание, что элемент ListView содержит метод setItems(), который принимает на вход объект класса ObservableList. При вызове метода регистрация и обновление спискового элемента происходит без нашего участия – мы просто должны вызвать метод setItems() и передать ему ObservableList.
Заменим использование ListViewObserver на использование стандартного элемента ListView и изменим содержимое класса Controller.
Запустим приложение и убедимся, что его работа не изменилась, но мы избавились от необходимости создавать свой подкласс спискового элемента и вручную добавлять блок оповещения наблюдателей и вручную прописывать реакцию на оповещение об изменении данных.
Синтаксис создания классов
Базовые сведения об объектах и классах
Объектно-ориентированное программирование – это методология программирования, основанная на представлении программы в виде совокупности взаимодействующих объектов, каждый из которых является экземпляром определенного класса, а классы являются членами определенной иерархии наследования.
Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Фактически, объект является основным строительным блоком объектно-ориентированных программ.
Об объектах можно думать как о полезных существах, которые "живут" в нашей программе и коллективно решают некоторую задачу. Наша обязанность заключается в том, чтобы создать эти существа, распределить между ними обязанности, регламентировать сценарий их поведения и взаимодействия при решении поставленной задачи.
Класс – шаблон для объектов. Каждый объект является экземпляром (instance) какого-либо класса («безклассовых» объектов не существует). В рамках класса задается общий шаблон, структура, на основании которой создаются объекты. Данные, относящиеся к классу, называются полями класса, а программный код для их обработки называется методами класса. Поля и методы называют общим термином – члены класса.
Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом.
Объект состоит из следующих частей:
имя объекта;
состояние (переменные состояния). Данные, содержащиеся в объекте, представляют его состояние. В терминологии ООП эти данные называются атрибутами. Например, атрибутами работника могут быть: имя, фамилия, пол, дата рождения, номер телефона. В разных объектах атрибуты имеют разное значение. Фактически, в объектах определяются конкретные значения тех переменных (полей класса), которые были заявлены при описании класса;
методы (операции) – применяются для выполнения операций с данными, а также для совершения других действий. Методы определяют, как объект взаимодействует с окружающим миром.
В теории ООП, объекты могут отправлять друг другу сообщения. В языках программирования этот механизм реализуется через вызов функции другого объекта. Понятие "сообщение" в ООП можно объяснить следующим образом: мы не можем напрямую изменить состояние объекта и должны как бы послать сообщение объекту, что мы хотим как-то изменить его состояние. Очень важно понять, что объект сам меняет свое состояние, а мы можем только попросить его об этом с помощью отсылки сообщения.
В объектно-ориентированной программе весь код должен находиться внутри классов!
В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.
Классы как абстрактные типы данных
В окончательном виде любая программа представляет собой набор инструкций процессора. Все, что написано на любом языке программирования - более удобная, упрощенная запись этого набора инструкций, облегчающая написание, отладку и последующую модификацию программы. Чем выше уровень языка, тем в более простой форме записываются одни и те же действия.
С ростом объема программы становится невозможным удерживать в памяти все детали, и становится необходимым структурировать информацию, выделять главное и отбрасывать несущественное. Этот процесс называется повышением степени абстракции программы.
Для языка высокого уровня первым шагом к повышению абстракции является использование функций, позволяющее после написания и отладки функции отвлечься от деталей ее реализации, поскольку для вызова функции требуется знать только ее интерфейс. Если глобальные переменные не используются, интерфейс полностью определяется заголовком функции.
Следующий шаг - описание собственных типов данных, позволяющих структурировать и группировать информацию, представляя ее в более естественном виде. Например, все разнородные сведения, относящиеся к одному виду товара на складе, можно представить с помощью одной структуры.
Для работы с собственными типами данных требуются специальные функции. Естественно сгруппировать их с описанием этих типов данных в одном месте программы, а также по возможности отделить от ее остальных частей. При этом для использования этих типов и функций не требуется полного знания того, как именно они написаны - необходимы только описания интерфейсов. Объединение в модули описаний типов данных и функций, предназначенных для работы с ними, со скрытием от пользователя модуля несущественных деталей является дальнейшим развитием структуризации программы.
Все три описанных выше метода повышения абстракции преследуют цель упростить структуру программы, то есть представить ее в виде меньшего количества более крупных блоков и минимизировать связи между ними. Это позволяет управлять большим объемом информации и, следовательно, успешно отлаживать более сложные программы.
Введение понятия класса является естественным развитием идей модульности. В классе структуры данных и функции их обработки объединяются. Класс используется только через его интерфейс - детали реализации для пользователя класса не существенны.
Идея классов отражает строение объектов реального мира - ведь каждый предмет или процесс обладает набором характеристик или отличительных черт, иными словами, свойствами и поведением. Программы в основном предназначены для моделирования предметов, процессов и явлений реального мира, поэтому удобно иметь в языке программирования адекватный инструмент для представления моделей.
Класс является типом данных, определяемым пользователем. В классе задаются свойства и поведение какого-либо предмета или процесса в виде полей данных (аналогично структуре) и функций для работы с ними. Создаваемый тип данных обладает практически теми же свойствами, что и стандартные (примитивные) типы (тип задает внутреннее представление данных в памяти компьютера, множество значений, которое могут принимать величины этого типа, а также операции и функции, применяемые к этим величинам).
Пользовательские типы данных принято называть абстрактными типами данных
Существенным свойством класса является то, что детали его реализации скрыты от пользователей класса за интерфейсом. Интерфейсом класса являются заголовки его открытых методов. Таким образом, класс как модель объекта реального мира является черным ящиком, замкнутым по отношению к внешнему миру.
Конкретные переменные типа данных "класс" называются экземплярами класса, или объектами. Объекты взаимодействуют между собой, посылая и получая сообщения. Сообщение - это запрос на выполнение действия, содержащий набор необходимых параметров. Механизм сообщений реализуется с помощью вызова соответствующих функций.
Пример использования объектного подхода
Создадим класс Box, который описывает контейнер, допустим, на каком-то складе.
Класс Box определяет три переменные экземпляра: width (ширина), height (высота) и depth (глубина). В настоящий момент класс Box не содержит никаких методов.
Как мы уже говорили, класс определяет новый тип данных. В данном случае новый тип данных называется Box. Это имя будет использоваться для объявления объектов типа Box. Не следует забывать, что объявление class создает только шаблон, но не конкретный объект. Таким образом, приведенный выше код не приводит к появлению каких-нибудь объектов типа Box.
Чтобы действительно создать объект класса Box, нужно воспользоваться оператором new
После выполнения этого оператора объект myBox станет экземпляром класса Box. Таким образом, он обретет "физическое" существование.
Также следует напомнить, что каждый объект содержит собственную копию переменной экземпляра, которая определена в классе. Каждый объект типа Boxбудет содержать собственные копии переменных width, height и depth(рис. 4.2).
Изменения в переменных экземпляра одного объекта не влияют на переменные экземпляра другого объекта. Таким образом, каждый объект класса Box будет содержать собственные копии переменных width, height и depth. Для доступа к этим переменным служит оператор-точка (.). Эта операция связывает имя объекта с именем переменной экземпляра. Например, чтобы присвоить переменной width экземпляра myBox значение 100, нужно выполнить следующий оператор:
Этот оператор предписывает компилятору, что копии переменной width, хранящейся в объектe myBox, требуется присвоить значение 100. В общем, операция-точка служит для доступа как к переменным экземпляра, так и к методам в пределах объекта.
Ниже приведет пример программы, в которой используется класс Box
Как пояснялось ранее, каждый объект содержит собственные копии переменных экземпляра. Это означает, что при наличии двух объектов класса Box каждый из них будет содержать собственные копии переменных width, height и depth. Следует, однако, иметь ввиду, что изменения в переменных экземпляра одного объекта не влияют на переменные экземпляра другого. Например, в следующей программе объявлены два объекта класса Box:
Программа выводит следующий результат:
Как видите, данные из объекта myBox1 полностью изолированы от данных, содержащихся в объекте myBox2.
Методы класса
Как упоминалось ранее, классы состоят из двух компонентов: переменных экземпляра и методов. Общая форма метода выглядит следующим образом:
где возвращаемый тип означает конкретный тип данных, возвращаемый методом. Он может быть любым допустимым типом данных, в том числе и типом созданного класса. Если метод не возвращает значение, то его возвращаемым типом должен быть void. В качестве имени методов может быть любой допустимый идентификатор, кроме тех, которые уже используются другими элементами кода в текущей области действия. А список параметров обозначает последовательность пар "тип-идентификатор", разделенных запятыми. По существу, параметры - это переменные, которые принимают значения аргументов, передаваемых методу во время его вызова. Если у метода отсутствуют параметры, то список параметров оказывается пустым. Методы, возвращаемый тип которых отличается от void, возвращают значение вызывающей части программы с помощью оператора return.
Вернемся к нашему примеру с классом Box. Было бы логично, если бы расчет объема коробки выполнялся в классе Box, поскольку объем коробки зависит от ее размеров. Для этого добавим в класс Box метод getVolume()
Внимательно рассмотрим две следующие строки кода
В первой строке вызывается метод volume() для объекта myBox1. Следовательно, метод volume() вызывается по отношению к объекту myBox1, для чего было указано имя объекта, а вслед за ним - операция-точка. Таким образом, в результате вызова метода myBox1.volume() выводится объем коробки, определяемого объектом myBox1, а в результате вызова метода myBox2.volume() - объем коробки, определяемого объектом myBox2.
При вызове метода myBox1.volume() исполняющая система Jаvа передает управление коду, определенному в теле метода volume(). По окончании выполнения всех операторов в теле метода управление возвращается вызывающей части программы и далее ее выполнение продолжается со строки кода, следующей за вызовом метода. В самом общем смысле метод - это способ реализации подпрограмм в Java.
В методе volume() следует обратить внимание на еще одну очень важную особенность: ссылка на переменные экземпляра width, height и depth делается непосредственно без указания перед ними имени объекта или операции-точки. Когда в методе используется переменная экземпляра, определенная в его же классе, это делается непосредственно, без указания явной ссылки на объект и применения операции-точки . Это становится понятным, если немного подумать. Метод всегда вызывается по отношению к какому-то объекту его класса. Как только этот вызов сделан, объект известен. Таким образом, в теле метода вторичное указание объекта совершенно излишне. Это означает, что переменные экземпляра width, height и depth неявно ссылаются на копии этих переменных, хранящиеся в объекте, который вызывает метод volume().
Подведем краткие итоги. Когда доступ к переменной экземпляра выполняется из кода, не входящего в класс, где определена переменная экземпляра, следует непременно указать объект с помощью операции-точки. Но когда такой доступ осуществляется из кода, входящего в класс, где определена переменная экземпляра, ссылка на переменную может делаться непосредственно. Эти же правила относятся и к методам.
Следует обратить внимание, что метод getVolume() возвращает значение 3000, и это значение рассчитанного объема сохраняется в переменной vol. При обращении с возвращаемыми значениями следует принимать во внимание два важных обстоятельства:
тип данных, возвращаемых методом, должен быть совместим с возвращаемым типом, указанным в методе. Так, если какой-нибудь метод должен возвращать логический тип boolean, то возвратить из него целочисленное значение нельзя;
переменная, принимающая возвращаемое методом значение (например, vol), также должна быть совместима с возвращаемым типом, указанным для метода.
Плейлист доступен
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Ссылка на видео -
Сайт курса переехал на
Статья Дейкстры в переводе -
Неплохая статья по поводу отличия композиции и наследования -
Подробнее: , , ,
Справочник по исключениям типа Error смотрите здесь –
скачаем и установим приложение SceneBuilder по ссылке ;
вызовем статический метод FXMLLoader.load(), на вход которого передадим URL ресурса, в качестве которого выступает наш fxml-файл (что такое ресурс и как нам получить URL ресурса подробнее читайте здесь );
String str = "строка";
Point point = str;
Point point2 = (Point) str;
incompatible types: java.lang.String cannot be converted to java.awt.Point
incompatible types: java.lang.String cannot be converted to java.awt.Point
Object o1 = new String("строка");
Object o2 = new Point(100, 200);
class Person {
public void foo() {}
}
class Student extends Person {
public void bar() {}
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
Student s = (Student) person;
}
}
Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to Student
class Person {
public void foo() {}
}
class Student extends Person {
public void bar() {}
}
public class Main {
public static void main(String[] args) {
Person person = new Student(); // << --- ВНИМАНИЕ!
Student s = (Student) person;
}
}
public class Main {
public static void main(String[] args) {
Instrument instrument = new Instrument();
Guitar guitar = new Guitar();
tune(instrument);
tune(guitar);
}
public static void tune(Instrument instrument) {
instrument.play();
}
}
class Instrument {
public void play() {
System.out.println("Играет инструмент");
}
}
class Guitar extends Instrument {
@Override
public void play() {
System.out.println("Играет гитара");
}
}
Играет инструмент
Играет гитара
public class Main {
public static void main(String[] args) {
Instrument instrument = new Instrument();
Guitar guitar = new Guitar();
tune(instrument);
tune(guitar);
}
public static void tune(Instrument instrument) {
instrument.play();
}
}
class Instrument {
public void play() {
System.out.println("Играет инструмент");
}
}
class Guitar extends Instrument {
@Override
public void play() {
System.out.println("Играет гитара");
}
}
public static void tune(Guitar guitar) {
guitar.play();
}
public class Main {
public static void main(String[] args) {
Instrument instrument = new Instrument();
Guitar guitar = new Guitar();
Violin violin = new Violin();
Saxophone saxophone = new Saxophone();
tune(instrument);
tune(guitar);
tune(violin);
tune(saxophone);
}
public static void tune(Instrument instrument) {
instrument.play();
}
public static void tune(Guitar guitar) {
guitar.play();
}
public static void tune(Saxophone saxophone) {
saxophone.play();
}
public static void tune(Violin violin) {
violin.play();
}
}
class Saxophone extends Instrument {
@Override
public void play() {
System.out.println("Играет саксофон");
}
}
class Violin extends Instrument {
@Override
public void play() {
System.out.println("Играет скрипка");
}
}
class Instrument {
public void play() {
System.out.println("Играет инструмент");
}
}
class Guitar extends Instrument {
@Override
public void play() {
System.out.println("Играет гитара");
}
}
class MyClass {
public void foo() {
// ... код
}
public void foo(String s) {
// ... код
}
}
// ДАННЫЙ КОД ВЫЗОВЕТ ОШИБКУ КОМПИЛЯЦИИ!
class MyClass {
public int foo() {
return 0;
}
public double foo() {
return 0;
}
}
class MyClass {
public MyClass() {
// какой-то код
}
public MyClass(int arg0) {
// какой-то код
}
public MyClass (int arg0, String arg1) {
// какой-то код
}
}
class MyClass {
public MyClass() {
// Вызываем конструктор MyClass(int arg0)
this(0);
}
public MyClass(int arg0) {
// Вызываем конструктор MyClass (int arg0, String arg1)
this(arg0, " ");
}
public MyClass(int arg0, String arg1) {
// какой-то код
}
}
public static void tune(Instrument i) {
System.out.println(i.play());
}
class Shape {
public String draw() {
return null;
}
public String erase() {
return null;
}
}
class Circle extends Shape {
@Override
public String draw() {
return "Рисуем круг";
}
@Override
public String erase() {
return "Стираем круг";
}
}
class Triangle extends Shape {
@Override
public String draw() {
return "Рисуем треугольник";
}
@Override
public String erase() {
return "Стираем треугольник";
}
}
class Square extends Shape {
@Override
public String draw() {
return "Рисуем квадрат";
}
@Override
public String erase() {
return "Стираем квадрат";
}
}
public static void main(String[] args) {
Shape s = new Circle();
}
public static void main(String[] args) {
Shape s = new Circle();
System.out.println(s.draw());
}
public static void main(String[] args) {
Shape s = new Circle();
System.out.println(s.draw());
Shape s2 = new Triangle();
System.out.println(s.draw());
Shape s3 = new Square();
System.out.println(s.draw());
}
Рисуем круг
Рисуем треугольник
Рисуем квадрат
Collection<String> collection;
Метод
Описание
add(Object o)
Добавляет указанный объект в коллекцию
remove(Object o)
Удаляет указанный объект из коллекции
clear()
Удаляет все элементы из коллекции
size()
Возвращает количество элементов в коллекции
iterator()
Возвращает объект, который используется для доступа к элементам коллекции
List<String> names = List.of("Петр", "Василий", "Александр");
// Выбросит исключение, так как коллекция неизменяемая (Immutable)
names.add("Сергей");
Добавляет в список все элементы коллекции col по индексу index. Если в результате добавления список был изменен, то возвращается true, иначе возвращается false
E get(int index)
Возвращает из списка объект по индексу index
int indexOf(Object obj)
Возвращает индекс первого экземпляра заданного объекта в списке. Если заданный объект в списке отсутствует, возвращается значение -1
E remove(int index)
Удаляет объект по индексу index из списка, при этом метод возвращает удаленный объект
E set(int index, E obj)
Присваивает элементу по индексу index объект obj (фактически, записывает новую ссылку на объект поверх старой), метод возвращает старый объект
List<E> sublist(int start, int end)
Возвращает список элементов, которые находятся между индексами start и end
// Создание обычного пустого списка
List<String> list = new ArrayList<>();
// Создание неизменяемого списка, Java 9+
List<String> strings = List.of("one", "two", "three", "four");
// Добавление элементов
list.add("one");
list.add("two");
list.add("three");
list.add("four");
// Удаление элемента с индексом 1
list.remove(1);
// Обход списка с помощью обычного цикла
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// Обход списка с помощью foreach
for (String s : list) {
System.out.println(s);
}
// Получение подсписка
List<String> subList = list.subList(1, 3);
// Изменение значение списка
// В строку old записывается старое значение
String old = list.set(0, "new");
// интерфейс класс, реализующий список
List<String> list = new ArrayList<>();
Queue<String> queue;
Метод
Описание
E element()
Возвращает элемент из головы очереди. Возращаемый элемент не удаляется. Если очередь пуста, генерируется исключение типа NoSuchElementException
boolean offer(E object)
Пытается ввести заданный object в очередь. Возвращает логическое значение true, если object введен, иначе - false
E peek()
Возвращает элемент из головый очереди. Если очередь пуста, возвращает пустое значение null. Возвращаемый элемент не удаляется из очереди
E poll()
Возвращает элемент из головый очереди и удаляет его. Если очередь пуста, возвращает значение null
E remove()
Удаляет элемент из головы очереди, возвращая его. Генерирует исключение типа NoSuchElementException, если очередь пуста
List<String> list = new LinkedList<>();
list.add("one");
list.add("two");
list.add("three");
list.add("four");
Queue<String> queue = (Queue) list;
queue.offer("five");
queue.poll();
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(100);
queue.offer(20);
queue.offer(544);
queue.offer(11);
queue.offer(89);
while (queue.size() > 0)
System.out.println(queue.poll());
11
20
89
100
544
Set<String> set;
Map<K, V> map;
Оценка
Стилизация исходного кода (до 20 баллов)
Оформление протокола (до 10 баллов)
Качество кода (до 10 баллов)
A
Исходный код без заметных нареканий, названия, отступы, пробелы реализованы грамотно, комментарии по делу.
18-20 баллов
Оформление без видимых замечаний, присутствуют осмысленные пояснения к коду. Код вставлен текстом с соблюдением отступов и моноширинным шрифтом. Присутствуют выводы к лабораторной работе.
Есть замечания по стилю, отступам или названиям. Комментарии избыточны или недостаточны.
15-17 баллов
Оформление с замечаниями, код вставлен изображением или отсутствуют отступы. Пояснения к коду слишком краткие или формальные. Выводы к лабораторной работе формальные.
6-7 баллов
Неоптимальная форма циклов (отступление от каноничной формы без видимой причины) и ветвлений (пустые блоки).
5-7 баллов
C
Названия, отступы, стилизация соблюдаются спорадически. Комментарии отсутствуют или присутствуют на каждой строчке.
10-14 баллов
Оформление с существенными замечаниями, код вставлен не моноширинным шрифтом, не соблюдены отступы. Пояснения к коду отсутствуют. Выводы к лабораторной работе формальные или отсутствуют.
0-5 баллов
Циклы избыточно сложны, очень сложные условия ветвлений, инициализация переменных выполнена в стиле языка C, неиспользуемые переменные.
2-4 балла
D
Правила грамотного написания кода не соблюдены. Отступы, названия, стилизация не соответствуют современным правилам оформления кода.
0-9 баллов
В протоколе некорректно указан номер группы и(-или) студент. Код нечитабелен, пояснения отсутствуют, выводы отсутствуют. Протокол отсутствует.
Лабораторная не принимается.
Не выдержана структура класса, некорректные ветвления и циклы.
0-1 балл
class Box {
private String item;
public Box(String item) {
put(item);
}
public void put(String item) {
this.item = item;
}
public String get() {
return item;
}
}
class Box {
private Object item;
public Box(Object item) {
put(item);
}
public void put(Object item) {
this.item = item;
}
public Object get() {
return item;
}
}
interface BoxItem {}
class Box {
private BoxItem item;
public Box(BoxItem item) {
put(item);
}
public void put(BoxItem item) {
this.item = item;
}
public BoxItem get() {
return item;
}
}
Box box = new Box(new Item());
Object obj = box.get();
if (obj instanceof Item) {
Item item = (Item)obj;
}
class Box<T> {
private T item;
public Box(T item) {
put(item);
}
public void put(T item) {
this.item = item;
}
public T get() {
return item;
}
}
Box<Item> box = new Box<>(new Item());
Box<Item> box = new Box<>(new Item());
box.put(new Item());
Item i = box.get();
Box<int> box = new Box<>(20); // ошибка компиляции
Integer i = 5; // автоупаковка
foo(5); // автоупаковка при передачи функции аргумента
Double result = bar(); // автоупаковка при получении результата
void foo(Integer i) {
...
}
double bar() {
return 0.0;
}
Integer i = 5;
int a1 = i; // автораспаковка
int a2 = new Integer(10); // старый вариант
int a3 = Integer.valueOf(10); // новый вариант
Integer a4 = 10;
foo(a4); // автораспаковка при передачи функции аргумента
double result = bar(); // автораспаковка при получении результата
static void foo(int i) {
...
}
static Double bar() {
return 0.0;
}
DualBox<String, Integer> box = new DualBox<>("Вася", 2);
class DualBox<T, V> {
private T firstItem;
private V secondItem;
public DualBox(T firstItem, V secondItem) {
this.firstItem = firstItem;
this.secondItem = secondItem;
}
}
class NumericValue<T extends Number> {
private T value;
public NumericValue(T value) {
this.value = value;
}
}
NumericValue<Double> num1 = new NumericValue<>(100.0);
NumericValue<Integer> num2 = new NumericValue<>(100);
NumericValue<Long> num3 = new NumericValue<>(100L);
void foo(GenericClass<? super C> obj1, GenericClass<? super B> obj2) {
// типы obj1 могут быть C, B, A, Object
// типы obj2 могут быть B, A, Object
}
class GenericClass<T> {}
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
class Student {
private String name;
private int avgMark;
public Student(String name, int avgMark) {
this.name = name;
this.avgMark = avgMark;
}
}
class PostGradStudent extends Student {
private String phDTopic;
public PostGradStudent(String name, int avgMark, String phDTopic) {
super(name, avgMark);
this.phDTopic = phDTopic;
}
}
public <T extends Comparable<T>, V extends T> boolean compareArrays(T[] arg0, V[] arg1) {
if (arg0.length != arg1.length)
return false;
for (int i = 0; i < arg0.length; i++) {
if (!arg0[i].equals(arg1[i]))
return false;
}
return true;
}
Student[] students1 = new Student[10];
PostGradStudent[] students2 = new PostGradStudent[10];
compareArrays(students1, students2);
java: method compareArrays in class com.company.Main cannot be applied to given types;
required: T[],V[]
found: com.company.Student[],com.company.PostGradStudent[]
reason: inference variable T has incompatible bounds
lower bounds: java.lang.Comparable<T>
lower bounds: com.company.Student
class Student implements Comparable<Student> {
private String name;
private int avgMark;
public Student(String name, int avgMark) {
this.name = name;
this.avgMark = avgMark;
}
@Override
public int compareTo(Student o) {
return this.avgMark - o.avgMark;
}
}
java: method compareArrays in class com.company.Main cannot be applied to given types;
required: T[],V[]
found: com.company.Student[],com.company.PostGradStudent[]
reason: inference variable V has incompatible bounds
lower bounds: java.lang.Comparable<T>,T
lower bounds: com.company.PostGradStudent
class Accumulator {
public <T extends Number> Accumulator(T number1, T number2) {
// ...
}
}
Accumulator accumulator = new Accumulator(5, 10);
Accumulator accumulator2 = new Accumulator(5.5d, 10L);
Accumulator accumulator3 = new Accumulator(5.5d, "four"); // вызовет ошибку
interface MyInterface<T> {
void foo(T value);
}
interface MyInterface<T> {
void foo(T value);
}
// Ошибка, класс должен быть обобщенным
class MyClass1 implements MyInterface<T> {
@Override
public void foo(T value) {}
}
// Правильное объявление. Класс реализующий
// обобщенный интерфейс должен быть обобщенным
class MyClass2<T> implements MyInterface<T> {
@Override
public void foo(T value) {}
}
// Мы явно указали параметр типа интерфейса
// и класс может быть не обобщенным
class MyClass3 implements MyInterface<Double> {
@Override
public void foo(Double value) {}
}
interface Iface<T extends Number> {}
// Выдаст ошибку
class MyClass4<T> implements Iface<T>{}
// Всё правильно
class MyClass5<T extends Number> implements Iface<T> {}
// Выдаст ошибку
class MyClass6<T extends Number> implements Iface<T extends Number>{}
StaticTest.java
class StaticTest {
static int val = 924;
}
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
StaticTest.val++;
class NetworkConnector {
public static final int baud;
private static final int bits_per_interval = 4;
static {
baud = 9600 / bits_per_interval;
}
// ...
}
class Incrementable {
static void increment() {
StaticTest.val++;
}
}
Incrementable sf = new Incrementable();
sf.increment();
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;
public class Main {
public static void main(String[] args) {
double side1 = 3.0;
double side2 = 4.0;
double hypot;
hypot = sqrt(pow(side1, 2) + pow(side2, 2));
}
}
import static java.lang.Math.*;
public class Main {
public static void main(String[] args) {
Parcel parcel = new Parcel();
parcel.ship("Лимпопо");
}
}
class Parcel {
class Contents {
private int i = 11;
public int value() {return i;}
}
class Destination {
private String label;
public Destination(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
public void ship(String destination) {
Contents c = new Contents();
Destination d = new Destination(destination);
System.out.println(d.getLabel());
}
}
public class Outer {
private List<Integer> numbers = new ArrayList<>();
public void printAnalyticalData() {
Inner inner = new Inner();
System.out.println("Min: " + inner.min());
System.out.println("Max: " + inner.max());
System.out.println("Sum: " + inner.sum());
}
private class Inner {
private int min() {
return numbers.stream().min(Integer::compareTo).orElse(0);
}
private int max() {
return numbers.stream().max(Integer::compareTo).orElse(0);
}
private int sum() {
return numbers.stream().reduce(Integer::sum).orElse(0);
}
}
}
public class Main {
public static void main(String[] args) {
class Local {
private int value;
private Local(int value) {
this.value = value;
}
}
Local local = new Local(10);
}
}
public static void main(String[] args) {
System.out.println(5 / 0);
}
"C:\Program Files\Java\jdk1.8.0_60\bin\java"...
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ua.opu.Main.main(Main.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Process finished with exit code 1
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ua.opu.Main.main(Main.java:6)
at ua.opu.Main.main(Main.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
public class Main {
public static void main(String[] args) {
methodA();
}
public static void methodA() {
methodB();
}
private static void methodB() {
methodA();
}
}
Exception in thread "main" java.lang.StackOverflowError
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
...
List size: 236557355
List size: 236557356
List size: 236557357
List size: 236557358
List size: 236557359
List size: 236557360
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3511)
at java.base/java.util.Arrays.copyOf(Arrays.java:3480)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:244)
at java.base/java.util.ArrayList.add(ArrayList.java:454)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
at com.company.Main.main(Main.java:13)
Process finished with exit code 1
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Введите первое число: ");
String firstNumber = scanner.nextLine();
System.out.println("Введите второе число: ");
String secondNumber = scanner.nextLine();
int a = 0;
int b = 0;
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
System.out.println("Результат: " + (a + b));
}
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
int a = 0;
int b = 0;
try {
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println("Одно или оба значения некорректны!");
}
System.out.println("Результат: " + (a + b));
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println("Одно или оба значения некорректны!");
}
public static void main(String[] args) {
int a = getNumberFromConsole("Введите первое число");
int b = getNumberFromConsole("Введите второе число");
System.out.println("Результат: " + (a + b));
}
public static int getNumberFromConsole(String message) {
Scanner scanner = new Scanner(System.in);
System.out.print(message + ": ");
String s = scanner.nextLine();
return Integer.parseInt(s);
}
public static int getNumberFromConsole(String message) throws NumberFormatException {
Scanner scanner = new Scanner(System.in);
System.out.print(message + ": ");
String s = scanner.nextLine();
return Integer.parseInt(s);
}
public static void foo() throws NumberFormatException, ArithmeticException, IOException {
}
public static void main(String[] args) {
int a = 0;
int b = 0;
try {
a = getNumberFromConsole("Введите первое число");
b = getNumberFromConsole("Введите второе число");
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println("Одно или оба значения некорректны!");
}
System.out.println("Результат: " + (a + b));
}
public static void main(String[] args) {
Path p = Paths.get("c:\\temp\\file.txt");
BufferedReader reader = Files.newBufferedReader(p);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
public static void main(String[] args) {
try {
Path p = Paths.get("c:\\temp\\file.txt");
BufferedReader reader = Files.newBufferedReader(p);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// запись ошибки в лог
System.out.println("Ошибка при чтении файла!");
}
}
public static void main(String[] args) {
Path p = Paths.get("c:\\temp\\file.txt");
try {
printFile(p);
} catch (IOException e) {
// запись ошибки в лог
System.out.println("Ошибка при чтении файла!");
}
}
public static void printFile(Path p) throws IOException {
BufferedReader reader = Files.newBufferedReader(p);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
public static void foo() {
try {
// some action
} catch (ArithmeticException e) {
// обработка арифметического исключения
} catch (IndexOutOfBoundsException e) {
// обработка выхода за пределы коллекции
} catch (IllegalArgumentException e) {
// обработка некорректного аргумента
}
}
public static void main(String[] args) {
Path p = Paths.get("c:\\temp\\file.txt");
try {
printFile(p);
} catch (IOException e) {
// запись ошибки в лог
System.out.println("Ошибка при чтении файла!");
} catch (FileNotFoundException e) {
// данный блок никогда не будет вызван
}
}
public static void printFile(Path p) throws IOException {
BufferedReader reader = Files.newBufferedReader(p);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
public static void foo() {
try {
// some action
} catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException e) {
// три типа исключений обрабатываются одинаково
}
}
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root =
FXMLLoader.load(getClass().getResource("sample.fxml"));
Scene scene = new Scene(root, 300, 200);
primaryStage.setTitle("JavaFX-приложение");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
SampleController.java
public class SampleController implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
}
}
SampleController.java
public class SampleController implements Initializable {
@FXML
private Button button1;
@FXML
private Button button2;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
}
SampleController.java
public class SampleController implements Initializable {
@FXML
private Button button1;
@FXML
private Button button2;
@Override
public void initialize(URL location, ResourceBundle resources) {
button1.setOnAction(event -> {
new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
});
}
}
SampleController.java
public class SampleController implements Initializable {
@FXML
private Button button1;
@FXML
private Button button2;
@FXML
protected void handleButton2(ActionEvent event) {
new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 2").showAndWait();
}
@Override
public void initialize(URL location, ResourceBundle resources) {
button1.setOnAction(event -> {
new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
});
}
}
public class Controller implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
}
@FXML
public void add(ActionEvent event) {
// Обработчик кнопки "Добавить"
}
@FXML
public void delete(ActionEvent event) {
// Обработчик кнопки "Удалить"
}
}
Main.java
public class Main extends Application {
private List<Student> list;
@Override
public void init() throws Exception {
list = new ArrayList<>();
}
public void addStudent(Student student) {
list.add(student);
}
public void deleteStudent(Student student) {
list.remove(student);
}
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
StudentFacade.java
public class StudentFacade {
private List<Student> studentList;
public StudentFacade() {
studentList = new ArrayList<>();
}
public void addStudent(Student student) {
studentList.add(student);
}
public void deleteStudent(Student student) {
studentList.remove(student);
}
}
Main.java
public class Main extends Application {
private StudentFacade facade;
@Override
public void init() throws Exception {
facade = new StudentFacade();
}
public void addStudent(Student student) {
facade.addStudent(student);
}
public void deleteStudent(Student student) {
facade.deleteStudent(student);
}
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
@FXML
public void add(ActionEvent event) {
// Создаем объект студента
Student student = new Student(
String.valueOf(new Random().nextInt(100)),
String.valueOf(new Random().nextInt(100)),
String.valueOf(new Random().nextInt(100))
);
// Вызываем метод добавления студента
main.addStudent(student);
}
StudentFacade.java
public class StudentFacade extends Observable {
private List<Student> studentList;
public StudentFacade() {
studentList = new ArrayList<>();
}
public void addStudent(Student student) {
studentList.add(student);
setChanged();
notifyObservers(studentList);
}
public void deleteStudent(Student student) {
studentList.remove(student);
setChanged();
notifyObservers(studentList);
}
}
public class ListViewObserver extends ListView<Student> implements Observer {
@Override
public void update(Observable o, Object arg) {
if (o instanceof StudentFacade) {
// В параметре arg передан список для отображения
List<Student> list = (List<Student>) arg;
getItems().clear();
getItems().addAll(list);
}
}
}
Main.java
public void bindObserverToFacade(Observer o) {
facade.addObserver(o);
}
Controller.java
public class Controller implements Initializable {
private Main main;
@FXML
private ListViewObserver list;
public void getMainController(Main main) {
this.main = main;
main.bindObserverToFacade(list);
}
...
}
Box.java
public class Box {
double width;
double height;
double depth;
}
Main.java
public class Main {
public static void main(String[] args) {
Box myBox = new Box();
}
}
myBox.width = 100;
public class Main {
public static void main(String[] args) {
// Создаем объект типа Box
Box myBox = new Box();
// Присваиваем значения переменным экземпляра myBox
myBox.width = 10;
myBox.height = 20;
myBox.depth = 15;
// Рассчитываем объем коробки
double volume = myBox.width * myBox.height * myBox.depth;
System.out.println("Объем равен: " + volume);
}
}
public class Box {
double width;
double height;
double depth;
}
public class Main {
public static void main(String[] args) {
Box myBox1 = new Box();
Box myBox2 = new Box();
// Присваиваем значения для mybox1
myBox1.width = 10;
myBox1.height = 20;
myBox1.depth = 15;
// Присваиваем значения для mybox2
myBox2.width = 3;
myBox2.height = 6;
myBox2.depth = 9;
double volume;
// объем первой коробки
volume = myBox1.width * myBox1.height * myBox1.depth;
// будет выведено 3000
System.out.println("Объем равен: " + volume);
// объем второй коробки
volume = myBox2.width * myBox2.height * myBox2.depth;
// будет выведено 162
System.out.println("Объем равен: " + volume);
}
}
Объем равен: 3000.0
Объем равен: 162.0
[возвращаемый тип] имя ([список параметров]) {
[тело метода]
}
Тема: Инкапсуляция. Знакомство с языком моделирования UML
Цель лабораторной работы:
изучить принцип инкапсуляции и его реализацию в языке Java;
ознакомиться с базовой нотацией диаграммы классов UML;
научиться представлять классы в виде нотации диаграммы классов UML;
научиться "читать" диаграмму и писать код исходя из диаграммы классов UML.
Задание на лабораторную работу
1. Класс TimeSpan
1.1 Создание класса
Создайте класс с названием TimeSpan. Объект класса TimeSpanхранит интервал времени в часах и минутах (к примеру, временной интервал между 8:00 и 10:30 это 2 часа 30 минут). Класс TimeSpanдолжен иметь следующие методы
Название метода
Описание метода
TimeSpan(hours, minutes)
Конструктор. Входные аргументы – количество часов и минут
getHours()
Возвращает целое количество часов во временном интервале, без учета количества минут (например, если интервал 2 часа 15 минут, то метод вернет 2, так как 15 минут мы не учитываем)
getMinutes()
Возвращает количество минут во временном интервале, без учета количества часов (например, если интервал 2 часа 15 минут, то метод вернет 15, так как 2 часа мы отбрасываем)
add(hours, minutes)
Добавляет указанное количество часов и минут к промежутку. Новое значение промежутка должно пересчитываться корректно. Например, 2 часа 15 минут + 1 час 45 минут = 4 часа 0 минут. Помните, что необходимо проверить корректность входных аргументов (количество часов - не отрицательное число, количество минут от 0 до 59).
addTimeSpan(timespan)
Добавляет входной промежуток времени к указанному промежутку (используйте методы getHours() и getMinutes() для получения значений часов и минут).
getTotalHours()
Возвращает количество часов в промежутке в виде дробного числа. Например, если временной интервал 9 часов 45 минут, то метод должен вернуть 9.75.
getTotalMinutes()
Возвращает количество минут в текущем промежутке времени (помните, что в интервале есть еще показатель часов, который нужно умножить на 60)
Вы должны продумать:
как хранить значение временного интервала;
типы входных аргументов методов;
корректно реализовать требуемые методы;
в методах предусмотреть проверки на корректность входных данных.
1.2. Добавление метода subtract()
Добавьте к написанному ранее классу метод subtract()со следующей сигнатурой
public void subtract(TimeSpan span)
Метод вычитает из текущего временного интервала входной временной интервал. Если входной интервал больше текущего, выйдите из метода и не модифицируйте текущий интервал (можете выбросить исключение IllegalArgumentException, если знаете что это и знаете как выбрить исключение).
1.3 Добавление метода scale()
Добавьте к написанному ранее классу метод subtract()со следующей сигнатурой
public void scale(int factor)
Метод увеличивает текущий интервал в factorчисло раз. Например, если текущий временной интервал равен 1 час 45 минут, а входной аргумент равен 2, то интервал увеличится до 3 часов 30 минут. Убедитесь, что factor- неотрицательное число, помните про пересчет минут в часы при увеличении интервала.
2. Класс BankAccount
2.1 Добавление поля transactionFee
В программе существует класс BankAccount, исходный код которого приведен ниже.
// Каждый объект класса BankAccount представляет данные одного
// счета пользователя, включая имя и баланс счета
public class BankAccount {
String name;
double balance;
public void deposit(double amount) {
balance = balance + amount;
}
public double getBalance() {
return this.balance;
}
public boolean withdraw(double amount) {
balance = balance - amount;
return true;
}
}
Каждый объект класса предназначен для описания одного счета клиента банка, включая его имя и баланс. Модифицируйте класс следующим образом:
добавьте поле transactionFeeтипа double, которое хранит сумму, которая вычитается из баланса каждый раз, когда клиент банка снимает деньги (метод withdraw()). Изначальное значение равно 0.00, но значение может быть изменено в дальнейшем. Производите вычитание суммы каждый раз, когда клиент осуществляет снятие денег;
сделайте так, чтобы в результате снятия денег и снятия transactionFee, баланс счета не мог опуститься ниже нуля. Если в результате снятия денег и transactionFee баланс может стать отрицательным, выйдите из метода и не производите модификацию баланса вообще;
в методe deposit() входной аргумент не должен быть ноль или меньше;
в методе withdraw() входной аргумент не должно быть ноль или меньше.
Модификация класса может потребовать создания новых методов и полей.
2.2 Добавление метода transfer()
Добавьте к модифицированному ранее классу BankAccoutметод transfer() со следующей сигнатурой
public boolean transfer(BankAccount receiver, double amount)
Метод transfer() осуществляет перевод денег из одного счета (текущего) на другой счет (объект receiver). Первый параметр - счет получателя денег, второй параметр - количество денег, которые переводятся со счета на счет.
Важно!
При снятии денег должна учитываться комиссия (поле transactionFee). Следовательно, из текущего объекта должна быть вычтено amount + transactionFee.
Метод должен модифицировать баланс двух объектов. У текущего объекта должна вычтена сумма amount + transactionFeeиз баланса, а у объекта receiverдолжен быть пополнен баланс счета на сумму amount.
При реализации метода убедитесь, что amount > 0.
Если у текущего объекта баланс меньше amount + transactionFee, то выйдите из метода без какого-либо перевода денег.
3. Класс Student
Создайте класс Student, который хранит информацию о студенте. У студента есть имя, год обучения (от 1 до 4), а также массив изучаемых дисциплин.
Класс Student должен иметь следующие методы
Название метода
Описание метода
Student(name, year)
Конструктор. Входные аргументы – имя и год обучения
addCourse(courseName)
Добавляет дисциплину(в формате String) к массиву дисциплин студента
dropAll()
Удаляет все дисциплины студента
getCourseCount()
Возвращает количество дисциплин, которые студент изучает
getName()
Возвращает имя студента
getTuition()
Возвращает количество денег, которые студент заплатил за обучение (с условием, что каждый год обучения обошелся студенту в 1000 гривен)
getYear()
Возвращает год обучения студента (от 1 до 4)
Вы должны продумать:
как хранить данные студенты (типы данных, структуры и так далее);
типы входных аргументов методов и тип возвращаемого значения;
корректно реализовать требуемые методы;
в методах предусмотреть проверки на корректность входных данных.
Инкапсуляция
Инкапсуляция в Java реализована с помощью использования модификаторов доступа.
Язык Java предоставляет несколько уровней защиты, которые позволяет настраивать область видимости данных и методов. В Java имеется четыре категории видимости элементов класса:
private– члены класса доступны только членам данного класса. Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию;
по умолчанию (package-private) – члены класса доступны классам, которые находятся в этом же пакете;
protected– члены класса доступны классам, находящимся в том же пакете, и подклассам – в других пакетах;
public– члены класса доступны для всех классов в этом и других пакетах.
Модификатор класса указывается перед остальной частью описания типа отдельного члена класса. Это означает, что именно с него должен начинаться оператор объявления класса.
public String errMessage;
private AccountBalance balance;
private boolean isError(byte status) {}
public class Account {}
Когда член класса обозначается модификатором доступа public, он становится доступным для любого другого кода в программе, включая и методы, определенные в других классах.
Когда член класса обозначается модификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену класса.
При отсутствии модификатора доступа, члены класса доступны другим членам класса, который находится в этом же пакете.
Модификатор доступа protected связан с использованием механизма наследования и будет рассмотрен позже.
Модификатор доступа указывается перед остальной частью описания типа отдельного члена класса (то есть, именно с модификатора доступа начинается объявление члена класса).
Член класса (переменная, конструктор, методы), объявленный public, доступен из любого метода вне класса.
Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию.
Сокрытие полей класса
В подавляющем большинстве случаев, поля класса объявляются как private (это не касается статических переменных и констант, там ситуация может быть другая). Должны быть веские основания объявить поле класса общедоступным. Манипулирование данными должно осуществляться только с помощью методов.
Для того чтобы дать возможность получить доступ к переменной или дать возможность изменить ее значение, объявляют специальные методы, которые называются "геттерами" и "сеттерами".
Геттер возвращает значение приватного поля, тогда как сеттер меняет значение приватного поля (новое значение передается в качестве аргумента метода).
Хотя сигнатура и имена геттеров и сеттеров могут быть любыми, приучите себя соблюдать строгий шаблон для объявления геттеров и сеттеров.
Геттер должен иметь префикс get, после которого идет название поля с большой буквы. Геттер, как правило, не имеет входных аргументов.
Сеттер должен иметь префикс set, после которого идет название поля с большой буквы. Сеттер принимает на вход новое значение поля. Возвращаемый тип, как правило, void.
Account.java
public class Account {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
Большинство IDE для Java имеют механизм для генерации геттеров и сеттеров. В IntelliJ IDEA нажмите комбинацию Alt+Insert находясь в окне редактирования java-файла. Откроется контекстное меню Generate, где вы можете выбрать генерацию геттера и сеттера, после чего указать поля, для которых необходимо сгенерировать методы.
5. Пример использования инкапсуляции
Представим, что нам необходимо создать класс «Корзина» (Cart), который хранит в себе набор объектов класса «Товар» (Item).
Какие методы «Корзина» должна предоставлять для внешнего использования? Это могут быть, например, методы «Добавить товар», «Убрать последний добавленный товар», «Подсчет суммы цен товаров в корзине», «Повышение цен в корзине на N процентов» и «Снижение цен в корзине на N процентов».
Название метода
Описание
public Cart(int capacity)
Конструктор с 1 параметром – максимальным количеством товаров в корзине.
public boolean addItem(Item item)
Добавление товара в корзину. Возвращает успешность операции.
public Item deleteLastAddedItem()
Удаление последнего добавленного товара в корзину. Возвращает удаленный товар.
public double calculateItemPrices()
Подсчет суммы цен всех товаров в корзине.
public void raiseItemPrices(double percent)
Поднять цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).
public void cutItemPrices(double percent)
Снизить цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).
Как вы можете заметить, это публичные методы, а значит, их можно вызвать через оператор-точку имея ссылку ну объект.
Cart cart = new Cart();
cart.addItem(new Item("Клавиатура", 2000));
Перечень этих публичных методов и составляет интерфейс класса – то есть, с помощью этих методов объект класса будет взаимодействовать с внешним миром.
Эти методы имеют вполне четко определенные входные аргументы и могут возвращать значения четко определенных типов, и никак иначе. По аналогии с этим, поворот колес автомобиля осуществляется четко определенным образом – поворотом руля, и бензин надо заливать в четко определенное отверстие крышки бензобака, а не как-то еще.
То – как будет реализовано хранение товаров в корзине – это внутренняя логика класса и она не должна быть доступна внешнему миру, она должна быть скрыта от внешнего вмешательства. Другие классы, которые будут использовать объекты класса Cartне должны знать и не должны иметь доступ к тому – как там «внутри» реализовано хранение товаров, подсчет цен и изменение цены на определенный процент и так далее, они могут только лишь использовать предоставленные им публичные методы. Давайте реализуем «Корзину» с помощью структуры «стек», которая, в свою очередь, реализована обычным массивом.
Cart.java
public class Cart {
private Item[] stack; // массив для реализации стека
private int topIndex; // указатель на вершину стека
// При создании корзины мы должны
// указать максимальное количество элементов
// в корзине
public Cart(int capacity) {
stack = new Item[capacity];
topIndex = -1;
}
// Добавление нового товара в корзину
public boolean addItem(Item item) {
return push(item);
}
// Приватный метод, который реализует добавление в стек
private boolean push (Item item) {
// Добавляем товар в стек
return true; // или false если не стек переполнен
}
// Удаление последнего добавленного товара в корзину
public Item deleteLastAddedItem() {
return pop();
}
// Приватный метод, который реализует извлечение из стека
private Item pop() {
return new Item(); // Извлеченный из стека товар
}
}
Как мы видим, массив с товарами, указать на вершину стек объявлены как privateчлены класса. Это значит, что мы не можем получить к ним доступ извне – они доступны только внутри данного класса.
Программиста, который будет использовать класс Cart, не должна волновать ситуация с переполнением стека, с попыткой извлечь элемент из пустого стека, он не должен следить за указателем на вершину стека, он даже не должен знать что это стек.
Для него объект класса Cart это некоторый объект, который предоставляет «услугу» в виде корзины товаров и с этой корзиной можно работать с помощью определенных публичных методов.
В дальнейшем мы можем переделать класс Cart и поменять внутреннюю реализацию. Мы можем использовать структуру "очередь", мы можем использовать коллекции, мы можем иначе реализовать операции добавления и удаления элемента в стеке, но если мы сохраним интерфейс класса неизменным, то для внешнего мира эти изменения внутренней логики не будут важны и если мы поменяем внутреннюю логику одного небольшого участка программы, то вся остальная программа будет работать так же.
2. Основы синатксиса Java
При разработке языка Java был взят за основу синтаксис языков С и C++, поэтому, многие аспекты синтаксиса языка покажутся вам знакомыми.
Комментарии
В Java, как и в C, существуют однострочные и блоковые комментарии. Однако, кроме этого, согласно конвенции Oracle, существуют другие виды комментариев: copyright-блок вверху, doc-комментарии, TODO-комментарии, комментарии после statement`ов, комментарии для комментирования кода. Ознакомьтесь с принятыми правилами использования комментариев, на защите лабораторных они будут требоваться в обязательном порядке, согласно принятым конвенциям.
package ai201.ivanov;
/**
* Это специальный комментарий для документирования (Doc comment).
* С помощью специальной утилиты (javadoc), такие комментарии можно превратить в HTML-странички,
* которые вместе создают документацию к вашей программе.
* Для удобства, здесь можно использовать специальные ссылки {@link Main} и <i>HTML-тэги</i>
*/
public class Main {
/**
* Этот метод является "точкой входа" приложения. В проекте может быть только один метод с такой сигнатурой
* @param args аргументы при запуске приложения
*/
public static void main(String[] args) {
// Это однострочный комментарий (single line comment)
/*
* Это комментарий в виде блока (block comment)
*/
// TODO: это специальный TODO-комментарий. Тут можно описать, что нужно доделать на каком-то участке кода
// FIXME: это тоже TODO-комментарий, обычно тут мелкие баги и что нужно исправить на данном участке кода
// TODO:2019-09-03:NickGodov: дописать вывод данных в файл
int my_variable = 5; /* Комментарии после statement должны быть выровнены слева */
int my_other_variable = 999; /* с помощью табуляции */
// Если нужно закомментировать код, то каждая строчка комментируется однострочным комментарием
// int my_old_variable = 100;
// int my_other_old_variable = 200;
// Перед комментарием принято оставлять пустую строку
int number = 924;
}
}
Переменные
Переменные чувствительны к регистру (variableи Variable– две разные переменные), могут быть бесконечной длины, состоять из букв юникода и цифр юникода, символов _и $.
Первый символ переменной может быть буквой, символом _ или $ (использовать _ или $ первым символом КРАЙНЕ НЕ РЕКОМЕНДУЕТСЯ, они существуют для специальных ситуаций, которые нас сейчас не интересуют, так что считайте, что начинаться переменная может только с буквы юникода). Крайне не рекомендуется использовать буквы национальных алфавитов, кириллицу, транслит. Только латинские буквы, названия на английском. Также, названия переменных не должны совпадать со списком зарезервированных слов, который представлен ниже.
abstract
continue
for
new
switch
assert
default
goto
package
synchronized
boolean
do
if
private
this
break
double
implements
protected
throw
byte
else
import
public
throws
case
enum
instanceof
return
transient
catch
extends
int
short
try
char
final
interface
static
void
class
finally
long
strictfp
volatile
const
float
native
super
while
record
var
yield
Кроме ключевых слов в Java существуют три литерала: null, true, false, не относящиеся к ключевым и зарезервированным словам, а также зарезервированное слово var, значение которого зависит от его позиции в коде.
Приведенный ниже блок кода даст вам общее представление о том, как надо называть переменные
int spd = 25; // ПЛОХО: Можно, но не рекомендуется, т.к. название не информативно
int carminspd = 25; // ПЛОХО: Не экономьте на названиях переменных!
int carMinSpeed = 25; // ХОРОШО: Название переменной говорит само за себя
int s = 0; // МОЖНО: однобуквенные допускаются, только если это какие-то короткоживущие бросовые переменные
int speed = 150; // ХОРОШО: Нормально, понятно, что переменная отвечает за скорость
int Speed = 150; // ПЛОХО: Крайне не рекомендуется, переменные не должны начинаться с капса
int SPEED = 150; // ПЛОХО: Крайне не рекомендуется, полностью капсом пишутся константы
const int MIN_SPEED = 25; // НЕЛЬЗЯ: в Java const не используется (хотя это зарезервированное слово)
final int MIN_SPEED = 25; // ХОРОШО: в Java для констант используется final
int моя_переменная = 356; // ПЛОХО: Только латиница
int DŽDžDzȯȺώϷЂ = 145; // ПЛОХО: Только латиница
int moya_peremennaya = 29; // ПЛОХО: Транслит - это полный моветон, только английский!
int $myvar = 100; // ПЛОХО: Теоретически можно, но НЕ РЕКОМЕНДУЕТСЯ
int _myvar = 100; // ПЛОХО: Теоретически можно, но НЕ РЕКОМЕНДУЕТСЯ
int 2pac = 0; // НЕЛЬЗЯ: с цифры начинать нельзя
int %d = 5; // НЕЛЬЗЯ: с других знаков начинать нельзя
int 'f' = 5; // НЕЛЬЗЯ: с кавычек начинать нельзя
// Если название переменной состоит из двух слов
int max_speed = 150; // ПЛОХО: Использовать _ для отделения слов не в константах не рекомендуется
int MaxSpeed = 150; // ПЛОХО: Крайне не рекомендуется, переменные не должны начинаться с заглавной буквы
int maxSpeed = 150; // ХОРОШО: Вот так нормально, используется lowerCamelCase
final int MAX_SPEED = 150; // ХОРОШО: Константы пишутся капсом, каждое слово отделяется _
Типы данных
В языке Java существуют примитивные типы (аналогичны типам данных в C) и ссылочные (или объектные) типы данных. На данный момент нас интересуют только примитивные типы данных.
Java – строго типизированный язык программирования. Это значит, что переменная, перед использованием, должна быть объявлена и ей должен быть присвоен тип, который нельзя поменять. Также, при выполнении операций присваивания, компилятор проверяет соответствие типов (какого-то механизма автоматического приведения типов у Java нет).
Всего существуют восемь примитивных типов данных: int, long, short, byte, double, float, char, boolean. Их очень легко запомнить:
4 типа для целых чисел («короткое short», «среднее int», «длинное long» и байт);
2 типа для чисел с плавающей запятой (старая парочка double и float);
2 «специальных» типа – символ и булевый тип.
Type
Min
Max
RAM
Default
Объявления и литералы
byte
-128
127
8 bit
0
byte b = 100;
short
-32,768
32,767
16 bit
0
short b = 10000;
int
-2^31
-2^31-1
32 bit
0
int a = 15;
int aHex = 0xaa;
int aBin = 0b0001111;
(это же справедливо и для byte,short,long, если соблюдать диапазоны)
long
-2^63
-2^63-1
64 bit
0L
long number = 10000L;
double
4.9^-324
~1.8^308
64 bit
0.0d
double d = 6.6;
float
~1.4^-45
~3.4^38
32 bit
0.0f
float f = 5.5f;
char
0
65535
16 bit
'\u0000'
char c = ‘f’;
char c = 63;
char c = '\u2422';
boolean
false
true
1 bit
false
boolean b = true;
Операторы ветвления
Операторы ветвления в C и Java практически идентичны
int a = 5;
int b = 4;
int min;
// Так нужно оформлять обычный if
if (a >= b) {
min = b;
}
// Так нужно оформлять if-else
if (a >= b) {
min = b;
} else {
min = a;
}
// Так нужно оформлять if-else if-else
if (a > b) {
min = b;
} else if (a < b) {
min = a;
} else {
min = a;
}
// В Java используется тернарный оператор
min = (a >= b) ? b : a;
// Это равнозначно следующему выражению
if (a >=b) {
min = b;
} else {
min = a;
}
// Так оформляется switch
switch (a) {
case 1:
// что-то делаем
break;
case 2:
// делаем что-то другое
break;
default:
// это выполняется в том случае, если ни одно из кейсов не выполнился
}
Циклы
Работа с циклами в Java мало чем отличается от языка C
int progression = 0;
// Так оформляется for
for (int i=0; i < 5; i++) {
progression +=i;
}
// ПЛОХО: так оформлять циклы не рекомендуется
for (int i=0; i < 5; i++) progression +=i;
// МОЖНО: если тело цикла состоит из одного statement'а, то можно опустить
// фигурные скобки
for (int i=0; i < 5; i++)
progression +=i;
// Пустой for
for (int j=0; j < 10; j++);
// Так оформляется while
int iterator = 0;
while (iterator < 10) {
// делаем что-то в цикле
iterator++;
}
// Так оформляется do-while
int loops = 10;
do {
// что-то делаем
loops--;
} while (loops > 0);
// Также, в Java есть аналог foreach
int[] array = { 1, 2, 3, 4, 5 };
int sum = 0;
for(int i : array) {
sum += i;
}
// Этот же цикл можно представить обычным for`ом
for(int i = 0; i < 5; i++) {
sum += array[i];
}
Массивы
Работа с массивами в Java несколько отличается от работы с массивами в C, в основном, из-за механизма выделения памяти под массивы.
// Объявление массивов
/*
* ХОРОШО: согласно всем соглашениям по коду и различным рекомендациям, квадратные скобки
* ставят ПОСЛЕ ТИПА ДАННЫХ
*/
int[] goodArray;
/*
* ПЛОХО: компилятор не выдаст ошибку, но такой синтаксис делает код менее читабельным
*/
int badArray[];
/*
* НЕЛЬЗЯ: при объявлении массива нельзя указать его размерность.
* Java не выделит память, пока массив не будет инициализирован
*/
int[5] anotherBadArray;
// Объявления многомерных массивов
int [][] twoDimensionalArray;
int [][][] threeDimensionalArray;
// Инициализация массивов
goodArray = new int[10]; // Инициализируем массив с 10 элементами
goodArray[0] = 15; // Присваиваем значение первому элементу массива
goodArray[1] = 25; // Присваиваем значение второму элементу массива
twoDimensionalArray = new int [5][4]; // Двумерный массив 5х
twoDimensionalArray[0] = new int[4];
twoDimensionalArray[1] = new int[8]; // ПЛОХО: Компилятор проглотит, но по факту выделится место всего под 4 инта
twoDimensionalArray[0][0] = 1; // Присваиваем значение
twoDimensionalArray[1][5] = 5; // НЕЛЬЗЯ: Компилятор выдаст ошибку
System.out.print(twoDimensionalArray[1][6]); // НЕЛЬЗЯ: Компилятор выдаст ошибку
// Объявление с инициализацией
int[] quickArray = {1, 2, 3 ,4}; // Объявляем и сразу заполняем данные. Компилятор выделит место под 4 инта
quickArray[5] = 6; // НЕЛЬЗЯ: Компилятор выдаст ошибку, т.к. индекс выходит за пределы массива
int[][] quick2DArray = {
{1 ,2 ,3},
{1, 3, 4}
};
Методы
Так как Java является объектно-ориентированным языком, функции здесь называются методами (на данный момент мы будем считать, что методы и функции выполняют одну и ту же роль, но методы могут находиться только внутри классов).
/*
* Синтаксис функции:
* [1-модификаторы доступа] [2-тип возвращаемого значения] [3-имя]([4-аргументы]) [5-список исключений] {
* 6- тело функции
* }
*
* 1 - модификаторы доступа: на данный момент они нас не интересует, можно ничего не писать или писать private
* 2 - тип возвращаемого значения - тип данных либо void, если функция ничего не возвращает
* 3 - ограничения как и на имена переменных, но есть дополнительные правила наименования, о них ниже
* 4 - список аргументов через запятую. Например (int a, double b). Если нет аргументов - пустые скобки
* 5 - исключение пока не рассматриваем, если их нет, то просто ничего не пишут
* 6 - тело функции, в нем происходит выполнение функции. Если есть возвращаемый тип данных - должен быть return
*/
private int findMinimum(int a, int b) {
int min;
min = (a < b) ? a : b;
return min;
}
/*
* Название метода начинается с маленькой буквы, если несколько слов - используется lowerCamelCase.
* Первое слово должно быть глаголом (т.к. метод, как правило, "что-то делает"), остальные слова могут быть
* прилагательными, существительными и тд. Символ _ крайне желательно не использовать (кроме юнит-тестов)
*/
// ХОРОШО: с маленькой буквы, первое слово - глагол
private void drawCircle() {}
// ПЛОХО: Символы $ и _ не используем
private void $er () {}
// ПЛОХО: используйте camelCase для названия методов
private void draw_circle () {}
// ПЛОХО: название метода должно начинаться с маленькой буквы
private void Draw() {}
// ПЛОХО: первое слово должно быть глаголом
private void circle() {}
Лабораторная работа 1
В данной лабораторной работе мы установим необходимое программное обеспечение и познакомимся с базовым синтаксисом и операторами языка Java.
Тема: Установка программного обеспечения. Базовый синтаксис Java.
Цели лабораторной работы:
установить программное обеспечение для программирования на языке Java;
изучить базовый синтаксис языка Java и сравнить его с языком C;
реализовать несколько тестовых задач на языке Java.
Тема: Повторное использование кода. Наследование и композиция
Цель лабораторной работы:
изучить принцип наследования;
изучить принцип композиции;
разобраться в чем отличие наследования от композиции и когда следует применять тот или иной принцип;
научиться отображать композицию и наследование в диаграмме классов UML.
Лабораторная работа 2
Тема: Создание классов и объектов
Цель лабораторной работы:
изучить синтаксис и процесс создания классов;
научиться создавать объекты созданных классов.
Язык моделирования UML
UML – унифицированный язык моделирования (Unified Modeling Language) – это система обозначений, которую можно применять для объектно-ориентированного анализа и проектирования. Его можно использовать для визуализации, спецификации, конструирования и документирования программных систем.
Словарь UML включает три вида строительных блоков:
Диаграммы.
Сущности.
Связи.
Сущности – это абстракции, которые являются основными элементами модели, связи соединяют их между собой, а диаграммы группируют представляющие интерес наборы сущностей.
Диаграмма – это графическое представление набора элементов, чаще всего изображенного в виде связного графа вершин (сущностей) и путей (связей). Язык UML включает 13 видов диаграмм, среди которых на первом месте в списке — диаграмма классов.
Диаграмма классов описывает типы объектов системы и различного рода статические отношения, которые существуют между ними. На диаграммах классов отображаются также свойства классов, операции классов и ограничения, которые накладываются на связи между объектами.
Если кто-нибудь подойдет к вам в темном переулке и спросит: «Хотите посмотреть на диаграмму UML?», знайте – скорее всего, речь идет о диаграмме класса. Большинство диаграмм UML, которые я встречал, были диаграммами классов.
- Мартин Фаулер
Графически класс изображается в виде прямоугольника, разделенного на три блока горизонтальными линиями:
имя класса;
атрибуты (свойства) класса;
операции (методы) класса.
Для атрибутов и операций может быть указан один из трех типов видимости:
- private;
~ без модификатора (default);
# protected;
+ public.
Видимость для полей и методов указывается в виде левого символа в строке с именем соответствующего элемента.
Каждый класс должен обладать именем, отличающим его от других классов. Имя – это текстовая строка. Имя класса может состоять из любого числа букв, цифр и знаков препинания (за исключением двоеточия и точки) и может записываться в несколько строк.
Атрибут (свойство) – это именованное свойство класса, описывающее диапазон значений, которые может принимать экземпляр атрибута. Класс может иметь любое число атрибутов или не иметь ни одного. В последнем случае блок атрибутов оставляют пустым. Можно уточнить спецификацию атрибута, указав его тип, кратность (если атрибут представляет собой массив некоторых значений) и начальное значение по умолчанию.
Статические атрибуты класса обозначаются подчеркиванием.
Операция (метод) – это реализация метода класса. Класс может иметь любое число операций либо не иметь ни одной. Часто вызов операции объекта изменяет его атрибуты. Графически операции представлены в нижнем блоке описания класса. Допускается указание только имен операций. Имя операции, как и имя класса, должно представлять собой текст. Можно специфицировать операцию, устанавливая ее сигнатуру, включающую имя, тип и значение по умолчанию всех параметров, а применительно к функциям – тип возвращаемого значения.
Абстрактные методы класса обозначаются курсивным шрифтом. Статические методы класса обозначаются подчеркиванием.
Изображая класс, не обязательно показывать сразу все его атрибуты и операции. Для конкретного представления, как правило, существенна только часть атрибутов и операций класса. В силу этих причин допускается упрощенное представление класса, то есть для графического представления выбираются только некоторые из его атрибутов. Если помимо указанных существуют другие атрибуты и операции, вы даете это понять, завершая каждый список многоточием. Чтобы легче воспринимать длинные списки атрибутов и операций, желательно снабдить префиксом (именем стереотипа) каждую категорию в них. В данном случае стереотип – это слово, заключенное в угловые кавычки, которое указывает то, что за ним следует.
Рассмотрим пример класса - его графическое представление и код на языке Java.
Human.java
public class Human {
private String name; // private это "-"
Boolean gender = true; // default это "~"
protected long chromosome; // protected это "#"
public int age; // public это "+"
// Статические атрибуты подчеркиваются
public static long dna;
// Константы можно отобазить как readOnly
final int SECRET = 924;
/* Как правило, конструкторы
* изображаются как обычные методы */
public Human() {}
public Human (String name) {this.name = name;}
/* Методы отображаются как
* [-~#+]имя(тип_аргументов): возвращаемый тип
* Например: public String foo (int a, double b)
* будет +foo(int, double): String */
public void breath() {}
private void sleep(int hours) {}
protected boolean sneeze() { return true; }
int run (int speed, String direction) { return 0; }
public static int calculateAge() { return 0; }
}
1. Установка программного обеспечения, создание и запуск проекта
Для работы с Java, вам нужно установить JDK и среду разработки.
Установка JDK
Java Development Kit (сокращенно JDK) — бесплатно распространяемый компанией Oracle Corporation (ранее Sun Microsystems) комплект разработчика приложений на языке Java, включающий в себя компилятор Java (javac), стандартные библиотеки классов Java, примеры, документацию, различные утилиты и исполнительную систему Java (JRE).
Установка JDK, как правило, проходит без проблем, настраивать параметры установки не требуется.
Установка среды разработки
Интегрированная среда разработки (Integrated development environment — IDE) — комплекс программных средств, используемый программистами для разработки программного обеспечения.
Для Java существует "большая тройка" сред разработки:
Все остальные среды являются более простыми и менее функциональными.
В рамках данного курса будет использоваться среда IntelliJ IDEA, которая считается самой популярной IDE для Java. Вы можете выполнять лабораторные работы любым удобным вам способом, в любой удобной вам среде разработки.
В рамках данного курса будет использована Community версия среды IntelliJ IDEA. Скачиваем и запускаем с сайта разработчика установщик.
В процессе установки желательно выбрать следующие параметры установки
Добавление плагинов для выполнения лабораторной работы
Кроме установки среды разработки, нам необходимо добавить дополнительные плагины - EduTools и CheckStyle.
Запустим IntelliJ IDEA, после чего выберем пункт Configure -> Plugins.
Найдите в поисковой строке EduTools и установим его.
После этого введем в поисковой строке CheckStyle и также установим его. После этого необходимо перезапустить среду разработки.
Создание проекта и запуск приложения
При первом запуске вас попросят выбрать цветовую схему среды разработки. Остальные параметры можете опустить. В стартовом окне выбираем пункт "Create New Project".
В следующем окне обратите внимание, что вас просят выбрать JDK, который будет использован в вашем проекте.
Нажмите кнопку "New..." и укажите путь к установленному ранее JDK (как правило, это C:\Program Files\Java\xxxx)
В следующем окне выбираем пункт "Create project from template" и выбираем "Command Line App"
Далее нам необходимо указать название проекта, а также Base Package проекта. Далее мы разберемся, что означает термин package, а пока можете указать его в формате [группа].[фамилия]
После завершения мастера нового проекта, IDEA сгенерирует новый пустой проект. Сразу запустим проект. Для этого можно выбрать пункт Run -> Run "Main", нажать комбинацию клавиш Shift+F10 либо нажать на зеленый треугольник справа в верхней части редактора.
Лекция 15
Тема: Компиляция и исполнение Java-приложений. Виртуальная машина Java. JRE и JVM. Сборка мусора. Cильные, слабые, мягкие и фантомные ссылки. WeakReference, SoftReference, PhantomReference, WeakHashMap и ReferenceQueue.
Языки программирования
Мы уже знаем, что программа – это просто набор инструкций, который говорит компьютеру что делать. Очевидно, что мы должны предоставить эти инструкции на языке, который компьютер может понять.
Было бы здорово, если бы компьютер понимал человеческую речь, и мы могли бы просто сказать компьютеру, что нужно сделать (как в научной фантастике). Но это пока остается фантастикой и, к тому же, «натуральные» языки тяжело подходят для описания сложных алгоритмов, так как любой «натуральный» язык наполнен неточностями и двусмысленностью.
Для того, чтобы общаться с компьютерами, учеными были придуманы специальные нотации, чтобы можно было точно и недвусмысленно сформулировать набор инструкций для компьютера. Эти нотации называются языками программирования.
Каждая запись в языке программирования имеет точную форму (синтаксис) и точное значение (семантику).
Язык программирования это что-то вроде кода для записи инструкций, которым будет следовать компьютер. Программисты часто говорят о программах как о компьютерном коде, а сам процесс записи алгоритма на языке программирования называют кодированием.
Языки низкого уровня
При использовании языков программирования возникает противоречие. Центральный процессор может понимать только машинный код – набор команд с аргументами. Машинный код тяжело воспринимается человеком и на нем тяжело написать хоть сколько-нибудь серьезную программу. Такого рода языки называются языками низкого уровня (low-levellanguage).
Например, вывод надписи «Hello, world!» в поток вывода в машинном коде выглядит следующим образом
b8 21 0a 00 00 #moving "!\n" into eax
a3 0c 10 00 06 #moving eax into first memory location
b8 6f 72 6c 64 #moving "orld" into eax
a3 08 10 00 06 #moving eax into next memory location
b8 6f 2c 20 57 #moving "o, W" into eax
a3 04 10 00 06 #moving eax into next memory location
b8 48 65 6c 6c #moving "Hell" into eax
a3 00 10 00 06 #moving eax into next memory location
b9 00 10 00 06 #moving pointer to start of memory location into ecx
ba 10 00 00 00 #moving string size into edx
bb 01 00 00 00 #moving "stdout" number to ebx
b8 04 00 00 00 #moving "print out" syscall number to eax
cd 80 #calling the linux kernel to execute print to stdout
b8 01 00 00 00 #moving "sys_exit" call number to eax
cd 80 #executing it via linux sys_call
Языки высокого уровня
Языки высокого уровня (high-level language), например: C, Python, Java, C++ и другие, призваны облегчить написание программы за счет того, что язык хорошо понятен для человека и оперирует привычными ему понятиями, а не регистрами, адресами памяти и так далее. К примеру, на языке Python та же программа (вывод сообщения «Hello, world!») выглядит следующим образом:
main.py
print("Hello, world!")
Но как преобразовать код на языке высокого уровня в набор инструкций на машинном языке? Ведь центральный процессор не понимает Python, C или Java.
Существует множество подходов для реализации этого преобразования, рассмотрим два из них.
Первый подход – компилируемые языки программирования. Компилятор – это сложная компьютерная программа, которая принимает на вход код высокого уровня и преобразует его в эквивалентную программу на машинном языке. Компиляторы пишутся для конкретного языка, для конкретного типа процессора и для конкретной операционной системы.
Программа на языке высокого уровня называется исходным кодом, а в результате получается машинный код, который может напрямую выполнять процессор.
Второй подход – интерпретируемые языки программирования.Интерпретатор – специальная программа, которая симулирует компьютер, который понимает язык высокого уровня. Вместо того чтобы преобразовывать (транслировать) исходный код в эквивалентный машинный код, интерпретатор анализирует и выполняет исходный код инструкция за инструкцией (то есть, напрямую с процессором работает не ваш код, а интерпретатор). Интерпретатор еще называют виртуальной машиной.
Существует множество плюсов и минусов того или иного подхода, но сразу стоит отметить одно важное отличие – портируемость (portability). Компилируемые языки программирования имеют низкую портируемость кода за счет того, что необходимо адаптировать код для конкретного процессора и конкретной операционной системы. К тому же, для некоторых конфигурацией может не быть готового компилятора или он может быть неэффективным.
В случае использования интерпретируемых языков, портируемость выше. Разработчики языка, а также сообщество предоставляют интерпретаторы для всевозможных устройств – от микроконтроллеров до полноценных ПК. Разработчикам не надо задумываться о том, где будет запускаться ваш код и нет нужды предоставлять разные версии приложения для разных конфигураций.
Для разработчика программы на интерпретируемом языке программирования, интерпретатор для определенной конфигурации ПК ничем не отличается от интерпретатора, например, для микрокомпьютера Raspberry Pi. Если для того или иного устройства реализован интерпретатор, вы можете запустить на нем ваш код без проблем.
Схема работы Java
Основная особенность Java, которая позволяет решать проблемы безопасности и переносимости программ, состоит в том, что компилятор Java выдает не исполняемый код, а так называемый байт-код (byte-code) - оптимизированный набор инструкций, предназначенный для выполнения в исполняющей среде Java, называемой виртуальной машиной Java (Java Virtual Machine).
Трансляция программы Java в байт-код значительно упрощает ее выполнение в разнотипных средах, поскольку на каждой платформе необходимо реализовать только виртуальную машину (JVM). Если в отдельной системе можно запустить JRE, то в ней можно выполнять любую программу на Java. Виртуальные машины Java, вне зависимости от платформы, интерпретируют один и тот же байт-код одинаково. Таким образом, разработчикам нет нужды предоставлять различные версии программы для различных платформ.
Тот факт, что программа на Java выполняется виртуальной машиной также способствует повышению ее безопасности. Виртуальная машины управляет выполнением программы, поэтому она может изолировать программу и воспрепятствовать возникновению побочных эффектов от ее выполнения за пределами данной системы.
Недостатком такого подхода можно считать то, что когда программа компилируется в промежуточную форму, а затем интерпретируется виртуальной машиной, она выполняется медленнее, чем если бы она была скомпилирована в исполняемый код. Но в Java это отличие в производительности не слишком заметно. Байт-код существенно оптимизирован, и поэтому его применение позволяет виртуальной машине выполнять программы значительно быстрее, чем следовало ожидать.
Язык Java был задуман как интерпретируемый, но ничто не препятствует ему оперативно выполнять компиляцию байт-кода в машиннозависимый код для повышения производительности. Поэтому вскоре после выпуска Java появилась технология HotSpot, которая предоставляет динамический компилятор (или так называемый JIT-компилятор, JIT - Just In Time) байт-кода. В этом случае избранные фрагменты байт-кода компилируются в исполняемый код по частями, в реальном времени и по требованию.
Одновременная компиляция всей программы Java в исполняемый код нецелесообразна, поскольку Java производит различные проверки, которые могут быть сделаны только во время выполнения. Вместо этого динамический компилятор компилирует код во время выполнения по мере надобности. Более того, компилируются не все фрагменты байт-кода, а только те, которым компиляция принесет выгоду, а остальной код просто интерпретируется. Тем не менее принцип динамической компиляции обеспечивает значительное повышение производительности.
Сборка мусора и методы завершения
Как было сказано ранее, при использовании оператора new свободная память для создаваемых объектов динамически выделяется из пула свободной памяти. Разумеется, оперативная память не бесконечна, и поэтому свободная память рано или поздно исчерпывается. Это может привести к невозможности выполнения оператора new из-за нехватки памяти для создания требуемого объекта.
Именно по этой причине одной из главных функций любой схемы динамического распределения памяти является своевременное освобождение памяти от неиспользуемых объектов, чтобы сделать ее доступной для последующего распределения.
Во многих языках программирования освобождение памяти происходит вручную (например, в C++ для этого используется оператор delete). В Java применяется другой, более надежный механизм, который называется сборка мусора.
Система сборки мусора Java освобождает память от лишних объектов автоматически, без вмешательства со стороны программиста. Эта система работает следующим образом: если ссылки на объект отсутствуют (на объект не ссылается ни одна переменная), то такой объект считается больше ненужным, и занимаемая им память в итоге возвращается в пул свободной памяти и, в дальнейшем, может быть распределена для других объектов. Сборка мусора осуществляется время от времени по ходу выполнения программы (обычно, когда количество свободной памяти подходит к концу).
Garbage collection
Сборщик мусора выполняет всего две задачи:
поиск "мусора";
очистка памяти от "мусора".
Для обнаружения мусора существует два подхода:
Reference counting - подсчет ссылок;
Tracing - трассировка.
Ясно, теперь нужно ответить на два вопроса: "Как Garbage Collector обнаруживает мусор?" и "Как очищает память?"
Reference counting
Суть подхода состоит в том, что каждый объект имеет счетчик. Счетчик хранит информацию о том, сколько ссылок указывает на объект. Когда ссылка уничтожается, счетчик уменьшается. Если значение счетчика равно нулю, то объект можно считать "мусором" и память можно очищать.
Главным минусом такого подхода является сложность обеспечения точности счетчика. Также при таком подходе сложно выявлять циклические зависимости (когда два объекта указывают друг на друга, но ни один живой объект на них не ссылается). Это приводит к утечкам памяти.
В общем, Reference counting редко используется из за недостатков. Во всяком случае HotSpot VM его не использует.
Tracing
В методе трассировка главная идея состоит в мысли: "Живые объект - те до которых мы можем добраться с корневых точек (GC Root), все остальные - мусор. Все что доступно с живого объекта - также живое".
Если мы представим все объекты и ссылки между ними как дерево, то нам нужно пройти с корневых узлов по всем узлам. При этом узлы, до которых мы сможем добраться - не мусор, все остальные - мусор.
Данный подход обеспечивает выявление циклических ссылок и используется в виртуальной машине HotSpot VM. Теперь необходимо понять, что из себя представляет корневая точка (GC Root)? Существуют следующие типы корневых точек:
основной Java-поток;
локальные переменные в основном методе;
статические переменные основного класса.
Таким образом, простое java-приложение будет иметь следующие корневые точки:
параметры метода main() и локальные переменные внутри метода main();
поток, который выполняет метод main();
статические переменные основного класса, внутри которого находится метод main().
Очистка памяти
Имеется несколько подходов к очистке памяти, которые в совокупности определяют принцип функционирования GC. JVM HotSpot использует алгоритм сборки мусора типа "Generational Garbage Collection", который позволяет применять разные модули для разных этапов сборки мусора. Всего в HotSpot реализовано четыре сборщика мусора:
Serial GC;
Parallel GC;
CMS GC;
G1 GC.
Serial GC относится к одним из первых сборщиков мусора в HotSpot VM. Во время работы этого сборщика приложение приостанавливается и возобновляет работу только после прекращения сборки мусора. В Serial Garbage Collection область памяти делится на две части («young generation» и «old generation»), для которых выполняются два типа сборки мусора:
minor GC - частый и быстрый с областью памяти "young generation";
mark-sweep-compact - редкий и более длительный с областью памяти "old generation".
Область памяти «young generation» разделена на две части, одна из которых Survior также разделена на 2 части (From, To).
Алгоритм работы minor GC
Алгоритм работы minor GC очень похож на метод «Copying collectors». Отличие связано с дополнительным использованием области памяти «Eden». Очистка мусора выполняется в несколько шагов:
приложение приостанавливается на начало сборки мусора;
«живые» объекты из Eden перемещаются в область памяти «To»;
«живые» объекты из «From» перемещаются в «To» или в «old generation», если они достаточно «старые»;
Eden и «From» очищаются от мусора;
«To» и «From» меняются местами;
приложение возобновляет работу.
В результате сборки мусора картинка области памяти изменится и будет выглядеть следующим образом:
Некоторые объекты, пережившие несколько сборок мусора в области From, переносятся в «old generation». Следует, также отметить, что и «большие живые» объекты могут также сразу же переместиться из области Eden в «old generation» (на картинке не показаны).
Алгоритм работы mark-sweep-compact
Алгоритм «mark-sweep-compact» связан с очисткой и уплотнением области памяти «old generation».
Принцип работы «mark-sweep-compact» похож на описанный выше «Mark-and-sweep», но добавляется процедура «уплотнения», позволяющая более эффективно использовать память. В процедуре живые объекты перемещаются в начало. Таким образом, мусор остается в конце памяти.
При работе с областью памяти используется механизм «bump-the-pointer», определяющий указатель на начало свободной памяти, в которой размещается создаваемый объект, после чего указатель смещается. В многопоточном приложении используется механизм TLAB (Thread-Local Allocation Buffers), который для каждого потока выделяет определенную область памяти.
Серия статей про сборщик мусора в HotSpot:
Метод finalize()
В Java предусмотрена возможность определить метод, который называется финализатор, который будет вызван непосредственно перед окончательным удалением объекта из памяти. Этот метод называется finalize(). Он позволяет убедиться, что объект можно безболезненно удалить (например, его можно использовать для закрытия файла, ранее открытого удаляемым объектом).
Для того, чтобы добавить в класс финализатор, достаточно определить метод finalize(). Исполняющая система Java вызовет этот метод перед фактическим удалением объекта. В теле метода finalize() следует предусмотреть действия, которые должны быть выполнены непосредственно перед удалением объекта.
protected void finalize() {
// …
}
Важно понимать, что метод finalize() вызывается тогда, когда система сборки мусора принимает решение об освобождение памяти, занимаемой данным объектом. Но если объект в это время оказывается вне области видимости, то метод finalize() не будет вызван. Поэтому невозможно предсказать, когда именно и при каких условиях метод finalize() будет выполнен. Например, если программа успеет завершиться еще до того, как будет запущена процедура сборки мусора, то вызова метода finalize() не произойдет. Поэтому, нормальная работа программы не должна зависеть от того, вызван или не вызван методfinalize().
Компилятор Java генерирует не исполняемый машинный код, а так называемый байт-код – набор инструкций, предназначенный для выполнения в исполняющей среде Java, которая называется виртуальной машиной Java (Java Virtual Machine – JVM).
Таким образом, для каждой платформы необходимо реализовать только виртуальную машину Java, после чего, на ней можно запускать программы на Java, т.к. все виртуальные машины одинаково выполняют байт-код.
Недостатком такого подхода является более медленная скорость работы программ за счет наличия исполняемой среды в качестве посредника.
Наличие исполняемой среды дает много преимуществ, среди которых следует выделить наличие сборщика мусора.
Сборщик мусора – специальный процесс, который запускается периодически и освобождает память, использованную объектами, которые больше не нужны. Таким образом, программисту не нужно следить за распределением памяти.
Представим себе класс Cart, который хранит набор положенных в корзину товаров и класс Order, в задачу которого входит формирование заказа из помещенных в корзину товаров. Для упрощения примера, представим себе, что "корзина" реализована обычным массивом и можем содержать не более 5 элементов.
Давайте внимательно посмотрим на метод makeOrder(). Он получает на вход объект Cart, после чего он должен взять каждое наименование и цену товара и, допустим, добавить его в базу данных. Как это сделать?
Самый очевидный способ реализовать перебор товаров - создадим геттер для поля items в классе Cart, после чего в методе makeOrder() с помощью цикла обойдем все элементы массива.
Подобное решение задачи является неправильным с точки зрения ООП:
нарушается принцип инкапсуляции, так как вы показываете внутреннюю структуру объекта. Сторонний класс может случайно или специально структуру и тогда объектом нельзя будет пользоваться;
класс Order теперь зависит от внутренней структуры класса Cart и, если мы каким-то образом изменим внутреннюю структуру класса (например, мы захотим вместо массива использовать какую-то коллекцию), или с помощью наследования модифицируем существующий класс - нам необходимо будет переписывать метод makeOrder();
мы устанавливаем сильную связь между логически разными классами, которые выполняют разные функции в приложении. Это нарушает принцип модульности.
Таким образом, нам необходимо решить следующую задачу: нам необходимо обойти все элементы "корзины" и добавить их в базу данных, но мы не должны показать внутреннюю структуру объекта типа Cart, класс Order должен работать успешно при любых изменениях внутри класса Cart.
Для решения этой задачи можно воспользоваться часто применяемым и очень простым паттерном проектирования, который называется "Итератор" (от относитс к категории паттернов поведения).
Краткое описание паттерна можно сформулировать так: итератор - это объект, который предоставляет интерфейс последовательного доступа к содержимому составных объектов, не раскрывая другим классам его внутренней структуры.
Давайте напишем свою версию этого паттерна. Прежде всего, нам необходимо описать интерфейс "Итератор", который будет описывать методы итератора.
Так как наш итератор может работать с разными элементами внутри составных объектов, то сделаем его обобщенным. Метод next() при каждом вызове возвращает следующий элемент составного объекта. То есть, если мы последовательно будем вызывать метод next(), то он будет возвращать первый, второй, третий и так далее элементы составного объекта.
Метод hasNext() возвращает нам сведения о том - есть ли еще эломенты, которые мы не обошли с помощью метода next(). Если впереди еще есть элементы - метод вернет true, если элементы закончились - метод вернет false.
Итак, мы описали интерфейс для итератора, теперь необходимо создать класс, который будет реализовывать интерфейс итератора.
Для этого необходимо создать так называемый внутренний класс.
В классе Cart создадим метод getIterator(), который будет создавать объект внутреннего класса CartIterator и возвращать его.
Теперь вернемся к классу Order. В методе makeOrder() получим итератор и проитерируем товары в корзине.
В результате:
класс Order не имеет доступа внутренней структуре объекта типа Cart;
класс Cart может спокойно изменить свою структуру - достаточно лишь переписать методы внутреннего класса CartIterator, чтобы реализовать методы итератора с учетом изменений структуры класса Cart;
класс Order будет продолжать нормальную работу, если внутренняя структура класса Cart изменится - главное, чтобы были корректно реализованы методы итератора;
вы можете предусмотреть несколько реализаций итератора и метод getIterator() может возвращать ту или иную реализацию интерфейса (например, вы можете предусмотреть несколько итераторов для обхода бинарного дерева - каждый итератор будет реализовывать свой способ обхода).
В реальности нам нет нужды свой интерфейс итератора - правильно будет воспользоваться стандартным интрефейсом java.util.Iterator и java.lang.Iterable. Это "стандартные" интерфейсы итератора, который все используют и в 99.9% случаях вы тоже должны его использовать.
Переделаем наш код для использования стандартных интерфейсов. Для начала перепишем внутренний класс CartIterator, чтобы он реализовывал интерфейс java.util.Iterator. Он мало чем отличается от нашего "самодельного" интерфейса Итератора. Минимально необходимо реализовать методы next() и hasNext(). Для полной реализации интерфейса необходимо дополнительно реализовать методы remove() и forEachRemaining().
Далее следует указать, что наш класс Cart может предоставить итератор для последовательного доступа к элементам составного объекта. Для этого укажем, что наш класс реализует интерфейс Iterable<Item>. Для минимальной реализации интерфейса Iterable необходимо реализовать метод Iterator<T> iterator(), который возвращает объект класса, реализующего интерфейс java.util.Iterator.
Таким образом, мы указали, что наш класс Cart может вернуть объект итератора для последовательного доступа к элементам внутренней структуры. Реализация интерфейса Iterable<T> часто бывает полезна: некоторые методы в стандартных библиотеках Java принимают на входы объекты интерфейсного типа Iterable<T>, кроме того, вы теперь можете обойти элементы внутри Cart с помощью цикла конструкции for-each:
Итераторы в коллекциях
Поддержка итераторов - необъемлемая часть работы с коллекциями. В интерфейсе Collection определен метод iterator(), который возвращает объект итератора. Таким образом, все коллекции, которые реализуют интерфейс Collection, имеют реализованный класс итератора.
Работа с итератором коллекции ничем не отличается от использования итератора в нашем примере:
запросить у Collection итератор посредством метода iterator(). Полученный итератор готов вернуть начальный элемент последовательности;
получить следующий элемент последовательности вызовом метода next();
проверить, есть ли еще объекты в последовательности (метод hasNext());
удалить из последовательности последний элемент, возвращаемый итератором, методом remove().
Також, коллекции, реализующие интерфейс List, имеют еще один итератор, который определен в интерфейсе java.util.ListIterator. Интерфейс ListIterator отличается от Iterator тем, что имеет дополнительные методы, а также позволяет двигаться по коллекции в обоих направлениях (Iterator позволяет двигаться только вперед).
Подробнее про ListIterator смотрите здесь
В версии Java 8 появился еще один тип итератора - Spliterator. Он позволяет разбивать коллекцию на части, что позволяет обрабатывать коллекции параллельно в несколько потоков. На данном этапе концепция сплитератора может показаться вам непонятной, поэтому оставим его за скобками.
Подробнее можете прочитать здесь.
Компараторы
Классы TreeSet и TreeMap сохраняют элементы в отсортированном порядке. Однако понятие "порядок сортировки" точно определяют применяемый ими компаратор. По умолчанию эти классы сохраняют элементы, используя то, что в Java называется естественным упорядочением, то есть ожидаемым упорядочением, когда после a следует b, после 1 следует 2 и так далее.
Создадим класс Person и попробуем поместить объекты этого класса в коллекцию. Запустим код и с удивлением обнаружим, что произошла ошибка при выполнении программы.
Почему произошла ошибка и что означает? Текст ошибки говорит о том, что тип Person не может быть преобразован в тип Comparable.
Происходит следующее - чтобы мы могли использовать коллекции типа TreeSet или TreeMap или чтобы можно было сортировать элементы в коллекции - мы должны написать код для сравнения объектов, а он сейчас отсутствует и коллекция TreeSet не может поместить элемент в дерево.
Для того, чтобы объекты класса Person можно было сравнить и сортировать, класс должен реализовать интерфейс Comparable<E>.
Для реализации интерфейса нам необходимо переопределить метод compareTo(). На вход метода подается второй объект для сравнения. Первым объектом служит текущий объект, у которого этот метод вызывается.
Если первый (текущий) объект меньше, то метод должен возвращать отрицательное число, если объекты равны - возвращается 0, если текущий объект больше, метод возвращает положительное число. Перепишем метод, чтобы было яснее
Добавим в класс Person метод toString() и напишем небольшую демонстрационную программу
Обратите внимание, что коллекция TreeSet отсортировала значения по возрастанию согласно той логике, чтобы мы прописали в методе compareTo(). Такой способ задания порядка сортировки удобен и прост, но он имеет недостатки:
что делать, если необходимо во время работы программы изменить порядок упорядочения объектов?
мы не можем изменить порядок сортировки объектов стандартных классов вроде класса String (от этих классов нельзя наследоваться);
мы не можем изменить порядок сортировки уже кем-то написанных классов (наследоваться от существующего класса с целью переопределить метод compareTo() - не лучшая идея).
Для реализации компаратора, нам, в самом простом случае, необходимо переопределить метод compareTo(), который принимает на вход два объекта класса Person. Логика работы метода не отличается от метода compareTo() - если первый объект меньше - метод возвращает отрицательное число, если первый объект больше - метод возвращает положительное число, если объекты равны - метод возвращает 0.
В данном случае наш компаратор сравнивает объекты по полю lastName. Объект класса компаратора передается коллекции TreeSet при создании объекта коллекции. Напишем небольшую демонстрационную программу.
Мы видим, что элементы были упорядочены по полю lastName. Изменим коллекцию на List и отсортируем список с помощью компаратора
Можно заметить, что в первом случае порядок элементов в списке соответствует порядку добавления этих элементов. После сортировки порядок элементов был изменен -мы отсортировали элементы в списке по полю lastName. Вместо отдельных классов, для реализации компаратора можно использовать соответствующие лямбда-выражения.
Потоки данных. Использование Stream API.
Главной особенностью потокового API является способность выполнять очень сложные операции поиска, фильтрации и преобразования и иного манипулирования данными. Используя потоковый API, можно, например, сформировать последовательность действий, подобных запросам базы данных на языке SQL. Кроме этого, работа потокового API выполняется параллельно, а, следовательно, повышается производительность, особенно при обработке крупных потоков данных.
Потоки данных позволяют выполнять вычисления на более высоком концептуальном уровне, чем коллекции. С помощью потока данных можно указать, что и как именно требуется сделать с данными, а планирование операций предоставить конкретной реализации.
В Stream API определен ряд потоковых интерфейсов, входящих в состав пакета java.util.stream. В основе их иерархии лежит интерфейс BaseStream.
Производными от интерфейса BaseStream являются несколько типов интерфейсов. Наиболее употребительными из них является интерфейс Stream.
Поток данных - канал передачи данных, последовательность объектов. В самом потоке данные не хранятся, а только перемещаются и, возможно, фильтруются, сортируются или обрабатываются иным образом в ходе этого процесса. Как правило, действие потока данных не видоизменяет их источник.
Рассмотрим пример. Допустим, что требуетя подсчитать все длинные слова в книге. Сначала считаем файл и выделим отдельные слова в коллекцию:
Теперь попробуем посчитать все слова, длина которых больше 8 символов:
При использовании потокового API, подсчет осуществляется следующим образом:
Достаточно заменить метод stream() на метод parallelStream(), чтобы организовать параллельное выполнение операций фильтрации и подсчета слов:
Потоки данных действуют по принципу "что делать", а не "как это сделать". В рассматриваемом примере мы описываем, что нужно сделать: получить длинные слова и подсчитать их. При этом, мы не указываем, в каком порядке или потоке исполнения это должно произойти.
Потоки данных имеют свои особенности:
поток данных не сохраняет свои элементы. Они могут сохраняться в основной коллекции или формироваться по требованию;
потоковые операции не изменяют их источник. Например, метод filter() не удаляет элементы из нового потока данных, а выдает новый поток, в котором они отсутствуют;
потоковые операции выполняются по требованию (так называемая "ленивая инициализация", lazy initialization). Это означает, что они не выполняются до тех пор, пока не потребуется результат.
Вернемся к примеру. Методы stream() и parallelStream() выдают поток данных для списка слов words. А метод filter() возвращает другой поток данных, содержащий только те слова, длина которых больше 8 символов. И наконец, метод count() сводит этот поток данных в конечный результат.
Такая последовательность операций весьма характерна для обращения с поткоками данных. Конвейер операций организуется в следующие три стадии:
создание потока данных (метод stream());
указание промежуточных операций (intermediate operations) для преобразования исходного потока данных в другие потоки, возможно, в несколько этапов (метод filter());
выполнение конечной операции (terminal operation) для получения результата (метод count());
Конечная операция потребляет поток данных и дает конечный результат, например, находит минимальное значение в потоке данных или выполняет некоторое действие, как это делает метод forEach(). Если поток данных потреблен, он не может быть использован повторно.
Промежуточная операция производит поток данных и служит для создания конвейера для выполнения последовательности действий.
Создание потока данных
Получить поток данных можно самыми разными способами. Вероятно, самый распространенный способ - получение потока данных из коллекции. Любую коллекцию можно преобразовать в поток данных методом stream() из интерфейса Collection.
Поток данных можно получить и из массива. Это можно сделать несколькими способами, например, с помощью класса Arrays и с помощью класса Stream.
Вы также можете сгенировать поток с помощью метода generate() или iterate(). Обратите внимание, что метод generate() возвращает бесконечный поток, поэтому его бывает полезно ограничить с помощью промежуточной операции limit()
Также следует помнить, что в библиотеках Java существует много методов, которые могут вернуть потоки данных. Например, можно получить поток строк из класса Pattern или поток строк из файла с помощью метода класса Files
Промежуточные операции
В результате выполнения промежуточных операций осуществляется преобразование потока данных в другой поток, элементы которого являются производными от элементов исходного потока.
На примерах выше мы рассматривали несколько таких операций, например, метод filter(), в результате которого получается новый поток данных с элементами, удовлетворяющими определенному условию. Рассмотрим еще один пример с использованием операции filter():
Нередко значения в потоке данных требуются каким-то образом преобразовать. Для этой цели можно воспользоваться методом map(), передав ему функцию, которая и выполняет нужное преобразование. Например
При вызове метода map() передаваемая ему функция применяется к каждому элементу потока данных, в результате чего образуется новый поток данных с полученными результатами.
Следующая операция - операция limit() возвращает поток данных, оканчивающийся после n элементов или по завершению исходного потока данных, если тот короче. Данный метод особенно удобен для ограничения бесконечных потоков данных до определенной длины. Так, в следующей строке кода получается новый поток данных, состоящий из 100 произвольных целых чисел
Метод skip() выполняет противоположную операцию - отбрасывает первые n элементов
Статический метод concat() позволяет соединить два потока данных. Первый из этих потоков не должен быть бесконечным
Метод distinct() возвращает поток данных, в котором исключены дубликаты.
Для сортирвоки потоков данных имеется несколько вариантов метода sorted(). Один из них служит для обработки потоков данных, состоящих из элементов типа Comparable, а другой принимает в качестве параметра компаратор типа Comparator
Как и во всех остальных потоковых преобразованиях, метод sorted() выдает новый поток данных, элементы которого происходят из исходного потока и располагаются в отсортированном порядке. Метод sorted() удобно применять в том случае, если процесс сортировки является частью поточного конвейера.
Метод peek() выдает другой поток данных с теми же элементами, что и у исходного потока, но при добавлении эелмента в новый поток вызывается лямбда-выражение, указанное в качестве аргумента метода peek().
Отличие метода peek() от метода forEach() является то, что метод метод peek() является промежуточной операцией и используется в целях отладки, а операций forEach() являетя конечной операцией, которая запускает конвейер и используется для получения результата работы потока.
Конечные операции
Конечные операции часто называют методами сведения. Они выполняют конечные операции, сводя поток данных к непотоковому значению, которое может быть далее использовано в программе. С некоторыми из них вы уже знакомы: count(), forEach() и collect().
Метод count(), как уже было сказано, возвращает количество элементов в результирующем потоке. Приведем еще один пример
К числу других простых методов сведения относятся методы max() и min(), возвращающие наибольшее и наименьшее значения соответственно.
Обратите внимание, что эти методы возвращают значение типа Optional<T>, которое заключает в себе ответ на запрос или обозначают, что запрашиваемые данные отсутствуют, поскольку поток оказался пустым. Раньше в подобных случаях возвращалось пустое значение null. Но это могло привести к исключениям в связи с пустыми указателями в не полностью протестированной системе. Тип Optional удобнее для обозначения остутствующего возвращаемого значения.
Метод findFirst() возвращает первое значение из непустой коллекции. Зачастую он применяется вместе с методом filter().
Если же потребуется любое совпадение, а не только первое, то следует воспользоваться методом findAny(). Это оказывается эффективным при распараллеливании потока данных, поскольку поток может известить о любом обнаруженном в нем совпадении, вместо того чтобы ограничиваться только первым совпадением.
Если же потребуется только выяснить, имеется ли вообще совпадение, то следует воспользоваться методом anyMatch()
Имеются также методы allMatch() и noneMatch(), возвращающие логическое значение true, если с предикатом совпадают все элементы в потоке данных или не совпадает ни один из его элементов соответственно. Эти методы также выгодно выполнять в параллельном режиме.
По завершении обработки потока данных нередко требуется просмотреть полученные результаты. С этой целью можно использовать метод forEach(), чтобы применить функцию к каждому элементу.
В параллельном потоке данных метод forEach() выполняет обход элементов в произвольном порядке. Если же требуется обработать данные в потоковом порядке, то следует вызвать метод forEachOrdered(), что может снизить производительность вычислений.
Но чаще всего требуется накапливать результаты в структуре данных (то есть, создать массив или коллекцию, которая будет содержать элементы итогового потока).
Если нужно получить результирующий массив, то можно воспользоваться методом toArray(). Создать обобщенный массив выполнения невозможно, и поэтому в результате вызова stream.toArray() возвращается массив типа Object[]. Если же требуется массив нужного типа, то этому методу следует передать конструктор такого массива.
Для накопления элементов потока данных с другой целью имеется метод collect, принимающий экземпляр класса, реализующего интерфейс Collector. В частности, класс Collectors предоставляет немало фабричных методов для наиболее полезных коллекторов. Для накопления потока данных в списке или множестве достаточно сделать один из следующих вызовов
Если же требуется конкретная разновидность получаемого множества, то нужно сделать следующий вызов:
Для накопления символьных строк, сцепляя их, используется следующие вызовы
Если результаты обработки потока данных требуется свести к сумме, среднему, максимуму или минимуму, можно воспользоваться методами типа summarizing()
Для накопления результатов в словарях используется метод Collectors.toMap() принимающий в качестве двух своих аргументов функции, чтобы получить ключи и значения их отображения. Процесс накопления данных в словарях имеет много нюансов, в рамках данного курса рассматриваются только простые случаи.
Механизм перечислений
Механизм перечислений был добавлен в JDK версии 5 и позволяет удобно хранить и работать с так называемыми "категориальными данными".
Категориальные данные - это данные с ограниченным числом унекальных значений или категорий. Примеры категориальных данных:
месяц год (12 значений: январь, февраль и так далее);
вероисповедание (православие, католицизм, ислам и так далее);
день недели (понедельник, вторник, среда и так далее).
Рассмотрим пример. Для информационной системы "Электронный институт" необходимо создать класс сотрудника кафедры DepartmentMember, в котором, среди прочих атрибутов, есть атрибут position (должность). Предположим, что сотрудники кафедры могут занимать одну из следующих должностей:
Engineer;
Assistant;
Lecturer;
Senior Lecturer;
Professor.
Каким образом мы можем представить эти данные в компьютерной программе? Самый очевидный вариант - использовать строки.
Данное решение имеет несколько очень серьезных недостатков. Вы никак не можете обеспечить правильность указания должностей и контролировать создание нового объекта
Например, должность старшего преподавателя может быть записана как "ст. преп., ст. преподаватель, ст. пр." и так далее, не говоря про возможные орфографические ошибки. С точки зрения формальной логики, это все разные категории должностей и вы не сможете группировать или фильтровать сотрудников кафедры по полю "должность".
Второй вариант - создать несколько статических целочисленных констант, которые можно использовать вместо строк.
Теперь мы сможем использовать именованные константы вместо строго фиксированных строк.
Использование именованных констант позволяет отчасти избавиться от недостатков предыдущего подхода, но и этот вариант не является приемлемым. Ничто не мешает клиентскому коду при создании объекта указать целое число вместо именованной константы, осуществлять проверку ввода - достаточно трудоемкая задача, если в классе предусмотрено более одного категориального значения, будет тяжело определять, какие конкретно именованные константы необходимо использовать. К тому же, при использовании конструктора или сеттера поля position, нет никакого указания на необходимость использования именованных констант, программист сам должен догадаться о наличии нужных констант или читать сопроводительную документацию.
Подход с использованием именованных констант напоминает улучшенную версию использования механизма специальных кодов и его не следует использовать при разработке коммерческих программ.
Корректным вариантом решения данной проблемы является использование специального механизма перечислений (enumeration). По сути, перечисление - это тип (то есть класс), ссылочная переменная которого может принимать одно из нескольких заранее определенных значений. Реализуем класс DepartmentMember с помощью механизма перечислений
Преимущества такого подхода очевидны: поле position может принять только одно из заранее определенных значений, которые указаны при создании перечислений. При вызове конструктора или сеттера поля position, пользователю класса сразу будет понятно, что необходимо передать в качестве аргумента метода.
Обратите внимание на синтаксис перечисления. Перечисление является классом, но очень своеобразным. Вместо ключевого слова class используется ключевое слово enum (от слова enumeration - перечисление). В самом простом варианте, в перечислении просто указывается список констант, через запятую.
Как уже было сказано, перечисление - это класс, а это значит, что он может иметь методы, и мы можем инкапсулировать перечисление и операции работы с ним в одной оболочке.
В данном примере, экземпляр перечисления хранит строку с цветом.
Лекция 13
Тема: Принципы разработки GUI. Библиотека JavaFX. Создание базового приложения с помощью JavaFX. События, обработка событий. Менеджеры компоновки.
Для корректной работы JavaFX рекомендуется установить 8, 9 или 10 версию JDK.
Отличие программы с графическим интерфейсом от консольной программы заключается не столько в способе подачи информации (использование графических объектов против текстового ввода и вывода информации), сколько в порядке выполнения (control flow) программного кода.
Консольная программа начинает свою работу с точки входа (метод main()), выполняет все инструкции, указанные в этом методе (включая создание объектов классов и вызовы методы этих объектов), после чего завершает свою работу. Пользователь может вводить значения в консоль, но в строго определенный программистом момент и программа не будет работать до тех пор, пока пользователь не осуществит ввод.
Такой порядок работы программы удобен, если рассматривать программу как математическую функцию или как решение какой-то математической задачи. Программа выполняет набор действий, а после того, как все действия выполнены, программы завершает свою работу.
Программа, использующая графический интерфейс пользователя (graphic user interface или GUI), работает иначе. Работа GUI-программы основана на событиях (event-driven; разработку программы с графическим интерфейсом иногда называют event-driver programming).
В данном случае событие - это изменение состояния объекта, которое может быть зафиксировано и обработано программой. Если говорить проще, событие - это:
взаимодействие пользователя с компонентами графического интерфейса;
выполнение некоторыми объектами определенного условия (например, сработал таймер, была выполнена некоторая операция);
сообщения от операционной система (например, прерывания операционной системы, сбой аппаратного или программного обеспечения и так далее).
Пока что будем считать, что события - это результат взаимодействия пользователя с компонентами графического интерфейса. Пользователь нажал на кнопку, передвинул курсор мыши, ввел символ из клавиатуры, выбрал пункт списка, осуществил скроллинг страницы - все эти действия порождают события (events).
Среда Java (JVM) содержит механизмы, которые позволяют приложению реагировать на события - выполнять некоторые действия в ответ на нажатие кнопки, движение курсора, ввода символа из клавиатуры и так далее.
Каким образом приложение реагирует на события? Реакция на событие – это выполнение некоторого кода. Так как исполняемый код в ОО-языках хранится в методах, то реакция на событие, по сути, сводится к вызову определенного метода, который что-то делает. Таким образом, когда пользователь, например, нажимает на кнопку, то программа реагирует путем выполнения некоторого кода.
В Java существует огромное количество различных типов событий – даже самое незначительное действие может служить источником одного, а иногда и целой серии событий.
Важно понять, что разработчик не обязан создавать методы для реакции на все возможные события, которые теоретически могут произойти в программе. Программист должен определить, на какие события программа будет реагировать, и какая будет реакция, а какие события программа будет игнорировать
Схему работы GUI-приложения можно представить следующей схемой. Это очень упрощенная схема, но она дает понимание того, как работает GUI-приложение.
Вы можете обратить внимание на то, что вышеприведенная схема работы GUI-приложения напоминает цикл. На самом деле, это так и есть. Работу приложения можно, очень условно, разделить на три этапа:
Подготовительный этап. На этом этапе программа создает необходимые объекты графического интерфейса (но пока их не показывает), устанавливает связи между объектами интерфейса и выполняет другую подготовительную работу. На этом этапе приложение не обрабатывает события и не взаимодействует с пользователем;
Основной этап работы приложения. Приложение входит в бесконечный цикл (он называется event loop), в ходе которого, приложение на каждой итерации цикла проверяет – произошли ли какие-то события. Если есть произошедшие события – оно отправляет первое в очереди событие на обработку, на следующей итерации – следующее за ним в очереди событие, и так далее. Если нет произошедших событий – приложение просто пропускает эту итерацию;
Завершение работы приложения. Приложение «крутится» в event loop до тех пор, пока не будет подана команда на закрытие приложения (пользователь выбрал пункт «Выйти из приложения», закрыл приложение из диспетчера задач, операционная система принудительно «убила» приложение и так далее). При поступлении команды на закрытие, приложение выходит из бесконечного цикла, после чего, оно может выполнить некоторые заключительные действия, после чего закрывается.
Основные библиотеки для разработки GUI на языке Java
Так как, согласно парадигме Java, всё является объектом, то и графический интерфейс, по сути, является набором объектов различных классов (кнопка, поле ввода, окно приложения – всё это является объектами соответствующих классов), который объединены в библиотеки графического интерфейса.
В Java нет никаких ограничений на реализацию своей библиотеки графического интерфейса – вы можете написать свою библиотеку, которая будет реализовывать свои классы компонентов графического интерфейса. Таким образом, наряду с библиотеками, которые были предложены разработчиками языка Java (AWT, Swing, JavaFX), существует множество библиотек, которые были разработаны сторонними фирмами или просто отдельными сообществами программистов (SWT, Apache Pivot и другие). Рассмотрим библиотеки, которые были предложены разработчиками языка Java.
AWT
AWT (Abstract Window Toolkit) – первая предложенная разработчиками библиотека GUI для Java. Главным недостатком данной библиотеки было то, что она использовала нативные компоненты ОС. Таким образом, внешний вид приложения, запущенного под ОС Window мог сильно отличаться от внешнего вида того же приложения, запущенного под OC Linux. На данный момент, эта библиотека считается устаревшей и не используется.
Swing
Swing – наиболее известная и вторая по счету библиотека GUI для Java. Обратите свое внимание на тот факт, что библиотека Swing активно использует компоненты AWT, наследуясь от них. Таким образом, хотя AWT и не используется напрямую, но Swing, во многом, опирается на библиотеку AWT.
Библиотека Swing является надежным проверенным средством для разработки GUI, а также имеет множество сторонних компонентов и классов, которые помогут вам в разработке. Недостатком Swing является то, что эта библиотека предназначена только для desktop-приложений и морально устарела, т.к. не поддерживает современные инструменты разработки графических интерфейсов (привязка данных, использование CSS и XML для описания интерфейса и так далее).
Разработчики языка Java, хотя и поддерживают библиотеку Swing, но настоятельно рекомендуют использовать библиотеку JavaFX.
JavaFX
Данная библиотека обладает всеми преимуществами Swing и, дополнительно предлагает более рациональный, простой в употреблении и усовершенствованный подход к построению GUI, а также значительно упрощает воспроизведение объектов благодаря автоматической перерисовке.
Создание базового приложения с помощью JavaFX
Стартовый класс JavaFX-приложения должен расширять класс Application, который входит в состав пакета javafx.application
Компоненты JavaFX содержится в отдельных пакетах, имена которых начинаются с префикса javafx (например, javafx.application, javafx.stage, javafx.scene и так далее).
Точкой входа для JavaFX-приложений является базовый класс Application. Точка входа программиста вызывает метод класса Application, который запускает JavaFX-приложение.
Main.java
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void init() throws Exception {
super.init();
}
@Override
public void start(Stage primaryStage) throws Exception {
}
@Override
public void stop() throws Exception {
super.stop();
}
}
В классе Application важными для нас являются три метода (они называются методами жизненного цикла, т.к. они вызываются системой в определенный момент «жизни» приложения):
метод init() вызывается в момент, когда приложение только начинает выполняться. Он служит для выполнения различных инициализаций. Если инициализация не требуется – просто не переопределяйте данный метод;
метод start() вызывается после init(). Этот метод в классе Application является абстрактным, поэтому нам необходимо его переопределить. Именно с него начинается работа приложения. Здесь создаются и настраиваются компоненты графического интерфейса;
метод stop() вызывается, когда приложение завершается. Именно в нем должны быть произведены все операции очистки или закрытия. Если это не требуется – просто не переопределяем метод.
Когда запускается приложение, JavaFX выполняет следующие действия:
создает объект класса, который наследуется от класса Application;
вызывает метод init();
вызывает метод start();
ждет завершения приложения. Приложение может завершиться в следующих случаях:
приложение вызвало метод Platform.exit();
закрыто последнее окно и атрибут implicitExit класса Platform равен true;
вызывает метод stop().
Запуск JavaFX-приложения
Для того чтобы запустить JavaFX-приложение на выполнение, следует вызвать статический метод launch(), определяемый в классе Application.
Вызов метода launch() приводит к построению приложения и последующему вызову методов init() и start(). Возврат из метода launch() не происходит до тех пор, пока приложение не завершится.
Классы Stage и Scene
При выборе терминов для JavaFX, разработчики пользовались театральной терминологией и представляли работу графического интерфейса как театр: в театре есть одна или несколько театральных сцен (окон). На театральной сцене, в течение спектакля (работы программы) сменяют друг друга сцены из спектакля (контейнер), на которых расположены различные декорации (элементы графического интерфейса).
Центральным понятием в JavaFX является Stage (во многих книгах этот переводится как «подмостки», имеется ввиду «театральные подмостки»). Класс Stage является нашей театральной сценой, подмостками, основным контейнером, который, как правило, представляет собой обрамлённое окно со стандартными кнопками: закрыть, свернуть, развернуть. Внутри Stage содержится сцена – объект класса Scene, которая может быть заменена другим объектом класса Scene. Объект класса Stage может содержать одновременно только один объект класса Scene.
Объект класса Scene содержит в один элемент – специальную вершину, которая называется корневая вершина (root node). Это самый верхний и единственный узел графа, не имеющий родителя. Все узлы в сцене прямо или косвенно происходят от корневого узла. В качестве корневого узла может выступать любой элемент графического интерфейса, который наследуется от абстрактного класса Parent.
Корневой узел включает в себя множество различных графических компонент – кнопок, полей, переключателей, надписей – всё то, что нам нужно для создания графического интерфейса. На первом этапе думайте о Stage просто как об окне приложения, а о Scene – как о полотне, на котором размещаются нужные вам элементы. Все элементы графического интерфейса будут добавляться в корневую вершину.
В рамках одного окна вы можете менять объекты Scene и, таким образом, менять содержимое окна. Вы также можете создать другие объекты Stage – когда вам нужно открыть другие окна в приложении. Стартовый объект Stage (аргумент primaryStage) создает за вас JavaFX и передает вам как аргумент метода start().
Main.java
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void init() throws Exception {
super.init();
}
@Override
public void start(Stage primaryStage) throws Exception {
// Устанавливаем заголовок окна
primaryStage.setTitle("JavaFX приложение");
// Создаем root node
FlowPane rootNode = new FlowPane();
// Создаем новую сцену и добавляем туда корневую вершину
Scene scene = new Scene(rootNode, 300, 200);
// Добавляем сцену в primaryStage
primaryStage.setScene(scene);
// Метод show() делает окно видимым на экране
primaryStage.show();
}
@Override
public void stop() throws Exception {
super.stop();
}
}
Узлы и графы сцены
Отдельные элементы сцены называются узлами. Например, кнопка, поле ввода, надпись и так далее. Некоторые узлы могут содержать в себе другие узлы (например, группа радиокнопок). Совокупность всех узлов сцены называется графом сцены, который образует дерево. Базовым классом для всех узлов служит класс Node. От Node, прямо или косвенно, происходят другие классы, например Parent, Group, Region и Control.
Если узел может иметь потомков, тогда он называется узлом ветвления (branch node), если у узла не может быть потомков, он называется листом (leaf node). Узлы ветвления наследуются от класса Parent.
Рассмотрим небольшой пример. Построим графический интерфейс в виде панели с тремя надписями.
В качестве панели будет использоваться компонент HBox. Этот компонент является менеджером компоновки (Layout Manager) – специальным контейнером, в который можно помещать элементы, которые будут расположены в определенном порядке.
Компонент HBox размещает содержащиеся в нем элементы в виде горизонтального ряда. Такой компонент, очевидно, является узлом ветвления, так как он может иметь детей. Текстовая надпись (класс Text), очевидно, является листом, т.к. вы не можете ничего поместить в текстовую надпись.
Граф сцены для вышеприведенного приложения будет следующим:
Код для такого приложения будет выглядеть следующим образом:
Main.java
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void init() throws Exception {
super.init();
}
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX app");
// Контейнер для текстовых надписей
HBox rootNode = new HBox();
// Расстояние между элементами
rootNode.setSpacing(40);
// Padding корневой вершины
rootNode.setPadding(new Insets(40));
// Объекты текстовых надписей
Text text1 = new Text("Текст1");
Text text2 = new Text("Текст2");
Text text3 = new Text("Текст3");
// Добавление текстовых надписей в корневую вершину
rootNode.getChildren().addAll(text1, text2, text3);
Scene scene = new Scene(rootNode);
primaryStage.setScene(scene);
primaryStage.show();
}
@Override
public void stop() throws Exception {
super.stop();
}
}
Менеджеры компоновки
При разработке GUI можно часто столкнуться с проблемой: как сделать так, чтобы при изменении размера окна, расположенные в нем компоненты не смешивались в кучу, а вели себя определенным образом. Необходимо, чтобы элементы либо пропорционально уменьшались или увеличивались в размерах, либо оставались того же размера, появлялись полосы скроллинга, компоненты как-нибудь перемещались по экрану и так далее.
Для того, чтобы самостоятельно не заниматься отслеживанием таких ситуаций, во многих библиотеках графического интерфейса используются специальные компоненты, которые называются менеджерами компоновки (LayoutManager).
Менеджеры компоновки определяют размер и расположение компонентов, а так же, при изменении размера окна пропорционально масштабируют компоненты формы.
При разработке графического интерфейса, необходимо определить – какие менеджеры компоновки подойдут для реализации задуманного интерфейса, после чего расположить их в правильном порядке и добавить в них необходимые элементы.
BorderPane
Класс располагает свои дочерние узлы в одном из пяти регионов: top, bottom, left, right, center. Эти регионы могут быть любых размеров. Если не поместить ни одного элемента в какой-либо регион, для него не будет выделено пространство.
Менеджер BorderPane используется, когда необходимо достичь классического вида приложения – с панелью инструментов вверху, статусной строкой внизу, навигационной панелью слева, дополнительной информацией справа и рабочей областью в центре. Например:
Рассмотрим небольшой пример
HBox
Менеджер разметки HBox позволяет легко упорядочить элементы в горизонтальный ряд. С помощью метода setPadding() можно установить расстояние между узлами и краями HBox. С помощью метода setSpacing() можно регулировать расстояние между узлами. Менеджер поддерживает установку стиля.
VBox
Менеджер разметки VBox работает схожим с HBox образом, за исключением того, что узлы упорядочены в вертикальный ряд.
StackPane
Данный менеджер разметки размещает узлы в один стек, один на другой. Новый добавленный узел будет расположен поверх старого. Данный менеджер полезен, когда необходимо, например, расположить текст поверх фигуры и изображения или для создания комплексных фигур.
GridPane
Данный менеджер разметки позволяет создавать гибкую сеть из рядов и колонок, в которой располагаются узлы. Узел может быть помещен в любую ячейку сетки и объединять ячейки, если нужно. Этот менеджер полезен для создания форм или любой разметки, которая может быть организована рядами и колонками.
Обратите внимание на то, как в разметке GridPane задаются свойства отдельных колонок и рядов: создается объект класса ColumnConstraints и RowConstrains соответственно, в объектах устанавливаются нужные свойства, после чего этот объект передается GridPane (также, это можно сделать, указав индекс колонки или ряда).
FlowPane
FlowPane – класс поточной компоновки. Элементы в этой компоновке располагаются построчно с автоматическим переходом на новую строку, если требуется.
Рассмотрим пример:
Попробуем изменять размер окна и увидим, что если кнопки не помещаются в окно по ширине, то кнопки автоматически переносятся на следующую строку.
По умолчанию, поточная компоновка выполняется по горизонтали, хотя можно указать и поточную компоновку по вертикали. Имеется возможность указать и другие свойства компоновки, в том числе промежутки между элементами по горизонтали и по вертикали, а также выравнивание.
TilePane
Работа этого менеджера компоновки схожа с работой FlowPane, отличие в том, что дочерние узлы помещаются в сетку, ячейки которого имеют одинаковый размер.
Реализуем предыдущий пример, но сделаем кнопку 3 больше остальных. Обратите внимание на отличия между работой FlowPane и TilePane
Обратите внимание, что TilePane, исходя из размеров самого большого узла, высчитывает размер ячейки, и каждый элемент помещает в эти ячейки одинакового размера.
AnchorPane
Разметка AnchorPane позволяет привязывать элементы к верхнему, нижнему, правому, левому краям контейнера. При изменении размеров окна, узлы поддерживают позицию относительно привязки. Узлы могут быть привязаны к нескольким позициям, и к одной позиции может быть привязано более одного узла
Group
Особняком в списке классов-контейнеров стоит класс Group. Элементы в Group не привязываются к местоположению, их позиция задается в абсолютных значениях. Если не задано другое, все дочерние узлы группы позиционируются по координате (0,0) (верхний левый угол).
Компонент Group часто используют для того, чтобы применить различные эффекты и преобразования к целому набору компонентов, которые находятся в группе. Размеры компонента Group определяются размерами его потомков.
Попробуем добавить пару кнопок в группу
@Override
public void start(Stage primaryStage) throws Exception {
// Заголовок окна
primaryStage.setTitle("JavaFX приложение");
Group root_node = new Group();
Button button1 = new Button("Кнопка 1");
button1.setPrefHeight(90);
button1.setPrefWidth(160);
Button button2 = new Button("Кнопка 2");
root_node.getChildren().addAll(button1, button2);
Scene scene = new Scene(root_node);
primaryStage.setScene(scene);
primaryStage.show();
}
На этом примере мы видим, что две кнопки наложены друг на друга, т.к. обе находятся в верхнем левом углу без всякой компоновки. Если мы попробуем изменить размеры окна, то увидим, что кнопки остаются на своем месте и разметка никак не реагирует на изменение размеров
Попробуем установить для кнопок позицию в группе. Мы видим, что кнопки расположились согласно координатам.
@Override
public void start(Stage primaryStage) throws Exception {
// Заголовок окна
primaryStage.setTitle("JavaFX приложение");
Group root_node = new Group();
Button button1 = new Button("Кнопка 1");
button1.setLayoutX(20);
button1.setLayoutY(20);
Button button2 = new Button("Кнопка 2");
button2.setLayoutX(70);
button2.setLayoutY(75);
root_node.getChildren().addAll(button1, button2);
Scene scene = new Scene(root_node);
primaryStage.setScene(scene);
primaryStage.show();
}
Полезным свойством Group является возможность устанавливать эффекты и преобразований для всей группы. На примере ниже мы устанавливаем эффект BoxBlur и этот эффект автоматически применяется ко всем узлам внутри группы.
@Override
public void start(Stage primaryStage) throws Exception {
// Заголовок окна
primaryStage.setTitle("JavaFX приложение");
Group root_node = new Group();
Button button1 = new Button("Кнопка 1");
button1.setLayoutX(20);
button1.setLayoutY(20);
Button button2 = new Button("Кнопка 2");
button2.setLayoutX(70);
button2.setLayoutY(75);
root_node.getChildren().addAll(button1, button2);
// **** Наложение эффекта
root_node.setEffect(new BoxBlur(5,5,1));
Scene scene = new Scene(root_node);
primaryStage.setScene(scene);
primaryStage.show();
}
Создание меню приложения
За создание меню в JavaFX отвечают несколько классов:
MenuBar – панель меню;
Menu – раздел меню (Главная, Вставка и т.д.);
MenuItem – действие в пункте меню;
RadioButtonItem – для создания пункта меню в виде RadioButton (выбор одного из нескольких вариантов);
CheckMenuItem – создание пункта меню в виде CheckBox (выделение\снятие выделения);
SeparatorMenuItem - для отделения одной категории от другой.
В JavaFX нет ограничений на расположение панели меню, однако, как правило, панель меню располагается вверху окна. Панель меню добавляется как обычный узел, наряду с другими компонентами графического интерфейса. Панель меню содержит один или несколько разделов. Каждый раздел в панели выглядит как кнопка с текстовым значением. Рассмотрим небольшой пример
При нажатии на раздел меню ничего не происходит, т.к. мы добавили пункты меню. Давайте добавим разнообразные пункты меню в раздел «Файл» и посмотрим на результат
Пример разработки графического интерфейса
В качестве примера воспроизведем интерфейс приложения «Калькулятор» для Windows 7.
Для начала, нам необходимо понять – какие менеджеры компоновки нам следует использовать. При разработке GUI желательно использовать как можно меньшую вложенность менеджеров разметок и как можно меньший граф сцены.
Для начала, мы можем заметить, что калькулятор состоит из трех вертикальный областей: меню, поле вывода результата и кнопки калькулятора.
Следовательно, здесь уместным будет использование менеджера разметки VBox, который размещает элементы в вертикальный список. Элемент класса VBox будет нашим корневым узлом графа (root node).
Создадим метод configureMenu(), в котором мы будем иницализировать и настраивать объекты меню.
Разделы меню мы заполнять не будем, а пока что попробуем воспроизвести окно результата. Создадим метод configureResultView(), в котором мы будем создавать и настраивать объекты для реализации окна показа результата.
Текстовое поле (класс Text) само по себе имеет не очень много возможностей для настройки выравнивания и внешнего вида. Поэтому, чтобы добиться сходства с оригинальным калькулятором, поместим текстовое поле в HBox, а уже HBox поместим как второй дочерний узел для VBox.
Теперь перейдем к третьему элементу VBox, в котором будут содержаться кнопки. Для того, чтобы выбрать нужную разметку, давайте внимательно посмотрим на область с кнопками.
Нетрудно догадаться, что это разметка типа Grid – то есть, сетка с набором кнопок. Две кнопки («=» и «0» занимают две строчки и два столбца соответственно). Таким образом, используем менеджер GridPane. Создадим метод configureButtons(), в котором будем создавать кнопки и помещать их в соответствующие ячейки сетки.
Для начала необходимо настроить кнопки. По умолчанию, размер кнопки определяется ее содержимым. Нам же необходимо будет, чтобы кнопка могла растягиваться в ширину и высоту, чтобы растягиваться в нашей сетке. Лучшим вариантом будет – создать собственный класс, который наследуется от стандартной кнопки и в конструкторе собственного класса вызвать метод setMaxSize()
Теперь наша кнопка будет растягиваться в случае необходимости. Теперь реализуем метод. Безусловно, добавление можно было бы реализовать более компактно, однако, в качестве примера, реализуем добавление каждой кнопки отдельно.
Далее, создадим объект класса ColumnConstraints, после чего передадим объект этого классов для каждой колонки нашей таблицы. В этом объекте мы указываем, что колонка должна максимально расширяться. Также, установить отступы и зазоры между ячейками таблицы.
Обработка событий играет важную роль, поскольку большинство элементов управления GUI генерируют события, которые обрабатываются программой. Когда вы используете кнопки, флажки или списки, все они генерируют события.
Базовым классом событий JavaFX является класс Event, находящийся в пакете javafx.event. Класс Event наследует класс java.util.eventObject, а это значит, что события JavaFX разделяют общую функциональность с другими событиями Java. Для класса Event определено несколько подклассов, каждый из которых отвечает за свой тип событий.
JavaFX различает много видов событий, среди которых, например, DragEvent, KeyEvent, MouseEvent, ScrollEvent и другие.
Чтобы среагировать на нажатие кнопки, вам необходимо написать код для обработки этого события. В данном случае, кнопка называется источник события (event source object) – объект, где произошло событие.
Само по себе событие является объектом соответствующего класса (наследуемого от javafx.event) и называется объектом события (event object). Чтобы обработать событие, необходимо создать объект, который может обработать это событие. Такой объект называется обработчик событий (event handler).
Не все объекты могут быть обработчиками события. Чтобы быть обработчиком action event, необходимо выполнить два требования:
объект должен быть экземпляром класса, который реализует интерфейс EventHandler<T extends Event>. Этот интерфейс определяет общее поведение для всех обработчиков. Как мы помним, <T extends Event> означает, что T – обобщенный тип, который является классом или подклассом Event;
обработчик события должен быть зарегистрирован источником события с помощью метода setOnAction().
Интерфейс EventHandler<T extends Event> содержит метод handle(T event) для обработки события. Ваш обработчик должен переопределить метод, чтобы среагировать на событие. Рассмотрим небольшой пример
На данный момент вы уже должны понимать содержимое метода start(). Если мы нажмем на одну из кнопок, то ничего не произойдет, т.к. по умолчанию, никакой реакции на нажатие не предусмотрено – реакцию должны прописать мы.
Для этого мы должны создать объект класса, который реализует интерфейс EventHandler<T extends Event> и передать этот объект кнопке. Передать кнопке объект обработчика событий мы можем двумя способами:
использовать метод addEventHandler(), которому нужно передать тип события, а также объект обработчика события;
использовать один из множества методов setOn***(), каждый из которых позволяет установить обработчик для того или иного события.
Событие нажатия на кнопку относится к типу ActionEvent. Для каждого элемента GUI событие ActionEvent означает свой тип события, как правило, событие центральное, наиболее значимое для этого элемента. Например, для кнопки основное событие – когда на нее нажали. Именно за это нажатие и отвечает тип ActionEvent.
Используем для каждой кнопки один из способов задания обработчика события
Результат работы выглядит следующим образом:
Важное отличие двух способов состоит в том, что в случае использования метода addEventHandler() мы можем назначить более одного обработчика на то или иное событие. Например, в следующем примере будут выполнены сразу два метода двух обработчиков
Механизм обработки событий
Кратко, описать механизм обработки событий можно следующим образом: в JavaFX обработка событий выполняется по цепочке диспетчеризации событий. Когда событие генерируется, оно передается сначала корневому узлу, а затем вниз по цепочке адресату события. После обработки события в узле адресата, оно передается обратно вверх по цепочке, предоставляя возможность родительским узлам обработать его по мере надобности. Такой механизм распространения событий называется всплыванием событий. Он позволяет любому узлу цепочки «поглотить» (consume) событие, чтобы оно больше не обрабатывалось.
Теперь разберемся более подробно.
Каждое событие (объект класса javafx.event.Event либо его подкласса) имеет три свойства:
тип события (event type);
источник события (event source);
цель события (event target).
Соответственно, класс Event предоставляет методы, общие для всех объектов события
Метод
Описание
getSource()
Возвращает источник события типа Object
getTarget()
Возвращает цель события типа Object
getEventType()
Возвращает тип события типа EventType
Подклассы класса Event содержат дополнительную информацию, которая описывают конкретный тип события. Например, класс MouseEvent определяет методы getX() и getY(), которые возвращает координаты курсора мыши относительно источника события.
Тип события.
Тип события является экземпляром класса EventType. Все типы событий являются экземплярами этого класса. Например, класс KeyEvent содержит следующие типы событий:
KEY_PRESSED;
KEY_RELEASED;
KEY_TYPED.
Типы событий организованы в иерархию. Каждый тип событий имеет имя и «супер-тип». Например, имя типа, который отвечает за нажатие кнопки клавиатуры называется KEY_PRESSED, а супер-типом является KeyEvent.ANY.
Самым верхним уровнем типа событий в иерархии является Event.ROOT, который идентичен типу Event.ANY. Тип событий ANY означает любое событие этого типа. К примеру, указав тип события KeyEvent.ANY, мы можем обработать любое событие, связанное с нажатием клавиши на клавиатуре. Если же вы хотите обработать только событие, связанное с «отжатием» клавиши, используйте тип KeyEvent.KEY_RELEASED.
Цель события
Цель события – элемент управления, при взаимодействии с которым было сгенерировано событие. Например, если пользователь нажмет на кнопку – целью события является объект кнопки. Если пользователь передвинет мышку, и курсор в этот момент будет находиться над GridPane, то целью будет являться GridPane.
Целью события может быть любой объект, который реализует интерфейс EventTarget. Классы Window, Scene и Node реализуют интерфейс EventTarget.
Механизм обработки событий
Механизм обработки событий состоит из четырех этапов:
выбор цели события (targetselection);
построение маршрута события (routeconstruction);
захват события (event capturing);
всплытие события (event bubbling).
1) Выбор цели.
Когда происходит событие, система определяет, какой узел является целью объекта события (eventtarget). Правила определения цели следующие:
· для событий, связанных с клавиатурой, целью является узел, у которого в данный момент есть фокус (что такое фокус – читайте ниже);
· для событий, связанных с мышью, цель это узел, который находится под курсором мыши;
Если под курсором мыши расположено больше одного узла, тогда целью считается самый верхний узел.
Фокус – это некий указатель, который говорит о том, какой сейчас компонент активен и может реагировать на клавиатуру. В фокусе может находиться только один компонент. Фокус, как правило, отображается прямоугольником с тонкой линией или пунктирным прямоугольником. Фокус можно переключать, чтобы добраться до требуемого компонента. Как правило, переключение фокуса производится при помощи кнопки Tab.
Например, у нас есть несколько текстовых полей, в которые требуется ввести некие данные. Одновременно вводить данные в несколько полей мы не можем – значит должно быть что-то, что говорит, какой компонент сейчас активен и в него можно ввести данные с клавиатуры. Указатель, указывающий на поле, в которое мы в данный момент вводим данные, и есть фокус. Фокус могут иметь не только текстовые поля. Его могут иметь, например, и кнопки.
2) Построение маршрута события
Маршрут события строится объектом цели события с помощью метода buildEventDispatchChain(), который вызывается у цели событий. Последовательность объектов, которые должен обойти объект события называется eventdispatchchain. Рассмотрим пример:
Цепочка элементов выглядит следующим образом
Когда мы кликнем на объект класса Circle, произойдет следующая последовательность действий:
1) Среда Java создает объект события;
2) Среда вычисляет, что целью события является объект класса Circle;
3) Система запрашивает у объекта класса Circle маршрут – как добраться до этого объекта начиная от объекта Stage. В данном случае маршрут будет выглядеть так
Обход маршрута события
Обход маршрута события состоит из двух фаз:
1) Захват события (capturing phase);
2) Всплытие события (Bubbling phase).
Событие обходит каждый узел маршрута дважды: один раз в течение фазы захвата события (сверху вниз) и один раз в течение фазы всплытия события (снизу вверх).
Вы можете зарегистрировать в узле фильтр события (event filter) и обработчик события (event handler), и обрабатывать события определенного типа, когда они будут проходить по маршруту в фазе захвата и в фазе всплытиясоответственно.
То есть, зарегистрированный фильтр события позволяет обработать события на этапе захвата, когда событие передается по цепочке сверху вниз. Обработчик события позволяет обработать событие на этапе всплытия события, когда объект события передается обратно по той же цепочке снизу вверх.
Для регистрации фильтра события используется метод addEventFilter(), а для регистрации обработчика события – метод addEventHandler(). Сами объекты фильтра и обработчика ничем друг от друга не отличаются и оба реализуют интерфейс EventHandler<T extends Event>.
При вызове фильтра\обработчика события, тот объект, у которого был вызван обработчик, называется источником события(eventsource). То есть, при прохождении по маршруту источник события постоянно меняется.
Посмотрите еще раз на схему маршрута события, которая была приведена выше. Вы можете, например, зарегистрировать обработчик или фильтр в узле Group и обрабатывать все события определенного типа, которые будут проходить через объект Group. В данном случае, это будут все события, которые будут происходить в сцене, т.к. Group в данном случае – корневой узел. Рассмотрим пример
Мы зарегистрировали фильтр событий в узле HBox, и наш узел HBox будет выполнять метод handle() всякий раз, когда объект события, проходя по маршруту, будет проходить через объект HBox сверху вниз, на фазе захвата событий.
Также объект класса Event содержит методы consume() и isConsumed(). Вызов метода consume() означает, что вы указываете событию, что оно «поглощается» текущим обработчиком, и дальнейшая обработка не требуется – событие не идет дальше по цепочке диспетчеров событий. Метод isConsumed() возвращает true, если был вызван метод consume() и false иначе. Модифицируем наш пример следующим образом – напишем обработчик для кнопки 1
Как мы видим, при нажатии на кнопку отрабатывает фильтр в HBox и обработчик в b1. Теперь вернемся в фильтр в HBox и допишем следующее
Мы в фильтре вызвали у события метод consume() и после вызова этого метода, путешествие события по узлам заканчивается – событие «поглощается» и дальше по цепочке не передается.
Как мы видим, фон кнопки не поменялся, т.к. обработчик, который мы добавили в кнопку, не сработал, т.к. после вызова метода consume(), событие не было передано дальше по цепочке. Если вы зарегистрировали в одном узле несколько обработчиков/фильтров и в одном из них вызвали метод consume(), тогда система даст отработать другим обработчикам/фильтрам в этом узле, после чего обработка события останавливается.
Таким образом, расставляя в нужных местах фильтры, обработчики и используя метод consume() мы можем очень тонко настраивать реакцию приложения на различные события. Рассмотрим несколько примеров.
Пример 1. Представим себе, что мы хотим, чтобы при нажатии мыши на любом месте в нашем окне появлялось окно с информацией. Но у нас в окне есть некоторый элемент (например, красный прямоугольник), нажатие на который обрабатывается иначе.
Мы можем поступить следующим образом:
1) зарегистрировать обработчик на объекте красного прямоугольника. Внутри обработчика прописать нужное поведение, после чего вызвать метод consume();
2) для объекта сцена зарегистрировать обработчик, который показывает окно с информацией.
Таким образом, если целью события будет красный прямоугольник – выполнится обработчик для красного прямоугольника и событие будет поглощено. Во всех остальных случаях будет выполнен обработчик для объекта сцены.
Пример 2. Рассмотрим приложение «Калькулятор», которое мы рассматривали на предыдущей лабораторной работе. Попробуем реализовать функционал для нажатия клавиш цифровой клавиатуры и кнопки «C» (обнулить значение в поле ввода).
Создавать отдельный обработчик для каждой кнопки было бы нецелесообразно, поэтому мы можем реализовать требуемое поведение следующим образом.
Так как у нас кнопки расположены внутри менеджера компоновки GridPane, то мы можем создать фильтр событий, который будет обрабатывать событие типа ActionEvent.ACTION. Внутри фильтра мы выясним – было ли нажатие на цифровой блок или на какую-то другую кнопку. Если произошло нажатие на цифровой блок – тогда мы соответствующим образом обработаем нажатие, после чего поглотим событие.
Для реализации кнопки «С» создадим для нее отдельный обработчик события.
1) Отнаследуемся от класса MyButton и создадим класс отдельно для клавиш цифрового блока.
Кнопка цифрового блока будет хранить внутри поле value, которое будет содержать соответствующую цифру цифрового блока.
Далее, пропишем фильтр событий для GridPane
Таким образом, обработку кнопок цифрового блока берет на себя GridPane.
Для кнопки «C» реализуем отдельный обработчик
В итоге получаем нужный нам функционал
Наследование и композиция в UML
Наследование (обобщение)
Отношение обобщения (generalization) в UML изображается при помощи сплошной линии и жирной треугольной стрелки, ведущей от подкласса с суперклассу.
Что обозначает это отношение? В спецификации UML по этому поводу сказано
Обобщение - таксономическое отношение между более общим элементом и более конкретным. Каждый экземпляр конкретного элемента также является непрямым экземпляром обобщенного элемента. Таким образом, конкретизированный элемент косвенно обладает свойствами обобщенного элемента
Является ли отношение обобщения синонимом наследования (inheritance) в объектно-ориентированном проектировании? Ответ зависит от области применения отношения. Для диаграммы концептуальных классов из модели предметной области ответом будет "нет". В этом случае отношение обобщения подразумевает то, что суперкласс является множеством, а подкласс - подмножеством. С другой стороны, на диаграмме классов проектирования это отношение подразумевает объектно-ориентированное наследование свойств подкласса от суперкласса.
Композиция
Композиция (composition), так же известная как композитная агрегация (composite aggregation), является строго определенным типом связи "целое-часть" и полезна в некоторых моделях. Отношение композиции предполагает, что
экземпляр части (например, Wheel) в каждый момент времени принадлежит только одному целому предмету (например, Car);
часть всегда принадлежит целому (пальцы не существуют отдельно от руки);
целое ответственно за создание и удаление своих частей - либо через самостоятельное создание\удаление, либо через взаимодействие с другими объектами. Следствием этих ограничений является то, что при уничтожении композитного объекта его части должны быть либо уничтожены, либо присоединены в другому композитному объекту. Например, если реальная настольная игра "Монополия" уничтожается, то также уничтожаются все ее клетки (с концептуальной точки зрения). Аналогично, если программный объект Car уничтожается, то уничтожаются и программные объекты Wheel.
Для обозначения композиции в UML используется закрашенный ромб на линии ассоциации со стороны целого.
Задание на лабораторную работу
Задание 1. Модификация классов
Добавьте модификаторы доступа к членам классов, разработанных в лабораторной работе 2 (классы TimeSpan, BankAccount и Student).
Подумайте над тем, какие члены класса сделать приватными, а какие публичными
Задание 2. Создание UML-диаграмм
Для модифицированных классов из второй лабораторной работы нарисуйте диаграммы классов UML.
Задание 3. Класс IntStack
Необходимо создать класс IntStack, который реализует функционал стека целых чисел с помощью статического массива.
Нарисуйте класс в диаграмме классов UML
При реализации стека учитывайте следующее:
при реализации класса вам необходимо указать область видимости для полей и методов. Подумайте о том, какие методы необходимо отнести к "интерфейсу", а какие к реализации;
размер стека для внешнего пользователя не имеет значения и теоретически неограничен;
внешний пользователь не должен знать и догадываться, что стек "внутри" реализован с помощью обычного статического массива;
вы должны написать "реализацию" класса таким образом, чтобы можно моделировать динамическое поведение стека с помощью обычного статического массива;
продумайте, какие аргументы методы должны принимать и какие значения возвращать.
Перейдите во вкладку "Подсказка", чтобы получить подсказку, которая поможет вам в реализации класса или попробуйте реализовать класс без подсказки.
При создании объекта стека инициализируйте внутренний статический массив с каким-то изначальным размером;
При вставке элементов проверяйте, не выходите ли вы за пределы внутреннего статического массива;
Если внутренний массив переполнен, создавайте новый массив большего размера: создавайте новый массив, копируйте значения из старого массива в новый, заменяйте новый массив на старый. Помните, что массивы в Java - объекты и что поле - просто указатель на массив и что указатель может указывать на другой массив;
При уменьшении размера стека реализуйте уменьшение внутреннего статического массива. Подумайте, как это лучше сделать, чтобы не уменьшать внутренний массив при каждом удалении элемента из стека.
ВАЖНО! Стек необходимо реализовать с помощью статического целочисленного массива!
НЕ ИСПОЛЬЗУЙТЕ КОЛЛЕКЦИИ И ПОТОКИ ДАННЫХ, ТОЛЬКО МАССИВЫ
Для внешнего пользователя класса, поведение класса выглядит следующим образом:
пользователь может создать новый стек с конструктором без параметров;
пользователю доступна команда pop() для извлечения элемента из стека. Метод pop() возвращает извлеченный элемент. При попытке извлечь из пустого стека, метод возвращает 0 или выбрасывает исключение (реализуйте выброс исключения, если вы знаете, как это делать и что такое исключение);
пользователю доступна команда push(), которая добавляет элемент в стек;
пользователю доступна команда peek() для просмотра элемента на вершине стека без его исключения.
пользователь может получить значение количества элементов в стеке с помощью метода size();
пользователь может проверить, пустой ли стек с помощью метода isEmpty();
пользователь может очистить стек полностью с помощью метода clear().
Задание 4. Создание класса по UML-диаграмме
Создайте Java-классы исходя из диаграммы, представленной ниже. Код методов реализовывать не нужно, только заголовки и общую структуру классов.
Задание на лабораторную работу
Как выполнять лабораторную работу
Скачайте архив с заданием на первую лабораторную работу.
Откройте IntelliJ IDEA (предварительно установите плагин EduTools), выберите пункт Learn and Teach -> Import Local Course
Выберите архив с заданием
Откройте задание с помощью кнопки Open
Задание на лабораторную работу состоит из 7 задач, которые необходимо решить
Для решения каждой задачи требуется написать код для метода и вернуть правильное значение. Условие работы метода описано в правой части окна. После написания кода, нажмите кнопку Check, чтобы проверить, правильно ли вы реализовали метод.
Пример выполнения лабораторной работы
Давайте рассмотрим выполнение лабораторной работы на примере первой задачи.
Нам необходимо реализовать метод sleepIn(). Согласно условию задачи, метод принимает на вход булеву переменную weekday (будний ли день) и переменную vacation (отпуск или нет).
Давайте нажмем кнопку Check и посмотрим, что произойдет.
Как мы видим, изначально задача не решена, нам необходимо корректно реализовать метод.
Согласно условию задачи, метод возвращает true, если выходной или отпуск. Очевидно, что код метода будет выглядеть следующим образом.
Проверим правильность написания кода.
Ура, задача решена. Для успешной сдачи лабораторной работы необходимо выполнить все 7 задач.
Условия задач
На всякий случай, продублируем условия задач на первую лабораторную работу
1. Метод sleepIn()
Метод sleepIn() принимает решение о том, необходимо ли нам проснуться или можно еще поспать.
Нужно проснуться, если будний день и нет отпуска
Параметр weekday определяет, будний ли день
Параметр vacation определяет, отпуск или нет
Метод возвращает true, если можно еще поспать и false в обратном случае
2. Метод closeToTen()
Дано два числа, метод closeToTen() возвращает число, которое наиболее близко к 10. Если оба числа равноблизки к 10, то метод возвращает 0.
Подсказка: используйте метод Math.abs() для получения абсолютного значения числа.
Пример:
closeToTen(8, 13) -> 8 (число 8 ближе к 10)
closeToTen(8, 11) -> 11 (число 11 ближе к 10)
closeToTen(13, 7) -> 0 (оба числа равноблизки к 10)
3. Метод reverseArray()
Метод принимает на вход массив и возвращает массив в обратном порядке
Пример:
reverseArray([1,2,3]) -> [3,2,1]
reverseArray([7,0,0]) -> [0,0,7]
4. Метод biggestArray()
Метод принимает на вход два массива и возвращает массив, сумма элементов которого больше. Если суммы элементов массивов равны, метод возвращает первый массив.
Пример:
biggestArray([1,2,3], [1,2,4]) -> [1,2,4]
biggestArray([1,5,10], [15]) -> [1,5,10]
biggestArray([2,4,6], [10,2]) -> [2,4,6]
5. Метод seriesUp()
Метод принимает на вход число n >=0. Метод возвращает массив по шаблону {1, 1,2, 1,2,3, ...1,2,3..n}. Длина итогового массива = n*(n+1)/2
Пример:
seriesUp(3) -> [1, 1, 2, 1, 2, 3]
seriesUp(4) -> [1, 1, 2, 1, 2, 3, 1, 2, 3, 4]
seriesUp(2) -> [1, 1, 2]
6. Метод canSplit()
Дан непустой массив. Метод возвращает true, если массив можно разбить на две части (необязательно равные) таким образом, чтобы сумма частей была равна.
Дан массив, метод возвращает true если каждый элемент массива равен или больше предыдущему
Пример:
scoresIncreasing([1, 3, 4]) -> true
scoresIncreasing([1, 3, 2]) -> false
scoresIncreasing([1, 1, 4]) -> true
Проект - игра "Крестики-нолики"
Реализуйте игру "Крестики-нолики". В игру играют два человека, поочередно вводя координаты поля, куда необходимо поставить крестик или нолик. Ввод происходит в формате [строка] [столбец]. Например: 0 0 или 1 2.
Сценарий игры будет примерно следующим
Ниже представлен возможный вариант метода для отрисовки поля. В данном случае подразумевается, что в двумерном массиве число 0 соответствует пустому полю, число 1 соответствует X, а число - 1 соответствует 0.
Блок-схема алгоритма программы может быть примерно следующей.
В качестве дополнительного задания необходимо предусмотреть метод для проверки корректности ввода: можно ставить крестик или нолик только в свободную ячейку, а также отслеживать выход за пределы поля при указании номера строки и столбца.
Если пользовательский ввод оказался некорректным, то программа просит игрока повторить ход.
Наследование и композиция
Повторное использование кода
Возможность повторного использования кода принадлежит к числу важнейших преимуществ языков объектно-ориентированного программирования.
композиция (composition) - объекты уже имеющихся классов просто создаются внутри нового класса. Программист просто использует функциональность уже готового кода;
наследование (inheritance) - новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса.
Композиция - использование функционала одних объектов в составе других объектов. Рассмотрим пример класса FileManager, в котором определен метод для сохранения текстовых данных в файл. Класс Document использует функционал класса FileManager, чтобы сохранить текстовый документ на жесткий диск.
Базовые понятия механизма наследования
Наследование - отношение между классами, в котором один класс повторяет структуру и поведение другого класса (или нескольких других классов).
Класс, на основе которого создается новый класс, называется суперклассом (базовым классом, родительским классом). Новый создаваемый класс называется подклассом (дочерним классом, производным классом, классом-наследником и так далее).
Создание подкласса в Java
Создание подкласса практически не отличается от создания обычного класса, кроме необходимости указать суперкласс, на основе которого создается подкласс. В Java для этого существует ключевое слово extends:
В Java, в отличие от C++, отсутствует множественное наследование, то есть подкласс может создаваться на основе только одного суперкласса.
В Java присутствует многоуровневое наследование: подкласс может быть суперклассом для другого класса. Благодаря этому можно создавать целые цепочки классов, связанные механизмом наследования
Наследование членов суперкласса
Если член класса определен как private, то при наследовании доступ к нему со стороны подкласса закрыт. Важно понимать, что приватный член суперкласса в подклассе есть, только он закрыт для прямого доступа. К примеру, данный код не скомпилируется
Закрытыми могут быть как поля класса, так и его методы. Если необходимо открыть поля или методы для доступа к ним со стороны подкласса, при объявлении членов суперкласса используют слово protected либо создают геттеры и сеттеры для доступа к полям. К примеру, данный код скомпилируется и будет работать корректно
Данный пример демонстрирует доступ к полям с помощью геттеров
Инициализация базового класса
Так как в наследовании участвуют два класса, базовый и производный, не сразу понятно, какой же объект получится в результате. Внешне все выглядит так, словно новый класс имеет тот же интерфейс, что и базовый класс, плюс еще несколько дополнительных полей и методов.
Однако наследование не просто копирует интерфейс базового класса. Когда вы создаете объект производного класса, внутри него содержится подобъект базового класса. Этот подобъект выглядит точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса "упакован" объект базового класса.
Чтобы подобъект базового класса был правильно инициализирован, при вызове конструктора подкласса, сначала вызывается конструктор базового класса, у которого есть необходимые знания и привилегии для проведения инициализации базового класса.
При использовании конструкторов без параметров, у компилятора не возникает проблем с вызовом таких конструкторов, так как нет нужды передавать аргументы. В этом случае Java автоматически вставляет вызовы конструктора базового класса в конструктор производного класса.
Результат работы такого приложения будет следующим
Как видно из данного примера, цепочка вызовов конструкторов начинается с самого базового класса. Таким образом, подобъект базового класса инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса Cat не будет определен, Java сгенерирует конструктор по умолчанию, в котором также будет вызван конструктор базового класса.
Если в классе не определен конструктор без параметров, то вызов конструктора базового класса надо будет оформлять явно. К примеру, такой код вызовет ошибку на этапе компиляции
Для явного вызова конструктора суперкласса используется ключевое слово super. Более подробно мы рассмотрим его ниже, а сейчас приведем пример корректного вызова конструктора суперкласса
Ключевое слово super
Как было сказано ранее, наследование в Java реализуется следующим образом - в объект производного класса добавляется скрытый объект базового класса, который и обеспечивает вызов методов суперкласса.
Ключевое слово super как раз и ссылается на этот скрытый объект суперкласса. Используя это ключевое слово, можно получить доступ к членам суперкласса (если позволяет их модификатор доступа)
Как видно из примера, ключевое слово super имеет что-то общее с ключевым словом this.
Переопределение методов
При использовании механизма наследования возникает проблема с использованием методов суперкласса. Часто метод суперкласса не отражает изменения и нововведения, внесенные в подклассе и вызов таких методов дает некорректную информацию об объекте. Рассмотрим следующий пример
Результат работы такого приложения будет следующим
Как вы уже понимаете, в данной части кода
был вызван метод getInfo() суперкласса, который выводит информацию только о двух параметрах коробки, тогда как класс Box3D содержит три параметра. Таким образом, метод getInfo() для класса Box3D становится как бы некорректным, неправильным, он выдает неполную информацию об объекте. Как решить эту проблему?
Первый вариант - создать в подклассе Box3D свой метод для вывода информации
Тогда мы будем вызывать метод get3DInfo()
что даст нам корректный результат
Такой вариант является интуитивно понятным, но все-таки не совсем корректным. Теперь у объекта Box3D существует аж целых два метода для получения информации об объекте, один из которых является некорректным, что вносит путаницу для нас и для тех программистов, которые будут использовать наш код.
Самым правильным вариантом будет как бы "переписать" метод getInfo(), предоставить версию метода getInfo() для класса Box3D. Таким образом и класс Box и класс Box3D будет иметь метод getInfo(), просто в классе Box3D он будет иначе реализован, с учетом появления третьего параметра. Кроме того, объект класса Box3D теперь будет содержать только один метод для получения информации об объекте и этот метод будет корректно работать.
Для реализации своеобразного "переписывания" метода в подклассе, в Java существует механизм переопределения метода (method overriding).
Воспользуемся механизмом переопределения метода, чтобы корректно решить проблему с классами Box и Box3D (в примере для наглядности опущены некоторые члены классов)
Обратите внимание что сигнатуры двух методов getInfo() полностью совпадают - это обязательное условие для срабатывания механизма переопределения метода. Метод в суперклассе называется переопределенным.
Также обратите внимание на строку 33, где записано @Override. Такая запись называется аннотацией.
Аннотация - это дополнительное пояснение для компилятора и для различных утилит, которые работают с кодом (анализаторы, генераторы документации и так далее). Указание аннотации не является обязательным, код будет работать и без аннотации, но указание аннотации является одним из важных правил грамотного написания кода.
Результат работы нашего примера будет следующим
Обратите внимание, что и для класса Box и для класса Box3D мы вызываем метод с одним и тем же названием и с одной и той же сигнатурой
Но в зависимости от того, для какого класса мы вызываем этот метод, в первом случае отрабатывает метод в классе Box, а во втором случае - метод в классе Box3D.
Следует отличать механизм перегрузки метода от механизма переопределения метода.
Метод может быть как перегруженным, так и переопределенным.
Запрет наследования с помощью ключевого слова final
Иногда бывает необходимо запретить наследоваться от какого-то класса либо запретить переопределять метод. В этом случае, в объявлении класса или метода укажите ключевое слово final
Класс Object
В Java определен специальный класс Object, который является суперклассом для всех классов Java. Иными словами, все классы в языке Java являются подклассами, производными от класса Object.
В классе Object определены перечисленные ниже методы, которые доступны в любом объекте
С некоторыми методами класса Object мы встретимся позже, а сейчас нам интересен только метод toString().
Метод toString()
Метод toString() призван возвращать строковое представление объекта (список значений полей). Рассмотрим пример
Обратите внимание, что класс Box не содержит никаких членов, однако, так как Box наследуется от класса Object, ему доступны методы суперкласса и метод toString() в частности.
Результатом работы данного примера является следующая строка
Метод toString() в классе Object выводит полное название класса и 16-ричное представление хеш-кода объекта.
Если вам необходимо вывести значения полей объекта в виде строки, используйте метод toString(), он для этого и был предназначен, его использование является общепринятым правилом.
Метод getInfo() в примере выше был использован только в демонстрационных целях!
Чтобы метод toString() выводил нужную информацию, его необходимо пепреопределить как в следующем примере
Результат будет следующим
Также метод toString() имеет одну примечательную особенность - его можно явно не вызывать, а просто указывать ссылочную переменную, Java сама вызовет метод toString(). Например, следующий код
выдаст следующий результат
Как вы видите, результат работы строк 5 и 6 является идентичным.
Ссылка на приложения для построения диаграмм UML -
Для установки JDK необходимо скачать установщик по следующей (также вы можете загуглить "download jdk" и перейти по первой ссылке).
Простой и понятный ролик о том, как работает Java -
часть 1 - ;
часть 2 - ;
часть 3 - .
Для работы JavaFX на с более поздними версиями Java требуется специальным образом создавать новый проект. Подробнее читайте .
Остальные правила связаны с касаниями и жестами и в данном курсе не рассматриваются (ссылка - , раздел «Target Selection»).
---------
| |
| |
| |
---------
First player: 0 0
---------
| X |
| |
| |
---------
Second player: 1 1
---------
| X |
| 0 |
| |
---------
First player: 0 1
---------
| X X |
| 0 |
| |
---------
Second player: 2 2
---------
| X X |
| 0 |
| 0 |
---------
First player: 0 2
First player won!
Process finished with exit code 0
Main.java
public static void drawField(int[][] array) {
System.out.println("---------");
for (int i = 0; i < 3; i++) {
System.out.print("| ");
for (int j = 0; j < 3; j++) {
String symbol = " ";
if (array[i][j] != 0)
symbol = array[i][j] == 1 ? "X" : "0";
System.out.print(symbol + " ");
}
System.out.println("|");
}
System.out.println("---------");
}
class FileManager {
public void saveToFile(String text, String path) {
// тело метода
}
}
class Document {
// класс Document содержит ссылку на объект
// класса FileManager
private FileManager manager;
private StringBuilder contents;
private String path;
public Document(FileManager manager, String path) {
this.manager = manager;
this.contents = new StringBuilder();
this.path = path;
}
public void saveDocument() {
manager.saveToFile(contents.toString(), path);
}
}
// Суперкласс
class Person {
String firstName;
String lastName;
}
// Подкласс
class Student extends Person {
String group;
long id;
}
public class Main {
public static void main(String[] args) {
Student student = new Student();
student.firstName = "Иван";
student.lastName = "Иванов";
student.id = 10000L;
}
}
// Суперкласс
class Person {
String firstName;
String lastName;
}
class UniversityMember{}
// МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ ЗАПРЕЩЕНО!
// ЭТОТ КОД ВЫЗОВЕТ ОШИБКУ КОМПИЛЯТОРА
class Student extends Person, UniversityMember {
String group;
long id;
}
class Vehicle {
public void moveTo(Point destination) {
// тело метода
}
}
class Truck extends Vehicle {
public void carryWeight(double weight) {
// тело метода
}
}
class DumpTruck extends Truck {
public void dumpWeight() {
// тело метода
}
}
class Shape2D {
private double width;
private double height;
}
class Rectangle extends Shape2D {
// ОШИБКА НА ЭТАПЕ КОМПИЛЯЦИИ
public double getArea() {
return width * height;
}
}
class Shape2D {
protected double width;
protected double height;
}
class Rectangle extends Shape2D {
// Данный код корректен
public double getArea() {
return width * height;
}
}
class Shape2D {
private double width;
private double height;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
}
class Rectangle extends Shape2D {
// Данный код корректен
public double getArea() {
return getWidth() * getHeight();
}
}
class Animal {
public Animal() {
System.out.println("Конструктор класса Animal");
}
}
class Mammal extends Animal {
public Mammal() {
System.out.println("Конструктор класса Mammal");
}
}
class Cat extends Mammal {
public Cat() {
System.out.println("Конструктор класса Cat");
}
}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
}
}
Конструктор класса Animal
Конструктор класса Mammal
Конструктор класса Cat
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100);
}
}
class Box {
public double width;
public double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
}
class Box3D extends Box {
public double depth;
// НЕТ ЯВНОГО ВЫЗОВА КОНСТРУКТОРА СУПЕРКЛАССА !
public Box3D(double depth) {
this.depth = depth;
}
}
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100, 200, 300);
}
}
class Box {
public double width;
public double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
}
class Box3D extends Box {
public double depth;
public Box3D(double width, double height, double depth) {
super(width, height); // <-- ВЫЗОВ КОНСТРУКТОРА СУПЕРКЛАССА
this.depth = depth;
}
}
class Box {
private double width;
private double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
// Площадь прямоугольника
public double getArea() {
return width * height;
}
}
class Box3D extends Box {
private double depth;
public Box3D(double width, double height, double depth) {
super(width, height); // <-- ВЫЗОВ КОНСТРУКТОРА СУПЕРКЛАССА
this.depth = depth;
}
// Мы используем метод суперкласса, чтобы
// посчитать площадь трехмерной коробки
public double get3DArea() {
double area2D = super.getArea(); // <---- Вызов метода суперкласса
return area2D * depth;
}
}
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.getInfo());
}
}
class Box {
private double width;
private double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
public String getInfo() {
return "Объект Box {" +
"ширина = " + width +
", высота = " + height +
'}';
}
}
class Box3D extends Box {
private double depth;
public Box3D(double width, double height, double depth) {
super(width, height);
this.depth = depth;
}
}
Объект Box {ширина = 100.0, высота = 200.0}
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.getInfo());
class Box {
private double width;
private double height;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Box(double width, double height) {
this.width = width;
this.height = height;
}
public String getInfo() {
return "Объект Box {" +
"ширина = " + width +
", высота = " + height +
'}';
}
}
public class Main {
public static void main(String[] args) {
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.get3DInfo());
}
}
Объект Box3D {ширина = 100.0, высота = 200.0, глубина = 300.0}
public class Main {
public static void main(String[] args) {
Box box = new Box(600,600);
System.out.println(box.getInfo());
Box3D box3D = new Box3D(100, 200, 300);
System.out.println(box3D.getInfo());
}
}
class Box {
// Поля, конструктор и геттеры\сеттеры
public Box(double width, double height) {
this.width = width;
this.height = height;
}
public String getInfo() {
return "Объект Box {" +
"ширина = " + width +
", высота = " + height +
'}';
}
}
class Box3D extends Box {
// Поля, конструктор и геттеры\сеттеры
@Override
public String getInfo() {
return "Объект Box3D {" +
"ширина = " + super.getWidth() +
", высота = " + super.getHeight() +
", глубина = " + depth +
'}';
}
}
Объект Box {ширина = 600.0, высота = 600.0}
Объект Box3D {ширина = 100.0, высота = 200.0, глубина = 300.0}
Box box = new Box(600, 600);
System.out.println(box.getInfo());
Box3D box3D = new Box3D(100, 200, 300);
System.out.println(box3D.getInfo());
final class A {}
class B extends A {
// ВЫЗОВЕТ ОШИБКУ КОМПИЛЯЦИИ !
}
class C {
final public void foo() {}
}
class D extends C {
@Override
public void foo() {} // <-- Ошибка компиляции !
}
Метод
Описание
Object clone()
Создает новый объект, аналогичный клонируемому объекту
boolean equals(Object объект)
Определяет равнозначность объектов
void finalize()
Вызывается перед тем, как неиспользуемый объект будет удален "сборщиком мусора"
Class<?> getClass()
Определяет класс объекта во время выполнения
int hashCode()
Возвращает хеш-код, связанный с вызывающим объектом
void notify()
Возобновляет работу потока, ожидающего уведомления от вызывающего объекта
void notifyAll()
Возобновляет работу всех потоков, ожидающих уведомления от вызывающего объекта
String toString()
Возвращает символьную строку, описывающую объект
void wait()
void wait(long мсек)
void wait(long мсек, int наносек)
Ожидает исполнения другого потока
public class Main {
public static void main(String[] args) {
Box box = new Box();
System.out.println(box.toString());
}
}
class Box{}
com.company.Box@1540e19d
public class Main {
public static void main(String[] args) {
Box box = new Box(100, 200);
System.out.println(box.toString());
}
}
class Box {
private double width;
private double height;
public Box(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public String toString() {
return "Box{" +
"width=" + width +
", height=" + height +
'}';
}
}
Box{width=100.0, height=200.0}
public class Main {
public static void main(String[] args) {
Box box = new Box(100, 200);
System.out.println(box.toString());
System.out.println(box);
}
}
Тема: Клиент-серверная архитектура приложения. Сокеты в Java. Технология Java Servlets. Проект Spring Boot. Быстрое создание приложения с помощью Spring Boot. Архитектура Spring-приложения (web layer, service layer, repository layer).
Клиент-серверная архитектура
Сервер - компьютер или программа, которая управляет ресурсами (информация, файлы, база данных) называется сервером этого ресурса или просто сервером.
Архитектура "клиент-сервер" определяет общие принципы организации взаимодействия, где имеются серверы (узлы-поставщики некоторых специфичных функций и сервисов) и клиенты, (потребители этих сервисов).
Между клиентами и серверами должны быть установлены правила взаимодействия, которые называются протоколом взаимодействия или протоколом обмена. Каждая часть взаимодействует друг с другом, обмениваясь сообщениями в заранее согласованном формате.
В рамках данного курса рассматривается так называемая "трехзвенная архитектура"
Компоненты трехзвенной архитектуры:
клиент - этот компонент отвечает за представление данных конечному пользователю;
выделенный сервер приложений - здесь содержится бизнес-логика приложения;
сервер БД - предоставляет запрашиваемые данные.
Сервер приложений (application server) – сервисная программа, которая обеспечивает доступ клиентов к прикладным программам, выполняющимся на сервере.
Большинство серверов приложений имеют в своем составе веб-сервер. Это означает, что сервер приложений может делать все, на что способен веб-сервер. Кроме того, сервер приложений имеет компоненты и функции для поддержки сервисов уровня приложения, таких как пул соединений, поддержка транзакций и так далее.
Технология Java Servlets
На заре развития интернета не существовало технологий для создания динамических веб-страниц. В то время сайт представлял собой набор статических заранее написанных и сверстанных страниц с помощью языка разметки HTML. Если владелец сайта хотел обновить информацию на страничке, он делал это непосредственно на своем компьютере, после чего загружал на сервер обновленную версию HTML-страницы.
Среди предложенных решений по созданию динамических страниц, одной из первых была технология Java Servlets. В то время это была революционная технология, которая позволяла расширить возможности веб-серверов на основе модели запрос-ответ (request - response). Технология сервлетов позволяла веб-серверам обрабатывать HTTP-запросы и динамически генерировать веб-странички в зависимости от HTTP-запроса.
Несмотря на почтенный возраст, технология сервлетов претерпела серьезные изменения для того, чтобы соответствовать современной технологии разработки веб-приложений. На данный момент, технология сервлетов является наиболее часто используемой технологией для обработки HTTP запросов/откликов. Кроме того, сервлеты являются базой для почти всех Java-фреймворков, которые работают с HTTP протоколом (JSF, Struts, Spring MVC, BIRT и так далее).
Сервлет (Servlet), по сути, является классом Java, который используется для расширения возможностей сервером, предназначенных для размещения приложений. Сервлеты могут отвечать на запросы и генерировать отклики. Базовым классом для всех сервлетов является класс javax.servlet.GenericServlet. Этот класс определяет обобщенный, независимый от протокола сервлет.
Схема работы технологии сервлетов представлена на рисунке ниже
клиент (например, веб-браузер) передает HTTP-запрос веб-серверу. В случае, если от веб-сервера требуется предоставить статический файл или какой-то ресурс (например, изображение), то он просто возвращает требуемый статический файл или ресурс;
если веб-сервер не может самостоятельно обработать HTTP-запрос (например, пользователь передает какие-то данные либо требуется предоставить динамическую страницу и так далее), веб-сервер передает этот запрос web-контейнеру (его еще называют servlet-контейнером);
контейнер определяет – какой сервлет может выполнить этот запрос, создает объекты классов HttpServletRequest и HttpServletResponse, создает thread, создает объект класса сервлета и передает ему объекты классов HttpServletRequest и HttpServletResponse;
Контейнер вызывает метод сервлета service(), который вызывает соответствующий HTTP-запросу метод (например, если запрос был HTTPGET, то будет вызван метод doGet(), подробнее этот вопрос будет разбираться далее), которому, в качестве аргументов, передает объекты классов HttpServletRequest и HttpServletResponse;
Соответствующий метод (например, метод doGet()) возвращает динамическую страницу внутри объекта класса HttpServletResponse, ссылку на который имеет контейнер;
После этого поток завершается, контейнер конвертирует объект класса HttpServletResponse в HTTP-отклик (HTTP response) и отдает его веб-серверу, который возвращает его клиенту.
Фреймворк Spring
Spring – свободно-распространяемый легковесный фреймворк, призванный упростить разработку корпоративных и веб-приложений (можно использовать и для любых других типов приложений) на языке Java (является альтернативной стеку Jakarta EE).
В данный момент Spring представляет собой целый набор модулей, которые можно использовать выборочно для тех или иных проектов.
Дадим краткую характеристику некоторым модулямSpring:
SpringCore – ядро платформы, предоставляет базовые средства для создания приложений — управление компонентами (бинами, beans), внедрение зависимостей, MVC фреймворк, транзакции, базовый доступ к БД. В основном это низкоуровневые компоненты и абстракции. По сути, неявно используется всеми другими компонентами;
SpringMVC – обеспечивает архитектуру паттерна Model-View-Controller при помощи слабо связанных готовых компонентов для разработки веб-приложений;
Spring Data – обеспечивает доступ к данным: реляционные и нереляционные БД, KV хранилища и т.п.;
Spring Cloud – используется для микросервисной архитектуры;
Spring Security – авторизация и аутентификация, доступ к данным, методам и т.п. OAuth, LDAP, и различные провайдеры.
SpringBoot
Проект SpringBoot – решение, которое позволяет вам легко создавать полноценные приложения Spring, про которые можно сказать «просто запусти».
Spring Boot позволяет быстро создать и сконфигурировать (т.е. настроить зависимости между компонентами) приложение, упаковать его в исполняемый самодостаточный артефакт. Это то связующее звено, которое объединяет вместе набор компонентов в готовое приложение.
ОсобенностиSpring Boot:
создание полноценных Spring-приложений;
встроенный сервлет-контейнер (Tomcat или Jetty);
обеспечивает начальные pom-файлы для упрощения конфигурации Maven;
используется автоконфигурация, где это возможно;
используется принцип «convention over configuration». Для большинства конфигураций не нужно ничего настраивать.
Изучение фреймворка Spring лучше всего начать с установки требуемого программного обеспечения и разработки тестового приложения с помощью Spring Boot.
Установка программного обеспечения
Для выполнения домашнего задания нам понадобится следующее программное обеспечение:
Создание Spring Boot проекта
Существует несколько способов создать Spring Boot проект. Из наиболее простых способов можно выделить:
создание проекта средствами IDE.
Создадим проект с помощью мастера Intellij IDEA. Создадим новый Spring Boot проект (выберите пункт Spring Initializr). Необходимо указать JDK, метаданные проекта, а также выбрать из списка модулей нужные нам модули Spring
Для выполнения задания нам необходимо выбрать web-модуль. Панель выбранных компонентов будет иметь следующий вид:
После окончания работы мастера создания проектов, мы получим стартовый проект Spring Boot. Рассмотрим структуру проекта и обозначим ключевые файлы:
HotelApplication.java - стартовый класс Spring Boot приложения;
application.properties - файл с настройками приложения. В нем можно переопределить настройки по умолчанию;
pom.xml - POM-файл проекта. Используется сборщиком Maven.
POM-файл (Project Object Model) – это XML-файл, который содержит информацию о деталях проекта, и конфигурации для создания проекта на Maven. Он всегда находится в базовом каталоге проекта. Во время выполнения задач, Maven ищет pom-файл в базовой директории проекта. Он читает его и получает необходимую информацию, после чего выполняет задачи.
Корневым элементом является элемент <project>. Внутри тега project содержится основная и обязательная информация о проекте.
Зависимости (dependency) – это те библиотеки, которые непосредственно используются в проекте для компиляции кода или его тестирования.
Мы создаем RESTful веб-службу с помощью Spring Boot, поэтому нам нужно «подтянуть» для нашего проекта различные Spring-модули (библиотеки с классами, jar-файлы).
В обычных проектах нам бы было необходимо добавлять каждую зависимость вручную, но Spring Boot позаботился о нас и предоставил нам своего рода «мета-зависимости». Смысл их в том, что Spring Boot понимает, что если вы создаете web-приложение то вам нужен примерно одинаковый набор jar-файлов, поэтому чтобы не писать каждый jar-файл отдельно, мы указываем одну зависимость, а она уже «подтянет» за нас другие отдельные зависимости для создания веб-приложения.
Запуск приложения
Теперь давайте сразу запустим приложение. Убедимся, что приложение запущено успешно
перейдем в браузер и попробуем зайти на сайт.
Как видите, Spring Boot приложение успешно запущено. Так как Spring Boot берет на себя большую часть рутинной работы по созданию и запуску приложения, давайте разберемся, что же происходит, когда мы запускаем приложение:
Устанавливается конфигурация приложения по умолчанию;
Запускается контекст приложенияSpring(Springapplicationcontext) – это контейнер для кода, который работает на сервере (службы, контроллеры и т.д.). Все приложения Spring имеют этот контекст, который запускается при запуске приложения. Spring Boot создает этот контекст при запуске приложения;
Выполняется сканирование пути к классам (class path scan). Чтобы добавить код в Spring Boot, необходимо создать свои классы и аннотировать их определенным образом. Например, если вы хотите добавить контроллер, вы создаете класс и аннотируете его с помощью аннотации @Controller и так далее. То есть, вы как бы помечаете ваши классы, что это контроллер, это сервис, это еще что-то. Spring сканирует эти классы и, в зависимости от нашего маркера, он работает с этими классами по-разному. То есть Spring сканирует ваш код и ищет классы с этими аннотациями (помимо маркеров, обычно в аннотациях содержатся другие метаданные, которые дают уточняющую информацию для Spring);
Запускается Tomcat-сервер. Мы как раз зашли на сервер через URL и получили страницу с ошибкой, так как на сервере не был предусмотрен обработчик запроса с таким URL. Мы не скачивали Tomcat и не устанавливали его – все за нас сделал Spring Boot.
Структура enterprise-приложения Spring
Простое приложение Spring имеет трехслойную структуру:
Weblayer – верхний слой приложения. Он отвечает за обработку ввода пользователя и возврат корректного ответа. Также веб-слой отвечает за обработку исключений, которые могут выбрасываться в других слоях приложения. Так как веб-слой является точкой входа в приложение, он также отвечает за аутентификацию и является первой линией защиты приложения;
Servicelayer – слой сервисов, находится ниже веб-слоя. Этот слой содержит сервисы приложения и инфраструктуры. Сервисы приложения предоставляют публичный API сервисного слоя. Они также отвечают за транзакции и авторизацию. Инфраструктурные сервисы содержат код для взаимодействия с внешними ресурсами, такими как файловая система, базы данных, почтовые сервера и так далее. Часто эти сервисы используются несколькими сервисами приложения;
Repositorylayer – самый нижний слой приложения. Он отвечает за взаимодействие с используемыми хранилищами данных.
Лекция 6
Тема: Интеграция с СУБД. JDBC и JPA. Технология ORM. Фреймворк Hibernate. Сущности и репозитории. Реализация связей один к одному, один ко многим, многие ко многим.
В Spring имеется набор модулей для интеграции с различными технологиями хранения данных. Spring позволяет избавить разработчика от рутины при разработке программного кода, реализующего доступ к данным. Вместо возни с низкоуровневым доступом к данным можно положиться на Spring, который выполнит эту работу за вас, и сконцентрироваться на управлении данными в самом приложении.
Что такоеJDBC, драйвер,JPA,ORMи как это все между собой соотносится?
Как правило, каждая система управления базами данных (MySQL, PostgreSQL и так далее) имеет свой протокол взаимодействия с клиентами. Чтобы работать с базой данных, клиент должен соблюдать протокол взаимодействия с базой данных.
Чтобы программист не тратил время на самостоятельную реализацию протокола при разработке очередного приложения, разработчик сервера баз данных сам предоставляет всем желающим программный код, который общается с базой данных на понятном этой базе протоколе. Такой программный код и называется драйвером базы данных. Драйвер реализует протокол общения с БД и предоставляет API, которое позволяет нам общаться с базой данных, не вдаваясь в детали реализации протокола.
Как раз для этого разработчики Java предоставили стандарт JDBC (Java DataBase Connectivity) – специальное API, которое используется приложениями Java для взаимодействия с базой данных. Стандарт JDBC позволяет отправлять запросы к базе данных для выполнения операций выбора, вставки, обновления и удаления.
Если разработчики СУБД хотят, чтобы их база данных использовалась Java-разработчиками, они предоставляют JDBC-драйвер для их базы данных. Разработчики Java подключат драйвер и используют его для общения с той или иной базой данных. Если, в какой-то момент, разработчики захотят сменить СУБД, они просто меняют драйвер старой базы на драйвер новой. Благодаря стандарту JDBC, ничего менять в коде работы с базой данных не требуется.
Что такое и зачем нужна технологияORM?
При написании объектно-ориентированного кода, который взаимодействует с базой данных, у разработчика возникает несколько проблем:
данные в программе и в базе данных используют разные парадигмы (объектно-ориентированная и реляционная соответственно). Работу по преобразованию данных из одной парадигмы в другую ложатся на плечи программиста, что влечет за собой лишнюю работу и может приводить к ошибкам в процессе преобразования;
программисту желательно абстрагироваться от конкретной схемы хранения данных. То есть, программисту желательно работать не с реляционной базой данных, а просто с некоторым «хранилищем», а конкретная реализация этого «хранилища» может быстро и безболезненно меняться.
Для устранения этих проблем используется технология ORM(Object-Relational Mapping, «объектно-реляционное отображение») — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных».
Проще говоря, ORM– это прослойка, посредник между базой данных и объектным кодом. Используя ORM, программист не занимается формированием SQL-запросов и не думает в терминах «таблица», «записи» и «реляционные отношения», а просто работает с «хранилищем объектов» – он может туда записывать и получать объекты, не заботясь о подробностях их хранения.
В Java предусмотрен специальный стандарт JPA (Java Persistence API), который использует концепцию ORM. Существует несколько реализаций этого интерфейса, например, Hibernate, OpenJPA, EclipseLink и другие.
SpringDataJPA – обертка над JPA в Spring, которая предоставляет много полезных «фишек» разработчику. Она позволяет легче создавать Spring-управляемые приложения, которые используют новые способы доступа к данным, например нереляционные базы данных, map-reduce фреймворки, cloud сервисы, а так же уже хорошо улучшенную поддержку реляционных баз данных.
ТерминологияJPA
Основное понятие JPA – сущность (Entity). Сущность – это Java-класс, который представляет бизнес-логику приложения и определяет данные, которые будут храниться в базе данных и извлекаться из нее.
Как правило, класс сущности представляет таблицу в базе данных, поля или свойства класса представляют собой колонки в таблице, а объект сущности представляет собой одну запись в таблице.
Важным моментом при работе с JPA являются аннотации, коих здесь будет очень много. Разберемся с некоторыми из них:
@Entity – позволяет серверу узнать, что это не просто какой-то класс, а сущность;
@Id – помечает первичный ключ в таблице. Вопрос составных ключей в данном занятии не рассматривается;
@Table – позволяет настраивать отображение класса в таблицу. В данном случае, мы можем указать, какое имя будет иметь соответствующая таблица в базе данных;
@GeneratedValue – указывает, что данное поле является генерируемым значением. Очень часто этой аннотацией помечают первичные ключи, чтобы они генерировались автоматически при добавлении новых записей в таблицу;
@Column – позволяет настраивать отображение колонки в таблице. В данном случае, мы можем указать, какое имя будет иметь соответствующая колонка в таблицу.
Репозитории. Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.
В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.
Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.
Практическая часть
Установим СУБД Postgres и запустим pgAdmin 4.
Создадим пользователя ejournal_user, после чего создадим базу данных для нашего приложения.
Добавляем в pom.xml зависимости для работы с Spring Data JPA и JDBC драйвер для Postgres.
Далее необходимо настроить подключение к СУБД и нужной базе данных.
Для настройки приложения Spring воспользуемся языком YAML. Для этого удалим файл resources/application.properties и создадим вместо него файл application.yml.
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private int age;
}
Для уменьшения количества кода, мы будем использовать плагин Lombok.
Проект Lombok — это плагин компилятора, который добавляет в Java новые «ключевые слова» и превращает аннотации в Java-код, уменьшая усилия на разработку и обеспечивая некоторую дополнительную функциональность.
Lombok преобразует аннотации в исходном коде в Java-операторы до того, как компилятор их обработает: зависимость lombok отсутствует в рантайме, поэтому использование плагина не увеличит размер сборки.
При использовании Lombok наш исходный код не будет валидным кодом Java. Поэтому потребуется установить плагин для IDE, иначе среда разработки не поймёт, с чем имеет дело. Lombok поддерживает все основные Java IDE. Интеграция бесшовная. Все функции вроде «показать использования» и «перейти к реализации» продолжают работать как и раньше, перемещая вас к соответствующему полю/классу.
Вернемся в класс Student, добавим аннотацию для геттеров, сеттеров, а также конструктор со всеми параметрами.
Student.java
@Entity
@Table(name = "students")
@Data
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private int age;
}
Программирование слоя сервисов
Service – это Java класс, который содержит в себе основную бизнес-логику. В основном сервис использует готовые DAO/Repositories или же другие сервисы, для того чтобы предоставить конечные данные для пользовательского интерфейса. Сервисы, как правило, вызываются контроллерами или другими сервисами.
Service.java
@org.springframework.stereotype.Service
public class Service {
public void addStudent(Student student, int id) {
// Добавление нового студента
}
public List<Student> getAllStudents() {
// Получение списка студентов
}
}
Объект службы создается контейнером Spring, каждая служба является «одиночкой» (синглтоном), который создается в момент запуска приложения и уничтожается в момент закрытия приложения. Обратите внимание на аннотацию @Service. Этой аннотацией мы сообщаем контейнеру Spring, что это не просто класс, а класс сервиса.
Итак, мы создали службу, у которой есть два публичных метода. Первый метод добавляет нового студента, второй метод возвращает список всех студентов. В дальнейшем служба будет обращаться к объекту репозитория за данными, а пока что оставим код таким, какой он есть.
Вернемся к созданию веб-слоя. Создадим класс контроллера, создадим две конечные точки: для добавления студента и для получения списка всех студентов.
Controller.java
@RestController
public class Controller {
@Autowired
private Service service;
@PostMapping("/student")
public void addStudent(@RequestBody Student student) {
service.addStudent(student);
}
@GetMapping("/student")
public List<Student> getAllStudents() {
return service.getAllStudents();
}
}
Обратите внимание, что мы не создаем объект службы, а получаем его «извне» с помощью аннотации @Autowired. Контейнер Spring «внедрит» ссылку на объект службы в поле service. Подробнее про внедрение зависимостей будет изложено в следующем занятии.
@Entity
@Table(name = "groups")
@Data
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
@OneToMany(mappedBy = "group")
private List<Student> studentList;
}
Работа с репозиторием
Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.
В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.
Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.
StudentRepository.java
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
Теперь перейдем в класс службы и создадим ссылку на объект репозитория.
Service.java
@org.springframework.stereotype.Service
public class Service {
@Autowired
private StudentRepository studentRepo;
public void addStudent(Student student) {
studentRepo.save(student);
}
public List<Student> getAllStudents() {
return studentRepo.findAll();
}
}
Обратите внимание, что мы не создавали класс, который реализует интерфейс StudentRepository, тогда откуда мы его получим объект интерфейсного типа? Дело в том, что Springсгенерирует класс за нас. Этот сгенерированный класс будет иметь набор стандартных операций для работы с сущностями. В нашем случае, это операция findAll(), которая возвращает все сущности в таблице student.
Запустим сервер и выполним два клиентских запроса - один на создание студента, второй - на получение списка всех студентов.
Добавляем нового студента
Теперь получим список всех студентов.
Реализация отношения "один-ко-многим"
Как мы знаем, важной составляющей реляционных баз данных является отношения между таблицами "один-к-одному", "один-ко-многим", "многие-ко-многим".
Реализуем отношение "один-ко-многим". Создадим сущность Group - студенческая группа. В студенческой группе может быть от 0 до N студентов.
Прежде всего перейдем в сущность Student. Добавим поле group, который будет ссылаться на студенческую группу, в которой будет состоять студент. Так как в группе может быть много студентов, указываем аннотацию @ManyToOne. Также указываем аннотацию @JoinColumn, которая указывает на имя колонки, которая будет содержать Foreign Key.
Технология ORM позволяет создавать двусторонние связи между таблицами. В этом случае, при выдаче JSON, может возникнуть бесконечный цикл. Чтобы его избежать, укажем аннотацию @JsonIgnore. В этом случае, колонка group будет проигнорирована в процессе сериализации\десериализации.
Student.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstname;
private String lastname;
private int age;
@ManyToOne (fetch = FetchType.LAZY,optional = false)
@JoinColumn(name = "group_id",nullable = false)
@JsonIgnore
private Group group;
}
Далее создадим сущность Group.
Group.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "groups")
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(mappedBy = "group", cascade = CascadeType.ALL)
private List<Student> students;
public void addStudent(Student student) {
students.add(student);
}
}
Обратите внимание, что отношение один-ко-многим мы моделируем с помощью обычной коллекции. Указываем аннотацию @OneToMany, также в свойстве mappedBy указываем, какое поле "держит" отношение со стороны студента.
Далее модифицируем класс контроллера. Создадим конечные точки для добавления новой группы, а также для получения списка всех групп. Также модифицируем конечную точку для добавления студента, чтобы указать id группы, в которую необходимо добавить студента.
Controller.java
@RestController
public class Controller {
@Autowired
private Service service;
@PostMapping("/student/{group_id}")
public void addStudent(@RequestBody Student student, @PathVariable(name = "group_id") int group_id) {
service.addStudent(student, group_id);
}
@GetMapping("/student")
public List<Student> getAllStudents() {
return service.getAllStudents();
}
@PostMapping("/group")
public void addGroup(@RequestBody Group group) {
service.addGroup(group);
}
@GetMapping("/group")
public List<Group> getAllGroups() {
return service.getAllGroups();
}
}
Теперь создадим репозиторий для сущности Group.
GroupRepository.java
public interface GroupRepository extends JpaRepository<Group, Integer> {}
Далее модифицируем класс сервиса. Добавим методы для добавления новой группы, а также для получения списка всех групп. Также модифицируем метод добавления новой группы. Метод работает следующим образом: получаем объект группы по id, после чего добавляем ссылку на группу в поле group объекта Student.
Service.java
@org.springframework.stereotype.Service
public class Service {
@Autowired
private StudentRepository studentRepo;
@Autowired
private GroupRepository groupRepo;
public void addStudent(Student student, int id) {
Group g = groupRepo.getOne(id);
student.setGroup(g);
studentRepo.save(student);
}
public List<Student> getAllStudents() {
return studentRepo.findAll();
}
public void addGroup(Group group) {
groupRepo.saveAndFlush(group);
}
public List<Group> getAllGroups() {
return groupRepo.findAll();
}
}
Запустим приложение и проверим его работу. Сначала добавим группу, после чего получим список групп.
Добавим новую группу
Получим список групп
Теперь добавим нового студента
Получим список всех групп
Абстрактные классы и интерфейсы в UML
Абстрактный класс
Абстрактный класс - это класс, который нельзя реализовать непосредственно. Вместо этого создается экземпляр подкласса. Обычно абстрактный класс имеет одну или более абстрактных операций. У абстрактной операции нет реализации, это чистое объявление, которые клиенты могут привязать к абстрактному классу.
Наиболее распространенным способом обозначения абстрактного класса или операции в UML является написание их имен курсивом. Можно также сделать свойства абстрактными, определяя абстрактное свойство или методы доступа. Если курсив сложно изобразить (например, если вы рисуете на доске), можно прибегнуть к метке {abstract}.
Для указания класса как абстрактного, выделите класс и выделите чекбокс isAbstract.
Для указания метода как абстрактного, выделите метод и выделите чекбокс isAbstract.
Интерфейс
Интерфейс - это класс, не имеющий реализации, то есть вся его функциональность абстрактна. Интерфейсы прямо соответствуют интерфейсам в Java или C# и являются общей идиомой в других типизированных языках. Интерфейс обозначается стереотипом (ключевым словом) <<interface>>.
Обозначение интерфейса в StarUML
По умолчанию, при добавлении интерфейса на диаграмму классов, он выглядит как кружок с названием интерфейса.
Для того, чтобы преобразовать его в классический интерфейс согласно стандарту UML, выделите интерфейс и в панели Editors выберите Format -> Label.
После этого снимите выделение с пункта "Suppress Operations"
Пример использования абстрактного класса и интерфейса
Графическое представление в формате UML
Исходный код
interface MyInterface{
void interfaceMethod();
}
abstract class MyAbstractClass implements MyInterface {
abstract void abstractMethod();
}
class MyClass extends MyAbstractClass {
@Override
void abstractMethod() {}
@Override
public void interfaceMethod() {}
}
Лекция 1-2
Тема: Принцип инверсии управления и внедрение зависимостей. Использование Spring Core для внедрения зависимостей. Spring IoC-контейнер. Виды связывания (wiring). Разрешение зависимостей. Классы JavaBeans.
Любое мало-мальски серьезное приложение состоит из нескольких классов, которые взаимодействуют друг с другом, чтобы реализовывать бизнес-логику. Обычно, каждый объект отвечает за получение ссылок на другие объекты, с которыми он взаимодействует (такие другие объекты называются зависимостями, dependencies). Такой подход может привести к созданию тесно связанного кода, который тяжело тестировать.
Рассмотрим небольшой участок кода, который состоит из класса User и класса Sender.
public class User {
public void sendMessage(String message, String target) {
Sender sender = new Sender();
sender.send(message, target);
}
}
public class Sender {
public void send(String message, String target) {
System.out.println("Tweet: " + message + " to " + target);
}
}
В результате мы получим тесно связанный код – класс User теперь напрямую зависит от класса Sender. Таким образом, если мы создадим класс EmailSender, который будет отсылать сообщения по электронной почте, то чтобы использовать объект класса EmailSender, нам придется изменять код класса User. К тому же, тестирование метода sendMessage() будет затруднительным.
Безусловно, мы не можем избежать связывания вообще, т.к. объектно-ориентированное программирование подразумевает взаимодействие множества объектов различных классов, программа из одного класса не имеет смысла. С другой стороны, нам необходимо избегать тесного связывания (tight coupling) классов, так как такой код тяжело повторно использовать, тестировать и тяжело понять, как это всё вместе работает.
В противовес тесному связыванию кода существует принцип слабо связного (loose coupling) кода. Слабая связность означает, что изменения, вносимые в один класс, повлекут за собой небольшие изменения в другие классы, что упростит тестирование, рефакторинг, повторное использование кода. Приложение с использованием принципа слабо связного кода легче модифицируется и поддерживается.
Инверсия контроля
Одним из приемов для написания слабо связного кода является принцип инверсии управления (Inversion of Control, IoC). Он заключается в том, что жизненным циклом (созданием, вызовом методов и уничтожением) ваших объектов управляете не вы сами, а некий сторонний код. Отсюда и термин «инверсия» – не я управляю кодом, а сторонний код управляет моими классами. Он решает, когда создавать объекты моих классов, когда вызывать их методы и когда уничтожать объекты.
На принципе инверсии управления базируется работа всех фреймворков.
Отличие библиотеки от фреймворка состоит в том, что библиотека – это по существу набор функций, организованных в классы, которые вы можете вызывать по мере надобности. Каждый вызов выполняет некоторую работу и возвращает управление обратно пользователю.
С другой стороны, фреймворк воплощает в себе некоторый абстрактный дизайн приложения со своим поведением. Для того, чтобы использовать его, вы должны добавить свой код в различные места фреймворка, либо через наследование, либо подключив свой собственный класс. Код фреймворка впоследствии будет вызывать ваш код.
Внедрение зависимости
Одной из реализаций принципа инверсии управления является внедрение зависимости (Dependency Injection, DI). Это принцип заключается в том, что зависимости класса не создаются или ищутся в самом классе, а внедряются (inject) извне некоторым другим внешним источником (например, каким-то другим объектом). В статье Мартина Фаулера «Inversion of Control Containers and the Dependency Injection pattern» этот объект называется «сборщиком» (an assembler), а сейчас его обычно называют контейнером (container) или IoC-контейнером (IoC-container).
В общем случае, IoC-контейнер – это некоторый программный код (фреймворк, отдельный класс), который осуществляет внедрение зависимостей в приложении и, насколько это возможно, упрощает данный процесс.
Как правило, внедрение зависимости осуществляется через:
конструктор класса (constructor injection);
поле класса (field injection);
входной аргумент метода (method injection), то есть через сеттер.
Внедрение через статические поля и методы не рекомендуется.
Фреймворк Spring, прежде чем стать многофункциональной платформой, изначально разрабатывался как IoC-контейнер для упрощения разработки JavaEE-приложений.
В приложениях на основе фреймворка Spring прикладные объекты располагаются внутри контейнера Spring. Как показано на рисунке, контейнер создает объекты, связывает их друг с другом, конфигурирует и управляет их полным жизненным циклом, от зарождения до самой их смерти (или от оператора new до вызова метода finalize()).
Классы, которыми управляет Spring-контейнер, называются бинами (bean) или компонентами. Контейнер создает, связывает между собой, а также уничтожает бины.
Фреймворк Spring имеет не один контейнер. В его состав входят несколько реализаций контейнера, которые подразделяются на два разных типа.
Фабрики компонентов (bean factories) (определяются интерфейсом org.springframework.beans.factory.BeanFactory) – самые простые из контейнеров, обеспечивающие базовую поддержку DI.
Контекст приложений (application contexts) (определяется интерфейсом org.springframework.context.ApplicationContext) основан на понятии фабрик компонентов и реализует прикладные службы фреймворка, такие как возможность приема текстовых сообщений из файлов свойств и возможность подписывать другие программные компоненты на события, возникающие в приложении.
С фреймворком Spring можно работать, используя и фабрики компонентов, и контексты приложений, но для большинства приложений фабрики компонентов часто оказываются слишком низкоуровневым инструментом. Поэтому контексты приложений выглядят более предпочтительно, чем фабрики компонентов.
В составе Spring имеется несколько разновидностей контекстов приложений. Три из них используются наиболее часто:
ClassPathXmlApplicationContext – загружает определение контекста из XML-файла, расположенного в библиотеке классов (classpath), и обрабатывает файлы с определениями контекстов как ресурсы;
FileSystemXmlApplicationContext – загружает определение контекста из XML-файла в файловой системе;
XmlWebApplicationContext – загружает определение контекста из XML-файла, содержащегося внутри веб-приложения.
Давайте перепишем наш код, чтобы подготовить его к использованию IoC-контейнера Spring. Руководствуясь принципом Dependency Inversion (не путать с Dependency Injection, это разные принципы), создадим интерфейс Sender, чтобы не привязываться к конкретной реализации отправщика сообщений.
public interface Sender {
void sendMessage(String message, String target);
}
Создадим класс TwitterSender, который реализует данный интерфейс.
public class TwitterSender implements Sender {
public void sendMessage(String message, String target) {
System.out.println("Tweet: " + message + " is sending to " + target);
}
}
Модифицируем класс User
public class User {
private Sender sender;
public User(Sender sender) {
this.sender = sender;
}
public void setSender(Sender sender) {
this.sender = sender;
}
public void send(String message, String target) throws NullPointerException {
if (sender != null) {
sender.sendMessage(message, target);
} else {
throw new NullPointerException("Sender object is null");
}
}
}
Обратите внимание на разницу – мы теперь не сами создаем объект зависимости, а получаем его «извне» с помощью аргумента конструктора либо с помощью сеттера. Использование интерфейса позволяет легко использовать разные реализации отправщик сообщений. Еще одним бонусом является удобство проведения тестирования методов класса User, так как вместо настоящего отправщика сообщений мы можем подставить специальный мок-объект (mock object), который будет имитировать работу настоящего отправщика.
Внедрение зависимости (wiring)
Итак, у нас есть соответствующие классы, теперь необходимо связывать это все воедино с помощью IoC-контейнера. Каким образом передать объект TwitterSender объекту User?
Процесс создания связи между компонентами приложения обычно называют wiring (в русской версии книги Spring in Action этот термин переводят как связывание, не путайте с сильным и слабым связыванием, которое переводится как tight coupling и loose coupling).
Подключим библиотеки Spring, которые нужны для связывания компонентов. Если вы используете Maven в качестве сборщика, от откройте pom-файл и добавьте следующие зависимости (на момент проведения занятия актуальная версия библиотек была 5.0.7, в вашем случае актуальная версия может быть другой)
Важное отступление. Вы можете не добавлять библиотеку spring-core в pom-файл явно, код все равно будет работать. Это связано с тем, что spring-context не может работать без spring-core и Maven автоматически загрузит spring-core в любом случае, укажете ли вы ее в pom-файле или нет. В этом случае библиотека spring-core называется транзитивной зависимостью.
Транзитивная зависимость - это зависимость, которая требуется для работы вашей прямой зависимости.
Такой механизм позволяет избежать ручного добавления в pom-файл всего графа зависимостей - вы просто указываете прямые зависимости, а Maven сделает все остальное.
Итак, вернемся к связыванию компонентов.
Важный момент, который необходимо запомнить при работе с контейнером - любой контейнер необходимо сконфигурировать. То есть, на плечи разработчика ложится обязанность указать контейнеру, какие компоненты создать и как их связать вместе.
Spring предлагает три способа связывания компонентов:
явная конфигурация с помощью XML-файлов;
явная конфигурация с помощью классов Java;
неявное обнаружение бинов и автоматическое связывание.
В данном случае нет "самого лучшего" способа связывания, все три способа имеют право на жизнь. В данном занятии мы рассмотрим конфигурацию с помощью классов Java и автоматическое связывание.
Конфигурация с помощью классов Java
Для начала создадим класс, в котором будет осуществляться конфигурация. Создадим пакет config и класс AppConfig. Так как в Spring может использоваться несколько способов связывания компонентов, то желательно пометить класс аннотацией @Configuration - такая аннотация говорит контейнеру, что этот класс является классом конфигурации.
@Configuration
public class AppConfig {...}
Конфигурация в классе осуществляется с помощью методов и аннотаций. Добавим в класс следующий метод
@Configuration
public class AppConfig {
@Bean
public TwitterSender twitterSender() {
return new TwitterSender();
}
}
Пометив метод аннотацией @Bean, мы говорим что данный метод возвращает объект, который который должен быть зарегистрирован как бин в контексте приложения Spring (то есть, в нашем IoC-контейнере). Таким образом, мы фактически объявляем бин в нашем контейнере. Название бина будет совпадать с названием метода, в нашем случа бин будет называться twitterSender.
Теперь добавим еще один метод
@Configuration
public class AppConfig {
@Bean
public User user() {
return new User(twitterSender());
}
@Bean
public TwitterSender twitterSender() {
return new TwitterSender();
}
}
Объявляем еще один бин User и в методе осуществляем связывание бинов. В нашем случае мы осуществляем связывание через конструктор (constructor injection).
Таким образом, мы объявили два бина - twitterSender и user, после чего связали их с помощью constructor injection.
Теперь модифицируем класс Main, создадим контейнер и попробуем использовать класс User.
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(AppConfig.class);
User user = context.getBean(User.class);
user.send("Hello!", "Nick");
}
}
Итак, сначала мы создали объект контейнера. В качестве реализации мы используем класc AnnotationConfigApplicationContext, который является реализацией интерфейса ApplicationContext, которая позволяет регистрировать аннотированные классы конфигурации. В нашем случае классом конфигурации является класс AppConfig, объявленный с помощью аннотации @Configuration. После того как вы зарегистрируете указанный класс, также регистрируются все типы bean-компонентов, возвращаемые с помощью методов, которые аннотируются с помощью @Bean.
После создания контейнера и загрузки конфигурации, используем класс User. Обратите внимание, что мы не сами создаем объект класса User и внедряем зависимости, а мы просто получаем объект из контейнера, с помощью метода getBean(). После того, как мы получили ссылку на объект, вызываем метод send() и получаем работающий класс User. Проверим работу приложения.
июл 01, 2018 2:37:09 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4534b60d: startup date [Sun Jul 01 14:37:09 EEST 2018]; root of context hierarchy
Tweet: Hello! is sending to Nick
Таким образом мы реализовали связывание бинов с помощью контейнера Spring и конфигурации с помощью Java-классов. Теперь давайте рассмотрим автоматическое связывание.
Автоматическое связывание
Способ автоматического связывания является наиболее простым в использовании.
Автоматическое связывание в Spring реализуется с помощью двух механизмов:
сканирование компонентов (component scanning) – механизм, с помощью которого Spring обнаруживает и создает экземпляры компонентов;
автосвязывание (autowiring) – механизм, с помощью которого Spring автоматически «удовлетворяет» зависимости компонентов (to satisfy a dependency).
Совместная работа этих механизмов обеспечивает минимальное явное конфигурирование контейнера.
Перепишем наш код для использования автоматического связывания. Для того, чтобы механизм сканирования компонентов обнаружил наши классы-бины, необходимо пометить их с помощью аннотации @Component.
@Component
public class TwitterSender implements Sender {...}
@Component
public class User {...}
Тот участок кода, где контейнеру необходимо осуществить внедрение зависимости, аннотируется с помощью аннотации @Autowired. В рамках данного примера мы решили, что внедрение зависимости происходит в методе (method injection). Обратите внимание, что это не обязательно должен быть сеттер, хотя это крайне желательно
@Component
public class User {
private Sender sender;
@Autowired
public void setSender(Sender sender) {
this.sender = sender;
}
}
Когда мы осуществляли конфигурацию с помощью Java-класса, мы явно указывали классы компонентов и явно создавали объекты бинов.
Однако Spring способен автоматически отсканировать пакеты проекта, обнаружить бины и создать их экземпляры. Этот механизм называется сканирование компонентов (component scanning). По умолчанию, механизм сканирования компонентов отключен. Чтобы его включить, вернемся в конфигурационный класс AppConfig и укажем аннотацию @ComponentScan перед объявлением класса.
@Configuration
@ComponentScan("app")
public class AppConfig {}
Прежде всего, удалим из класса AppConfig написанные ранее методы - они теперь не нужны.
Также обратите внимание, что в скобках я указал базовый пакет, где необходимо осуществить сканирование. Механизм сканирования компонентов будет искать компоненты в этом и в дочернем пакетах. Также вы можете указать параметр basePackages и перечислить пакеты для сканирования.
Запустим приложение и убедимся, что автоматическое связывание работает корректно.
июл 01, 2018 4:13:21 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:13:21 EEST 2018]; root of context hierarchy
Tweet: Hello! is sending to Nick
Разрешение зависимости (Dependency Resolution)
Использование автоматического связывания (связывание компонентов реализуется с помощью механизмов Spring) может привести к ситуации, когда будет существовать несколько бинов, которые могут быть использованы для связывания.
Пока что у нас был только один класс, который реализовывал интерфейс Sender. А что, если их будет два? Создадим класс EmailSender
@Component
public class EmailSender implements Sender {
public void sendMessage(String message, String target) {
System.out.println("Email: " + message + " to: " + target);
}
}
Запустим приложение
at app.Main.main(Main.java:28)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'app.model.Sender' available: expected single matching bean but found 2: emailSender,twitterSender
Сообщение при исключении четко описывает проблему: есть два бина, которые можно внедрить в класс User и Spring не знает, какой из них следует внедрить и закрывается с исключением.
Чтобы избавиться от данной проблемы, можно дать указания контейнеру, какой из компонентов следует выбрать в том или ином случае (ищите информацию по аннотации @Qualifier).
В нашем примере воспользуемся аннотацией @Conditional, чтобы решить проблему нескольких кандидатов на связыванеие.
Аннотация @Conditional перед объявлением класса бина означает, что бин будет доступен для регистрации в контейнере только, когда будет удовлетворено некоторое условие. В нашем случае, для каждого кандидата мы создадим отдельный класс - реализацию интерфейса Condition, в котором реализуем специальный метод. Если метод вернет true, значит условие выполнено и компонент можно зарегистрировать.
Прежде всего воспользуемся механизмом properties в Java. Создадим ресурс app.properties с содержимым
sender.type = email
В классе Main создадим объект Properties и загрузим файл
public class Main {
public static final Properties config = new Properties();
static {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try (InputStream resourceStream = loader.getResourceAsStream("app.properties")) {
config.load(resourceStream);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Теперь у нас есть публичное статическое поле config, в котором хранятся свойства.
Создадим классы условий
public class TwitterSenderCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return Main.config.getProperty("sender.type").matches("twitter");
}
}
public class EmailSenderCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return Main.config.getProperty("sender.type").matches("email");
}
}
В классах компонентов укажем аннотацию @Conditional и класс условия.
@Component
@Conditional(value = TwitterSenderCondition.class)
public class TwitterSender implements Sender {...}
@Component
@Conditional(value = EmailSenderCondition.class)
public class EmailSender implements Sender {...}
Теперь запустим приложение
июл 01, 2018 4:57:53 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:57:53 EEST 2018]; root of context hierarchy
Email: Hello! to: Nick
Если мы изменим в app.properties значение с email на twitter и снова запустим приложение, то получим
июл 01, 2018 4:58:56 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:58:56 EEST 2018]; root of context hierarchy
Tweet: Hello! is sending to Nick
Таким образом, проблема нескольких кандидатов решена.
Лекция 5
Тема: Устройство сети Интернет. Протокол HTTP\HTTPS. Веб-сервисы. Архитектурный стиль REST. Написание REST-контроллеров. Передача параметров в REST-запросах. Формат JSON.
Веб-сервисы
Всемирная паутина является готовой платформой для создания и использования распределенных систем на основе веб-служб. Веб-сервер выступает в качестве сервера приложений, к которым обращаются не конечные пользователи, а сторонние приложения. Это позволяет многократно использовать функциональные элементы, устранить дублирование кода, упростить решение задач интеграции приложений.
Веб-служба или веб-сервис (web-service) – сетевая технология, обеспечивающая межпрограммное взаимодействие на основе веб-стандартов. W3C определяет веб-службу как «программную систему, разработанную для поддержки интероперабельного межкомпьютерного (machine-to-machine) взаимодействия через сеть».
К моменту появления веб-служб уже существовали технологии, позволяющие приложениям взаимодействовать на расстоянии, где одна программа могла вызвать какой-нибудь другой метод в другой программе, которая при этом могла быть запущена на компьютере, расположенном в другом городе или даже стране. Это сокращенно называется RPC(RemoteProcedureCalling– удаленный вызов процедур). В качестве примеров можно привести технологии CORBA, а для Java – RMI (Remote Method Invoking – удаленный вызов методов).
Идея веб-службы заключалась в создании такого RPC, который будет упаковываться в HTTP пакеты. Такой подход стал очень популярным, т.к. HTTP был хорошо известен, прост, понятен и обеспечивал лучшее «прохождение» через различные firewall`ы. Именно с появлением веб-сервисов развилась идея SOA– сервис-ориентированной архитектуры веб-приложений (Service Oriented Architecture).
Протокол HTTP
Протокол HTTP лежит в основе обмена данными в Интернете. HTTP является протоколом клиент-серверного взаимодействия, что означает инициирование запросов к серверу самим получателем (браузером или другим клиентским приложением).
Клиенты и серверы взаимодействуют, обмениваясь одиночными сообщениями (а не потоком данных). Сообщения, отправленные клиентом называются запросами, а сообщения, отправленные сервером, называются ответами.
HTTP - это клиент-серверный протокол, то есть запросы отправляются какой-то одной стороной - участником обмена (user-agent). Чаще всего в качестве участника выступает веб-браузер, но им может быть кто угодно.
Каждый запрос (request) отправляется серверу, который обрабатывает его и возвращает ответ (response).
Участник обмена (user agent) - это любой инструмент или устройство, действующее от лица пользователя.
На другой стороне коммуникационного канала расположен сервер, который обслуживает (serve) пользователя, предоставляя ему документы по запросу. С точки зрения конечного пользователя, сервер всегда является некой одной виртуальной машиной, полностью или частично генерирующий документ, хотя фактически он может быть группой серверов, между которыми балансируется нагрузка, то есть перераспределяются запросы различных пользователей, либо сложным программным обеспечением, опрашивающим другие компьютеры.
HTTP-запросы (HTTP-request)
Пример HTTP запроса
Запросы содержат следующие элементы:
HTTP-метод, обычно глагол подобно GET, POST или существительное, как OPTIONS или HEAD, определяющее операцию, которую клиент хочет выполнить. Обычно, клиент хочет получить ресурс (используя GET) или передать значения HTML-формы (используя POST), хотя другие операции могут быть необходимы в других случаях;
путь к ресурсу;
заголовки (опционально), предоставляющие дополнительную информацию для сервера;
для некоторых методов, таких как POST, тело метода, которое содержит отправленный ресурс.
HTTP-ответы
Пример HTTP-ответа
Ответы содержат следующие элементы:
версию HTTP-протокола;
HTTP код состояния, сообщающий об успешности запроса или причине неудачи;
сообщение состояния - краткое описание кода состояния;
Код состояния - это трехзначное число, которое отдает сервер на запрос клиента и благодаря которому корректируется дальнейшая обработка запрашиваемого документа. За числом всегда идет краткое пояснение кода на английском языке, отделенное пробелом - первичная инструкция клиенту.
Классы состояния - группа кодов, объединенных определенными признаками. На класс состояния указывает первая цифра в коде.
Выделяют пять классов:
1ХХ - информационные кода. Они отвечают за процесс передачи данных. Это временные коды, они информируют о том, что запрос принят и обработка будет продолжаться;
2ХХ - успешная обработка. Запрос был получен и успешно обработан сервером;
3ХХ - перенаправление (редирект). Эти ответы сервера гласят, что нужно предпринять дальнейшие действия для выполнения запроса. Например, сделать запрос по другому адресу;
4ХХ - ошибка клиента. Это значит, что запрос не может быть выполнен на стороне клиента;
5ХХ - ошибка сервера. Эти коды возникают из-за ошибок на стороне сервера. В данном случае клиент сделал все правильно, но сервер не может выполнить запрос. Для кодов этого класса сервер обязательно показывает сообщение, что не может обработать запрос и по какой причине.
Протоколы веб-сервисов
На сегодняшний день наибольшее распространение получили следующие протоколы реализации веб-служб:
SOAP (Simple Object Access Protocol) – тройка стандартов SOAP/WSDL/UDDI. Сообщения упаковываются в виде структуры, которая называется конверт (envelope), которая включает идентификатор сообщения, заголовок и тело сообщения;
REST (Representational State Transfer) – архитектурны стиль, который использует концепцию ресурсов и определяет операции через методы HTTP-протокола;
XML-RPC (XML Remote Procedure Call) – вызов удаленных процедур, использующий XML для кодирования своих сообщений и HTTP в качестве транспортного механизма.
ПротоколREST
Передача состояния представления (Representational State Transfer (REST)) является архитектурным стилем, в котором веб-службы рассматриваются, как ресурсы и могут быть идентифицированы Унифицированными идентификаторами ресурсов (Uniform Resource Identifiers (URI)).
Веб-службы, разработанные в стиле REST и с учетом ограничений REST, известны как RESTfulвеб-службы.
Каждая единица информации в REST называется ресурсом и имеет однозначный URI, который является ее, своего рода, первичным ключом. То есть, например, третья книга с книжной полки будет иметь URI /book/3, а 35ая страница в этой книге – /book/3/page/35/. Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /book/3/page/35/ – это может быть и HTML, и отсканированная копия книги в виде jpeg-файла и документ Microsoft Word.
Над ресурсами выполняется ряд простых четко определенных операций. В качестве протокола передачи данных используется stateless-протокол, обычно HTTP.
При использовании протокола HTTP действия над данными выполняются с помощью HTTP-методов: GET (получить), PUT (добавить, заменить), POST (добавить, изменить, удалить), DELETE (удалить). Таким образом, действия CRUD (Create-Read-Update-Delete) могут выполняться как со всеми 4-мя методами, так и только с помощью GET и POST. Примеры запросов:
GET /book/ – получить список всех книг;
GET /book/3 – получить книгу номер 3;
PUT /book/ – добавить книгу (данные в теле запроса);
POST /book/3 – изменить книгу (данные в теле запроса);
DELETE /book/3 – удалить книгу.
Как правило, необязательно поддерживать все методы, но, как правило, веб-служба должна поддерживать:
GET – используется для получения существующих ресурсов;
POST – используется для создания/обновления нового ресурса;
PUT – используется для обновления/замены ресурса;
DELETE – используется для удаления ресурса.
Кроме этого, служба может поддерживать такие методы как PATCH (обновление части ресурса), HEAD (возвращение заголовка ресурса, т.е. метаданных) и т.д.
Разработка Web Layer
Для обработки запросов и возврата данных необходимо предусмотреть соответствующие контроллеры REST-запросов, которые и будут составлять наш веб-слой.
Контроллер – это java-класс, методы которого призваны обрабатывать HTTP-запросы. Отличие обычного контроллера от REST-контроллера заключается в том, что в REST-контроллере каждый метод класса возвращает данные вместо представления. Рассмотрим пример простого REST-контроллера. Создадим в проекте пакет controllers, внутри которого создадим класс HelloController.
HelloController.java
@RestController
public class HelloController {}
Обратите внимание, что мы пометили класс аннотацией @RestController. Таким образом, мы даем знать Spring, что это не просто класс, а контроллер REST-запросов. В классе создадим метод, который будет возвращать строку.
HelloController.java
@RestController
public class HelloController {
public String hello() {
return "hello";
}
}
Говорят, что методы контроллера «отображаются» на HTTP-запросы. Это значит, что при поступлении определенного HTTP-запроса (с определенным URL и HTTP-методом), будет вызван определенный метод контроллера, который вернет некоторые данные. Этим данные будут упакованы в HTTP-ответ и высланы обратно клиенту.
Нам необходимо сделать так, чтобы наш созданный метод был вызван, когда на сервер поступит HTTP-запрос с определенным URL, например http://localhost:8080/hello. Для этого необходимо пометить метод аннотацией @GetMapping c параметром (“/hello”) – часть URL, на который будет отображаться данный метод.
HelloController.java
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
Для каждого из четырех основных HTTP-метода предусмотрена своя аннотация (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping). Метод, помеченный определенной аннотацией, обрабатывает запросы только с определенным HTTP-методом.
Что произошло? Строка «hello» была помещена в тело HTTP-ответа, браузер получил text/plain с содержимым «hello» и просто вывел его на экран.
Передача параметров в REST-запросах
Очень часто клиенту необходимо вместе с запросом передать некоторые параметры запроса, которые уточняют и конкретизируют запрос.
Параметры запроса можно передать несколькими способами. Рассмотрим следующие способы:
указание параметра в URL-пути (localhost:8080/rooms/256);
указание параметра в строке запроса, которая идет после URL-пути и отделяется символом ? (localhost:8080/rooms?id=256¶m2=value2);
передача параметров в теле запроса (часто используется для передачи заполненной пользователем формы или передачи данных в формате JSON).
Рассмотрим, каким образом можно получить и обработать параметры запроса, переданные тем или иным способом.
Указание параметра в URL-пути
При создании endpoint, в аннотации необходимо указать вариативную часть и назначить ей идентификатор
@GetMapping("/room/{id}")
public void getRoomById() {
}
Далее необходимо предусмотреть входной аргумент метода, куда Spring запишет значение вариативной части и указать аннотацию @PathVariable для этой переменной. Также необходимо указать идентификатор, который вы указали в аннотации @GetMapping.
@GetMapping("/room/{id}")
public void getRoomById(@PathVariable(value = "id") int roomId) {
// ...
}
В рамках одного запроса может быть несколько вариативных частей, которые можно считать и обработать
@GetMapping("/room/{id1}/{id2}")
public void getRoomById(@PathVariable(value = "id1") int blockId, @PathVariable(value = "id2") int roomId) {
// ...
}
Указание параметра в строке запроса
В этом случае, для каждого параметра запроса создается входной аргумент, указывается аннотация @RequestParam, а также указывается имя параметра.
// запрос: http://localhost:8080/room?room_id=250&block_id=10
@GetMapping("/room")
public void getRoomById(@RequestParam(value = "room_id") int roomId, @RequestParam(value = "block_id") int blockId) {
// ...
}
Передача параметров в теле запроса
Если в качестве клиента выступает браузер пользователя, данные от клиента на сервер передаются в виде полей формы, которые заполняет пользователь браузера. В этом случае параметры передаются в теле запроса с помощью метода POST.
Форма может иметь следующие MIME-типы:
multipart/form-data: каждое значение посылается как блок данных ("body part"), с заданными пользовательским клиентом разделителем ("boundary"), разделяющим каждую часть. Эти ключи даются в заголовки Content-Disposition каждой части text/plain.
Для обработки данных формы необходимо создать входной аргумент для каждого параметра, для каждого входного аргумента указать аннотацию @RequestParam, а также имя параметра.
@PostMapping("/book")
public void bookRoom(@RequestParam(value = "room_id") int room_id,
@RequestParam(value = "firstname") String firstname,
@RequestParam(value = "lastname") String lastname,
@RequestParam(value = "days") int days) {
// ...
}
Существует несколько более простых способов получения данных формы, но в данном курсе они не рассматриваются. Вышеуказанный способ является самым простым и понятным на данном этапе изучения курса.
Передача данных между клиентом и сервером.
Так как язык Java является ОО языком, нам было бы удобно работать с входящими и исходящими данными в объектном виде - было бы здорово, если бы REST-контроллер возвращал бы данные в виде объекта некоторого класса, а не в виде набора полей со значениями. Также было бы здорово, чтобы мы могли просто возвращать клиенту объект или коллекцию объектов некоторых классов без необходимости формировать Map из полей и значений.
Для реализации этого функционала, в Spring используется механизм сериализации и десериализации.
Сериализация - это преобразование объекта в последовательность байтов, так что объект можно легко сохранить в постоянное хранилище или передать по каналу связи. Затем поток байтов можно десериализовать - преобразовать в реплику исходного объекта.
Язык Java предоставляет стандартный механизм Java Serialization API для создания сериализуемых объектов, однако, он нам не подходит, так как ограничивает возможности для использования различных языков и технологий на стороне клиента и сервера.
Мы можем использовать сторонние библиотеки для сериализации объекта с помощью формата XML или JSON.
Библиотека Jackson позволяет гибко настроить процесс сериализаци и десериализации, однако, в рамках данного курса мы будем использовать стандартные механизмы сериализации и десериализации, чтобы уделять этому процессу как можно меньше внимания.
Рассмотрим ситуацию, когда нам необходимо вернуть клиенту данные в объектном виде. Создадим класс с несколькими полями, создадим объект и вернем его в качестве результата GET-запроса.
@GetMapping("/roominfo")
public Room getRoomInfoById(@RequestParam(value = "room_id") int roomId) {
return new Room(roomId, "Отличная комната!", 2, 200);
}
Room.java
public class Room {
private Integer id;
private String roomInfo;
private Integer roomCapacity;
private double price;
public Room(Integer id, String roomInfo, Integer roomCapacity, double price) {
this.id = id;
this.roomInfo = roomInfo;
this.roomCapacity = roomCapacity;
this.price = price;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRoomInfo() {
return roomInfo;
}
public void setRoomInfo(String roomInfo) {
this.roomInfo = roomInfo;
}
public Integer getRoomCapacity() {
return roomCapacity;
}
public void setRoomCapacity(Integer roomCapacity) {
this.roomCapacity = roomCapacity;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
Обратите внимание, что в код класса Room не зря были включены геттеры и сеттеры. Их наличие обязательно для сериализации и десериализации!
Используем Postman для эмуляции клиента, сделаем GET-запрос и получим следующий результат
Как мы видим, поля объекта были сериализованы с помощью формата JSON. Теперь клиент, после получения этих данных, сможет с помощью процесса десериализации получить объект и удобно работать с ним.
Теперь рассмотрим обратную ситуацию. Клиент делает POST-запрос и передает в теле запроса данные о новом студенте.
На стороне сервера создаем класс Student с соответствующими полями.
Student.java
public class Student {
private String firstname;
private String lastname;
private String phone;
private int age;
public Student(String firstname, String lastname, String phone, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.phone = phone;
this.age = age;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"firstname='" + firstname + '\'' +
", lastname='" + lastname + '\'' +
", phone='" + phone + '\'' +
", age=" + age +
'}';
}
}
Создаем конечную точку для обработки запроса. Обратите внимание, что мы используем аннотацию @RequestBody.
@PostMapping("/student")
public void addStudent(@RequestBody Student student) {
System.out.println(student);
}
Конфигурация HTTP-response с помощью класса ResponseEntity
Далеко не всегда ответ сервера состоит в возврате какого-то значения или какого-то объекта. Очень часто необходимо вернуть ответ с определенным HTTP-кодом и сообщением об ошибке, указать определенный заголовок и так далее.
В этом случае необходимо использовать класс ResponseEntity. Класс ResponseEntity является оберткой для ответа и дополнительно для HTTP заголовков и кода статуса. Он является обобщенным, что позволяет использовать любой тип в качестве тела ответа.
@GetMapping("/student")
public ResponseEntity<?> getStudentById(@RequestParam(value = "id") int studentId) {
if (studentId < 1) {
return ResponseEntity.badRequest().body("Invalid id");
} else {
return ResponseEntity.ok(new Student("Ivan", "Ivanov", "223322", 20));
}
}
Задание на лабораторную работу
Задания для выполнения:
1. Перегрузка методов
Вернитесь к классу TimeSpan из второй лабораторной работы.
Используя механизм перегрузки методов, допишите или исправьте методы так, чтобы класс имел следующий функционал:
конструктор без аргументов (в этом случае временной интервал равен 0 часов и 0 минут);
конструктор с 1 аргументом (минуты);
конструктор с 2 аргументами (часы и минуты);
конструктор с 1 аргументом типа TimeSpan (из входящего TimeSpan считываются часы и минуты для создания нового TimeSpan);
метод добавления времени ко временному интервалу с 2 аргументами (часы и минуты);
метод добавления времени ко временному интервалу с 1 аргументом (минуты);
метод добавления времени ко временному интервалу с 1 аргументом типа TimeSpan (из входящего TimeSpan считываются часы и минуты для добавления к текущему);
метод вычитания времени из текущего временного интервала с 2 аргументами (часы и минуты);
метод вычитания времени из текущего временного интервала с 1 аргументом (минуты);
метод вычитания времени из текущего временного интервала с 1 аргументом типа TimeSpan (из входящего TimeSpan считываются часы и минуты для вычитания из текущего).
2. Цепочка наследования. В данном задании необходимо выполнить следующие действия:
1. Создайте класс Person, который должен содержать конструктор, геттеры\сеттеры и следующие поля:
фамилия;
имя;
возраст.
Переопределите метод toString(), который должен возвращать строку следующего формата:
Человек <фамилия> <имя>, возраст: <возраст>
2. Создайте класс Student, который должен наследоваться от класса Person. Добавьте конструктор, геттеры\сеттеры и следующие поля:
группа;
номер студенческого билета.
Переопределите метод toString(), который должен возвращать строку следующего формата:
Студент группы <группа>, <фамилия> <имя>, возраст: <возраст>. Номер студенческого билета: <номер>
3. Создайте класс Lecturer, который должен наследоваться от класса Person. Добавьте конструктор, геттеры\сеттеры и следующие поля:
кафедра;
зарплата.
Переопределите метод toString(), который должен возвращать строку следующего формата:
Преподаватель кафедры <кафедра>, <фамилия> <имя>, возраст: <возраст>. Зарплата: <зарплата>
4. Используя восходящее преобразование, создайте в классе Main несколько объектов классов Student и Lecturer, после чего создайте массив, который бы мог включать объекты классов Person, Student, Lecturer. Заполните массив объектами этих классов.
5. Используя цикл, обратитесь к элементам массива и выведите в консоль, с помощью метода toString(), информацию от каждого объекта.
3. Задание на простой полиморфизм
В данной задаче мы допишем игру "Камень Ножницы Бяумага".
Скачайте, распакуйте и откройте проект в IntelliJ IDEA, запустите приложение и изучите исходный код;
с помощью механизма полиморфизма создайте классы игровых фигур (Камень, Ножницы, Бумага), которые наследуются от GameShape;
Напишите логику метода generateShape(), который возвращает одну из трех случайных фигур (объект одного из трех классов);
в методе actionPerformed() допишите создание объекта игровой фигуры в зависимости от того, на какую кнопку нажал пользователь (читайте комментарии внутри метода);
реализуйте метод checkWinner(), который принимает на вход два объекта игровых фигур - фигура компьютера и игрока. С помощью оператора instanceof выясните, фактический объект какого класса скрывается за ссылочной переменной типа GameShape. В зависимости от сочетания фигур, метод возвращает: 1 если выиграл игрок, -1 если выиграл компьютер или 0 если ничья;
Сыграйте в игру и убедитесь, что ваш код корректен и игра работает правильно.
4. Модификация программы Draw
Скачайте архив с проектом
Данная программа позволяет рисовать простые фигуры. С помощью кнопок на верхней панели выбираете нужную фигуру, после чего в поле рисования зажимаете кнопку мыши и рисуете фигуру нужных размеров.
Задание состоит в следующем:
Откройте проект, прочитайте комментарии, разберитесь как работает приложение;
Используя механизм полиморфизма, модифицируйте программу, добавив возможность выбирать и рисовать эллипсы;
Задание на дополнительный балл. Добавьте в программу кнопку Clear, по нажатию на которую программа должна стирать все фигуры.
При сдаче лабораторной работы, вы должны быть готовыми показать, где и для каких целей используется полиморфизм и восходящее преобразование.
Задание на лабораторную работу
Нарисуйте диаграммы классов UML для задания 1 по абстрактным классам и для заданий 1, 2, 4 по интерфейсам.
Абстрактные классы
Задание 1.
Дан следующий абстрактный класс
abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
abstract String makeSound();
}
Создайте класс Dog как подкласс Animal и реализуйте метод makeSound(). Создайте класс Cat как подкласс Animal и реализуйте метод makeSound(). Создайте в методе main() объекты каждого из подклассов и вызовите метод makeSound().
Задание 2.
Напишите исходный код классов, приведенных в диаграмме классов UML (методы реализовывать не нужно).
Интерфейсы
Задание 1.
Напишите исходный код класса Point, который реализует следующий интерфейс
Напишите исходный код интерфейса, приведенного в диаграмме классов UML. Напишите исходный код класса DynamicIntArrayImpl, реализующего этот интерфейс (методы реализовывать не нужно)
Задание на дополнительные баллы. Реализуйте методы класса DynamicIntArrayImpl, чтобы получился рабочий класс. Проверьте корректность его работы.
При вставке элемента, массив должен "раздвигаться", размер массива увеличивается. При вставке элемента по индексу, старый элемент с этим индексом смещается вправо.
При удалении элемента, массив должен "сужаться", размер массива уменьшается.
Задание 4.
Найдите в третьей лабораторной работе задание "Класс IntStack". Исходя из условий задания, разработайте интерфейс Stack с перечнем методов, которые, на ваш взгляд, нужно внести в интерфейс.
Задание 5.
Дан следующий класс
class Car {
private int price;
private int year; // ГОД ВЫПУСКА
private int horsePower;
// ДОБАВЬТЕ КОНСТРУКТОР С 3 АРГУМЕНТАМИ
// ДОБАВЬТЕ ГЕТТЕРЫ И СЕТТЕРЫ
}
Сделайте так, чтобы класс реализовывал интерфейс Comparable. Напишите исходный код метода compareTo(). Логика метода следующая:
сначала сравнивается цена автомобилей, "больше" тот автомобиль, у которого меньше цена;
если цены равны, то сравнивается год выпуска автомобиля, "больше" тот автомобиль, который меньше по возрасте;
если возраст одинаковый, то автомобили сравниваются по количеству лошадиных сил. "Больше" тот автомобиль, у которого лошадиных сил больше.
Не используйте типизированный Comparable! Для сравнения используйте только арифметические операторы!
Задание на дополнительные баллы. Приложение SortingList
Данная программа выводит список студентов и позволяет отсортировать их по имени, фамилии и по среднему баллу.
В программе уже реализована сортировка студентов по имени.
Задание состоит в следующем:
доделать программу, чтобы по нажатию на соответствующие кнопки происходила сортировка студентов по фамилии и по среднему баллу;
задание на дополнительные баллы - доработайте программу так, чтобы при повторном нажатии на ту же кнопку сортировки, осуществлялась обратная сортировка студентов. То есть, при первом нажатии на кнопку "Сортировка по имени" студенты сортировались в прямом порядке (по возрастанию), а при повторном нажатии на эту же кнопку, студенты сортировались в обратном порядке (по убыванию).
Список студентов находится в коллекции students, для сортировки необходимо вызвать у коллекции метод sort() и в этот метод передать объект компаратора.
Что такое компаратор. Базовая операция для почти любой сортировки – сравнение двух элементов. Если вы сортируете обычные числа или строки, сравнение происходит элементарно по известным всем правилам. Но как быть, если вы хотите отсортировать объекты, которые могут иметь десятки полей различных типов?
Для этого существует так называемый компаратор. Компаратор в Java – это объект класса, который реализует интерфейс Comparator. Интерфейс определяет всего один метод compare(), который принимает на вход два объекта типаObject. Если первый объект «меньше» – метод возвращает отрицательное число (обычно это просто -1, но может быть и любое другое отрицательное число), если первый объект «больше» – метод возвращает положительное число (обычно это 1, но может быть и любое другое положительное число), если объекты «равны» – метод возвращает 0.
Задача программиста - прописать нужную логику сравнения и вернуть -1/0/1 в том или ином случае.
Вы создаете класс, указываете, что этот класс реализует интерфейс Comparator и в методе compare() описываете логику сравнения двух объектов. В случае со сравнением двух студентов по имени, код компаратора выглядит следующим образом
public class NameSorter implements Comparator {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Student && o2 instanceof Student) {
Student s1 = (Student) o1;
Student s2 = (Student) o2;
return s1.getName().compareTo(s2.getName());
}
return 0;
}
}
Обратите внимание, что сначала мы должны убедиться, что оба объекта являются объектами класса Student, после чего делаем нисходящее преобразование.
В данном случае мы сравниваем два поля name у двух объектов класса Student. Так как это тип String, нам нет нужды сравнивать строки «вручную», мы просто можем воспользоваться методом compareTo(), который есть у любого объекта класса String.
Задание на лабораторную работу
В данной лабораторной работе задания выполняются с помощью плагина EduTools!
При решении задачи сначала добавьте сигнатуру методов и возвращаемые значения для того, чтобы тесты могли скомпилироваться корректно!
Если вы обновили IntelliJ IDEA, то используйте данный архив для выполнения лабораторной работы
Реализуйте каждую задачу с помощью механизма наследования, а потом - с помощью механизма композиции.
Для каждой реализации нарисуйте UML-диаграмму классов. Вы можете нарисовать одну диаграмму для двух заданий, реализованных с помощью наследования и одну диаграмму для заданий, реализованных с помощью механизма наследования.
1. Класс Point3D
Дан класс Point, который моделирует точку в двумерном пространстве. Класс включает в себя следующие конструкторы и публичные методы:
Сигнатура
Описание
public Point()
Создает точку с координатами (0, 0)
public Point(int x, int y)
Создает точку с координатами (x, y)
public void setLocation(int x, int y)
Устанавливает новые координаты точки
public int getX()
Возвращает значение координаты X
public int getY()
Возвращает значение координаты Y
public String toString()
Возвращает строку в виде "(x,y)"
public double distance(Point p)
Возвращает расстояние от текущей точки до входной точки по формуле расстояния Евклида
public double distanceFromOrigin()
Создайте класс Point3D, который расширяет класс Point через наследование. Он должен вести себя как Point, за исключением того что это должна быть точка в трехмерном пространстве, которая хранит значение координаты Z.
Вы должны предоставить те же методы, что и суперкласс, а также реализовать дополнительное поведение
Сигнатура
Описание
public Point3D()
Создает точку с координатами (0, 0, 0)
public Point3D(int x, int y, int z)
Создает точку с координатами (x, y, z)
public void setLocation(int x, int y, int z)
Устанавливает новые координаты
public double distance(Point3D p)
Возвращает расстояние от текущей точки до входной точки по формуле расстояния Евклида с учетом координаты z
public int getZ()
Возвращает координату Z
Класс Point3D() должен переопределить требуемые методы, чтобы они работали корректно с учетом третьей координаты. Также класс Point3D должен вести себя иначе в следующих ситуациях:
при вызове метода setLocation(int x, int y), координата z должна быть выставлена в 0;
при вызове метода toString(), строка должна выводить три координаты, а не две;
метод distanceFromOrigin() должны учитывать координату z и возвращать расстояние по формуле (x1−x2)2+(y1−y2)2+(z1−z2)2 .
2. Класс DiscountBill
Дан класс GroceryBill, который моделирует чек и хранит список товаров, который покупает человек в супермаркете. Класс включает в себя следующие конструкторы и публичные методы:
Сигнатура
Описание
public GroceryBill(Employee clerk)
Создает объект GroceryBill для данного clerk
public void add(Item i)
Добавляет товар в чек
public double getTotal()
Возвращает итоговую стоимость товаров
public void printReceipt()
Распечатывает список товаров
Объект GroceryBill взаимодействует с объектами класса Item. Класс Item включает следующие публичные методы:
Сигнатура
Описание
public double getPrice()
Возвращает стоимость товара
public double getDiscount()
Возвращает скидку для этого товара
К примеру, товар стоит 1.35, а размер скидки 0.25 для постоянных покупателей. Это означает, что постоянный покупатель должен заплатить 1.10. Некоторые товары могут не иметь скидки (размер скидки 0.0). В классе GroceryBill не предусмотрена логика для учета скидки, то есть учитывается только полная стоимость товара.
Разработайте класс DiscountBill, который расширяет класс GroceryBill и добавляет логику для учета скидки для постоянных клиентов. Конструктор класс DiscountBill должен принимать на вход параметр, который указывает, является ли клиент постоянным.
Класс DiscountBill должен реализовывать собственную логику метода getTotal() для постоянных покупателей. Например, если полная сумма равна 80 гривен, а скидка для постоянного клиента составила 20 гривен, метод должен возвращать 60 гривен.
Также, вам необходимо отслеживать количество товаров со скидкой (у которых размер скидки больше 0.0), а также общую скидку, как в гривнах, так и в процентах от суммы в чеке (то есть, насколько в процентах постоянный покупатель заплатил меньше, чем если бы скидки не было).
Помимо переопределенных методов, класс DiscountBill должен иметь следующие конструкторы и публичные методы:
Сигнатура
Описание
public DiscountBill
(Employee clerk, boolean regularCustomer)
Создает объект DiscountBill для данного clerk
public int getDiscountCount()
Возвращает количество товаров со скидкой
public double getDiscountAmount()
Возвращает общую скидку в гривнах
public double getDiscountPercent()
Возвращает процент скидки для товаров
(на сколько процентов покупатель
заплатил меньше).
Процент можно посчитать по формуле:
100 - (цена со скидкой * 100) / полная цена
Если покупатель не является регулярным, класс DiscountBill должен вести себя как будто общая скидка равна 0 и все товары учтены по их полной стоимости.
Лекция 10
Тема: Аспектно-ориентированное программирование. Сквозная функциональность. Join Point, Advice, Aspect. Расширение языка AspectJ. Обеспечение логирования с помощью АОП,
Материал доступен в формате pdf-файла
Лекция 9
Тема: Получение данных формы. Валидация данных, Bean Validation API. Загрузка файлов на сервер.
6. Валидация данных формы
Создадим проект с индексной страницей, на которой расположена форма добавления нового студента.
Изначально, форма не имеет средств валидации, то есть мы не можем отследить корректность заполнения формы.
Spring предоставляет несколько инструментов для реализации валидации формы, воспользуемся библиотекой Bean Validation API
Добавим библиотеку в список зависимостей в файле pom.xml
Будем использовать механизм встроенных ограничений. Алгоритм использования встроенных ограничений следующий - с помощью аннотаций необходимо указать над полем класса-сущности требуемые параметры валидации и другие параметры. В нашем случае, сущностью выступает класс Student. Добавим необходимые ограничения для полей сущности.
Как мы видим, все достаточно просто и наглядно.
Далее, нам необходимо модифицировать контроллеры и реализовать следующий функционал:
указать, что объект типа Student должен пройти валидацию;
получить результаты валидации объекта;
если объект не прошел валидацию - не добавлять объект в хранилище, выдать сообщение об ошибке в консоль.
Нам необходимо модифицировать метод контроллера, который обрабатывает данные формы. Указываем аннотацию @Valid, которая говорит о том, что полученный объект необходимо подвергнуть валидации. Далее указываем аргумент типа BindingResult, который хранит информацию о результате валидации. С помощью метода hasErrors() получаем результат валидации объекта.
При попытке отправить пустую форму, получаем сообщение в консоли
Последний шаг - необходимо предоставить пользователю информацию о том, что то или иное поле формы не прошло валидацию.
Самый простой способ проинформировать пользователь - показать сообщение об ошибке около поля, которое не прошло валидацию. Чтобы реализовать данный функционал, перейдем в шаблон index.html.
Рассмотрим поле "Фамилия". Сообщение об ошибке мы разместим снизу поля. Добавим соответствующий элемент <small> в HTML-макет.
Используем тег th:if. Если выражение внутри тега равно true, то элемент <small> будет показан на экране, если false - будет скрыт.
Выражение ${fields.hasErrors('lastName)} означает, есть ли ошибки валидации для поля lastName? Если ошибки есть - поле будет показано. Текст ошибки выводим с помощью атрибута th:errors.
Добавляем элементы для вывода ошибок для остальных полей формы. Проверяем результат
Зайдем в аккаунт и создадим новое приложение (new app)
Зайдем в раздел Resources, в пункте Add-ons добавим Heroku Postgres
Перейдем в настройки базы данных
На страничке адд-она перейдем в раздел Settings и выберем View Credentials
Таким образом, мы получим credentials для подключения к базе.
Перейдем в application.yml и укажем настройки для БД на heroku
После указания настроек, можете сразу запустить приложение, чтобы убедиться, что вы правильно указали настройки для подключения БД.
2. Установка git и heroku
Заходим в командную строку и настраиваем имя и почту разработчика
Возвращаемся в Heroku, скачиваем и настраиваем Heroku CLI
Заходим в командую строку и логинимся на heroku с помощью команды heroku login
Переходим в директорию проекта, после чего инициализируем git-репозиторий с помощью команды git init
Далее устанавливаем удаленный репозиторий
Начинаем отслеживать файлы проекта с помощью команды git add ., после чего делаем коммит с помощью команды git commit -am "initial commit".
Теперь можно пушить проект на удаленный репозиторий с помощью команды git push heroku master.
Теперь попробуем осуществить REST-запросы к серверу. Добавим группу, добавим студента, получим список групп.
Добавляем группу
Добавляем студента
Получаем список групп
Лекция 13-14
Тема: Понятие сессии. Cookies. JWT-токены. Генерация токенов. Security Filter Chain. Разработка приложения с использованием jwt-токенов.
Материал по теме находится в стадии оформления
Добавляем в pom-файл поддержку jwt и jaxb
Создадим класс JwtUtils, добавим приватный ключ и методы для создания токена
Добавим метод для валидациия токена и сопутствующие ему методы
Создадим классы-обертки для входящего логина и пароля и для исходящего jwt
Создадим класс REST-контроллера, добавим метод для создания токена. Логика работы метода следующая:
Производится аутентификация по пришедшему логину и паролю;
Если аутентификация прошла успешно, то получаем UserDetails из БД по username;
Генерируем токен с помощью данных UserDetails;
Возвращаем токен как объект AuthResponse.
Настраиваем конфигурационный класс SecurityConfig. Добавляем bean для AuthenticationManager, а также доступ к URL. Для того, чтобы сервер не генерировал новую сессию, устанавливаем sessionCreationPolicy.
В качестве клиента используем Postman.
Для начала попробуем сделать запрос с некорректным логином и паролем
На стороне сервера было выброшено исключение
Теперь попробуем ввести логин и пароль существующего пользователя. В ответ мы получим jwt, который будем использовать для последующих запросов.
Аутентификация с помощью jwt
Добавим в класс-контроллер endpoint, для доступа к которому требуется аутентификация. Входной аргумент типа Principal хранит информацию об аутентифицированном пользователе. Получаем username пользователя и извлекаем из БД информацию о нем.
Редактируем конфигурационный класс SecurityConfig, добавляем требование аутентификации для endpoint /helloworld.
Для того чтобы провести аутентификацию с помощью jwt, создадим отдельный фильтр, который потом встроим в filter chain, которая используется в Spring Security.
Логика работы нашего фильтра следующая:
Считываем заголовок GET-запроса с ключом "Authorization";
Проверяем, есть ли в начале заголовка слово "Bearer ";
Извлекаем из jwt значение username;
Извлекаем из БД пользователя по полученному username и осуществляем валидацию токена;
Если jwt провел валидацию, то аутентифицируем пользователя;
Передаем управление дальше по цепочке фильтров.
Отредактируем конфигурационный класс SecurityConfig, добавим использование фильтра в цепочке фильтров (мы указываем, что наш фильтр должен быть встроен в цепочку ДО фильтра UsernamePasswordAuthenticationFilter, который является стандартным фильтром для аутентификации).
Лекция 11-12
Тема: Работа со Spring Security. Понятие аутентификации и авторизации. Расширение WebSecurityConfigurerAdapter. Способы formLogin() и httpBasic(). Хеширование паролей.
Spring Security – это фреймворк обеспечения безопасности, предоставляющий возможность декларативного управления безопасностью приложений на основе фреймворка Spring.
Создадим новый проект, который включает модуль Spring Security или добавим в существующий проект зависимость
При попытке перейти на любой URL-адрес проекта нас перенаправит на форму ввода логина и пароля
По умолчанию, логином является user, а пароль генерируется каждый раз при старте приложения.
Если вы ввели правильно логин и пароль, то сервер переадресует вас на указанный URL.
В файле application.properties вы можете указать желаемый логин и пароль для пользователя по умолчанию (в данной лекции будет использоваться конфигурация с помощью языка yaml).
Фреймворк Spring Security "из коробки" предоставляет вам возможность простой версии так называемой form-based аутентификации. Если быть точнее, то по умолчанию, Spring Security реализует следующее поведение:
добавляет обязательный процесс аутентификации для всех URL;
добавляет форму для входа;
обрабатывает ошибки формы ввода;
создает пользователя по умолчанию и генерирует пароль.
Основные понятия, связанные со Spring Security:
Authentication
Authorization
Principal - текущий залогиненный пользователь или текущий залогиненный аккаунт (если у одного физического лица или программы есть несколько аккаунтов, то тогда ему будет соответствовать несколько возможных principal`ов). Иногда, в общем случае, principal - это субъект, который принимает участие в осуществлении процедур безопасности. В качестве principal могут выступать люди, компьютеры, службы, процессы или их группа;
Granted Authority - ;
Role - .
Настройка процесса аутентификации в Spring
Для того, чтобы сконфигурировать процесс аутентификации, необходимо создать объект AuthenticationManager, в котором следует указать требуемые параметры аутентификации. Объект типа AuthenticationManager обычно настраивают с помощью builder`а, который имеет тип AuthenticationManagerBuilder.
Добавим класс SecurityConfig, который наследуется от класса WebSecurityConfigurerAdaper. Также укажем аннотации @Configuration (это означает, что данный класс является конфигурационным) и @EnableWebSecurity (это означает, что данный класс является содержит настройки для защиты веб-приложения).
Переопределим метод configure(), который принимает на вход объект типа AuthenticationManagerBuilder (обратите внимание, что нам нужна именно эта версия перегруженного метода).
Для начала укажем, что источник аутентификации это жестко прописанные пользователи (так называемая inMemoryAuthentication(). Далее указываем логин, пароль и роль пользователя.
Если необходимо указать несколько пользователей, после параметров первого пользователя вызываете метод and() после чего указываете параметры следующего пользователя.
Хеширование паролей
Хранить пароли без хеширования является грубейшим нарушением правил безопасности, поэтому нам необходимо добавить процесс хеширования пароля в нашу систему.
Не будем вдаваться в подробности различных алгоритмов хеширования пароля, просто скажем, что на даннай момент рекомендуемым является алгоритм Bcrypt. Для обеспечения хеширования, вы можете поступить несколькими способами.
Первый способ - создайте Bean, который будет возвращать объект Encoder`а и добавьте его как метод конфигурационного класса.
Далее найдите в интернете генератор хеша с помощью алгоритма Bcrypt, скопируйте хеш для вашего пароля в метод password.
Если не хотите использовать Bean для хеширования пароля, можете в начале хеша добавить обозначение, что это хеш для алгоритма bcrypt.
Настройка процесса авторизации
Добавим класс контроллера
Изменим SecurityConfig
Изменим formLogin() на httpBasic().
Создадим MyUserDetailsService
Создадим MyUserDetails
Добавим в pom.xml
Настроим подключение к БД
Добавим сущность User
Изменим MyUserDetailsService
Создадим UserRepository
Изменим MyUserDetails
Более подробно про клиент-серверное взаимодействие читайте здесь -
Информация о сервере приложений - .
Отличия веб-сервера и сервера приложений - Подробнее про сервлеты и контейнеры сервлетов -
На данный момент актуальной версией технологии является версия 4.0, спецификацию технологии смотрите .
JDK - ;
приложение Postman - ;
среда разработки, которая поддерживает Spring (например, IntelliJ IDEA Ultimate Edition или дистрибутив Eclipse под названием Spring Tool Suite – ) либо любая другая IDE с поддержкой Java и Maven.
генерация готового проекта на сайте (проект Spring Initializr);
Подробно читайте про инверсию контроля здесь или здесь .
Статью Мартина Фаулера (читать обязательно) читайте здесь или здесь (, ).
Неплохой материал по поводу конфигурации с помощью классов можно почитать и .
Запустим сервер, заходим на и видим строку с ответом.
application/x-www-form-urlencoded: значения кодируются в кортежах с ключом, разделенных символом '&', с '=' между ключом и значением. Не буквенно-цифровые символы - : это причина, по которой этот тип не подходит для использования с двоичными данными (вместо этого используйте multipart/form-data);
Использование формата JSON () является более предпочтительным. Для сериализации и десериализации в Spring по-умолчанию используется библиотека Jackson.
Подробную информацию по поводу ResponseEntity читайте здесь - .
Скачайте архив с проектом по этому адресу - .
Возвращает расстояние от начала координат (0, 0)
до точки по формуле
расстояния Евклида
Информацию по поводу использования библиотеки можно найти (см. раздел 8 мануала)
Заходим на сайт , скачиваем последнюю версию установщик и устанавливаем Git.
Как видим, мы успешно развернули проект на Heroku. Проверим работу веб-сервиса. Перейдем в браузер и укажем адрес
public class Student {
// Имя должно быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "First name should be from 2 to 50 characters")
private String firstName;
// Фамилия должна быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "Last name should be from 2 to 50 characters")
private String lastName;
// Возраст должен быть целым числом от 13 до 65
@Range(min = 13, max = 65, message = "Student age should be from 13 to 65 years")
private int age;
// Для валидации электронной почты используем регулярное выражение
@Pattern(regexp = "^[\\\\w!#$%&’*+/=?`{|}~^-]+(?:\\\\.[\\\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\\\.)+[a-zA-Z]{2,6}$",
message = "Invalid email format")
private String email;
...
}
@GetMapping("/")
public String addStudent(Model model) {
model.addAttribute("student", new Student());
return "index";
}
@PostMapping("/")
public String processAddStudentForm(@Valid Student student, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
System.out.println("Validation has been failed!");
return "index";
}
System.out.println(student);
list.add(student);
return "redirect:/";
}
public class Student {
// Имя должно быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "First name should be from 2 to 50 characters")
private String firstName;
// Фамилия должна быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "Last name should be from 2 to 50 characters")
private String lastName;
// Возраст должен быть целым числом от 13 до 65
@Range(min = 13, max = 65, message = "Student age should be from 13 to 65 years")
private int age;
// Для валидации электронной почты используем регулярное выражение
@Pattern(regexp = "^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$",
message = "Invalid email format")
private String email;
public Student(String firstName, String lastName, int age, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.email = email;
}
public Student() {
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Controller
public class StudentController {
private List<Student> list = new ArrayList<>();
@GetMapping("/")
public String addStudent(Model model) {
model.addAttribute("student", new Student());
return "index";
}
@PostMapping("/")
public String processAddStudentForm(@Valid Student student, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "index";
}
System.out.println(student);
list.add(student);
return "redirect:/";
}
}
C:\Users\nickg>heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/browser/630e1a02-f90e-4217-8170-cd71d7fc700e
Logging in... done
Logged in as hodovychenko@opu.ua
C:\Users\nickg>cd d:\springdemo\ejo
d:\springdemo\ejo>git init
Initialized empty Git repository in d:/springdemo/ejo/.git/
d:\springdemo\ejo>heroku git:remote -a opnu-ej
set git remote heroku to https://git.heroku.com/opnu-ej.git
d:\springdemo\ejo>git add .
warning: LF will be replaced by CRLF in .gitignore.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in .mvn/wrapper/MavenWrapperDownloader.java.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in .mvn/wrapper/maven-wrapper.properties.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in mvnw.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in mvnw.cmd.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in pom.xml.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in src/main/java/com/example/ejo/EjoApplication.java.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in src/test/java/com/example/ejo/EjoApplicationTests.java.
The file will have its original line endings in your working directory
d:\springdemo\ejo>git commit -am "initial commit"
[master (root-commit) 162bc53] initial commit
16 files changed, 886 insertions(+)
create mode 100644 .gitignore
create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java
create mode 100644 .mvn/wrapper/maven-wrapper.jar
create mode 100644 .mvn/wrapper/maven-wrapper.properties
create mode 100644 mvnw
create mode 100644 mvnw.cmd
create mode 100644 pom.xml
create mode 100644 src/main/java/com/example/ejo/Controller.java
create mode 100644 src/main/java/com/example/ejo/EjoApplication.java
create mode 100644 src/main/java/com/example/ejo/Group.java
create mode 100644 src/main/java/com/example/ejo/GroupRepository.java
create mode 100644 src/main/java/com/example/ejo/Service.java
create mode 100644 src/main/java/com/example/ejo/Student.java
create mode 100644 src/main/java/com/example/ejo/StudentRepository.java
create mode 100644 src/main/resources/application.yml
create mode 100644 src/test/java/com/example/ejo/EjoApplicationTests.java
d:\springdemo\ejo>git push heroku master
Enumerating objects: 32, done.
Counting objects: 100% (32/32), done.
Delta compression using up to 6 threads
Compressing objects: 100% (23/23), done.
Writing objects: 100% (32/32), 54.39 KiB | 9.06 MiB/s, done.
Total 32 (delta 1), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Java app detected
remote: -----> Installing JDK 1.8... done
remote: -----> Executing: ./mvnw -DskipTests clean dependency:list install
remote: [INFO] Scanning for projects...
...
remote: [INFO] Replacing main artifact with repackaged archive
remote: [INFO]
remote: [INFO] --- maven-install-plugin:2.5.2:install (default-install) @ ejo ---
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.pom
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.pom (4.0 kB at 450 kB/s)
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.jar
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar (38 kB at 1.1 MB/s)
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.15/plexus-utils-3.0.15.jar
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.jar (155 kB at 2.0 MB/s)
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.15/plexus-utils-3.0.15.jar (239 kB at 3.0 MB/s)
remote: [INFO] Installing /tmp/build_bee447e74484e663663b1d166f8e9456/target/ejo-0.0.1-SNAPSHOT.jar to /app/tmp/cache/.m2/repository/com/example/ejo/0.0.1-SNAPSHOT/ejo-0.0.1-SNAPSHOT.jar
remote: [INFO] Installing /tmp/build_bee447e74484e663663b1d166f8e9456/pom.xml to /app/tmp/cache/.m2/repository/com/example/ejo/0.0.1-SNAPSHOT/ejo-0.0.1-SNAPSHOT.pom
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] BUILD SUCCESS
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] Total time: 20.474 s
remote: [INFO] Finished at: 2019-11-26T21:46:52Z
remote: [INFO] ------------------------------------------------------------------------
remote: -----> Discovering process types
remote: Procfile declares types -> (none)
remote: Default types for buildpack -> web
remote:
remote: -----> Compressing...
remote: Done: 84.1M
remote: -----> Launching...
remote: Released v5
remote: https://opnu-ej.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/opnu-ej.git
* [new branch] master -> master
d:\springdemo\ejo>
При создании объектов с помощью оператора new возвращается ссылка на вновь созданный объект. Однако нас никто не обязывает эту ссылку присваивать в качестве значения ссылочной переменной. В таких случаях создается анонимный объект. Другими словами, объект есть, а переменной, которая бы содержала ссылку на этот объект, нет.
С практической точки зрения, это может выглядеть бесполезным, однако, анонимные объекты требуются довольно часто - обычно в тех ситуациях, когда объект класса используется один раз. Рассмотрим пример
В данном случае, нам нужен объект класса PrinterManager только для одного действия - для распечатки файла. То есть, мы создаем объект, вызываем метод, после чего объект нам больше нужен.
В первом случае используется обычный порядок работы с объектами - создаем объект класса PrinterManager, ссылка на объект записывается в ссылочную переменную manager.
Во втором случае мы создаем анонимный объект. Инструкцию
можно условно разбить на две части. Выражениеnew PrinterManager() создает новый объект класса PrinterManager и возвращает ссылку на объект в качестве результата выражения. Поскольку ссылка не присваивается переменной, то такой объект является анонимным. Но так как выражение new PrinterManager() возвращает ссылку на объект, мы можем у этой ссылки вызвать метод printFile().
Заметки по абстракции
В окончательном виде любая программа представляет собой набор инструкций процессора. Все, что написано на любом языке программирования - более удобная, упрощенная запись этого набора инструкций, облегчающая написание, отладку и последующую модификацию программы. Чем выше уровень языка, тем в более простой форме записываются одни и те же действия.
С ростом объема программы становится невозможным удерживать в памяти все детали, и становится необходимым структурировать информацию, выделять главное и отбрасывать несущественное. Этот процесс называется повышением степени абстракции программы.
Для языка высокого уровня первым шагом к повышению абстракции является использование функций, позволяющее после написания и отладки функции отвлечься от деталей ее реализации, поскольку для вызова функции требуется знать только ее интерфейс. Если глобальные переменные не используются, интерфейс полностью определяется заголовком функции.
Лекция 7-8
Тема:
Фреймворк Spring MVC. Создание MVC-приложения. Создание контроллера, переход между страницами. Использование Spring Expression Language. Создание представлений с помощью Thymeleaf.
Теоретические сведения
Spring MVC – веб-фреймворк, призванный упростить разработку веб-приложений. Опираясь на шаблон модель–представление–контроллер (Model-View-Controller, MVC), фреймворк Spring MVC помогает строить веб-приложения, столь же гибкие и слабо связанные, как сам фреймворк Spring.
Схема работы фреймворкаSpringMVC
Схема работы фреймворка выглядит следующим образом:
Краткое описание схемы работы Spring MVC звучит следующим образом:
вначале DispatcherServlet (диспетчер сервлетов) получает запрос, далее он смотрит свои настройки, чтобы понять какой контроллер использовать (на рисунке HandlerMapping);
после получения имени контроллера запрос передается на обработку в этот контроллер (на рисунке Controller). В контроллере происходит обработка запроса и обратно посылается ModelAndView (модель — сами данные; view (представление) — как эти данные отображать);
DispatcherServlet на основании полученного ModelAndView, должен определить, какое представление будет выводить данные. Для этого используется арбитр представлений (ViewResolver), который на основании полученного логического имени представления возвращает ссылку на файл View;
в представление передаются данные (Model) и обратно, если необходимо, посылается ответ от представления.
Давайте рассмотрим этот процесс более подробно:
Когда запрос покидает браузер, он несет в себе информацию о требовании пользователя. По крайней мере, запрос будет нести в себе запрошенный URL. Но он может также нести дополнительные данные, такие как информация из формы, заполненной пользователем;
Первой остановкой на пути запроса является DispatcherServlet. Как и большинство веб-фреймворков на языке Java, фреймворк Spring MVC пропускает все входящие запросы через единственный сервлет входного контроллера. Входной контроллер (frontcontroller) является типичным шаблоном проектирования веб-приложений, где единственный сервлет берет на себя ответственность за передачу всех запросов остальным компонентам приложения, выполняющим фактическую их обработку. В Spring MVC входным контроллером является DispatcherServlet;
Задача контроллера DispatcherServlet состоит в том, чтобы передать запрос контроллеру Spring MVC. Контроллер – это компонентSpring, обрабатывающий запрос. Но приложение может иметь несколько контроллеров, и входному контроллеру DispatcherServlet требуется помощь, чтобы определить, какому контроллеру передать запрос. Поэтому контроллер DispatcherServlet консультируется c одним или несколькими механизмами отображения (Handler Mapping) и выясняет, какой контроллер будет обрабатывать тот или иной запрос. При принятии решения механизм отображения в первую очередь руководствуется адресом URL в запросе;
Как только будет выбран соответствующий контроллер, DispatcherServlet отправляет запрос в путь к выбранному контроллеру. Достигнув контроллера, запрос отдаст часть своего груза (информацию, отправленную пользователем) и терпеливо будет ждать, пока контроллер обработает эту информацию. (На самом деле хорошо спроектированный контроллер сам почти не занимается обработкой информации, вместо этого он делегирует ответственность за обработку одному или нескольким служебным объектам);
В результате работы контроллера часто появляется некоторая информация, которая должна быть передана назад пользователю и отображена в браузере. Эта информация называется моделью (Model). Но отправки обратно необработанной информации недостаточно, перед отправкой ее следует представить в удобном для пользователя формате, обычно в HTML. Для этого информация должна быть передана в одно из представлений (View), которыми обычно являются JSP-страницы;
Последнее, что должен сделать контроллер, – упаковать вместе модель и имя представления для отображения результатов в браузере. Затем он отсылает запрос вместе с моделью и именем представления обратно входному контроллеру DispatcherServlet;
Чтобы контроллер не оказался тесно связанным с каким-либо конкретным представлением, имя представления, возвращаемое входному контроллеру DispatcherServlet, не определяет JSP-страницу непосредственно. Фактически оно даже не предполагает, что представление вообще является страницей JSP. Оно является лишь логическим именем представления, используемым затем для поиска фактического представления. Чтобы отобразить логическое имя представления в ссылку на конкретную реализацию, входной контроллер DispatcherServlet обратится к арбитру представлений (viewresolver);
Теперь, когда контроллер DispatcherServlet определил, какое представление будет отображать результаты, работа запроса подошла к концу. Его конечная остановка – реализация представления (возможно, страница JSP), куда он доставит модель данных. На этом работа запроса заканчивается. На основе модели данных представление создаст отображение страницы, которое будет отправлено обратно клиенту с другим (не таким трудолюбивым) курьером – объектом ответа.
Рассмотренную выше схему работы фреймворка можно также представить следующей диаграммой
Создание и настройка Spring Boot проекта
Создадим новый Spring Boot проект, выберем следующие модули
В новом проекте обратите внимание на структуру папок. В папке resources\templates будут содержаться html-файлы с использованием шаблонизатора Thymeleaf.
Создадим файл index.html. Обратите внимание, что в теге html необходимо указать пространство имен th для подключения тегов Thymeleaf.
Создадим класс контроллера, который обрабатывает GET запросы с URL "/" и "/add_student".
StudentController.java
@Controller
public class StudentController {
@GetMapping("/")
public String index(Model model) {
return "index";
}
@GetMapping("/add_student")
public String addStudent(Model model) {
return "add_student";
}
}
В файле index.html добавим ссылку для кнопки "Добавить студента" . Для формирования ссылки используем тег th:href. Для указания пути относительно домена используем комбинацию @{~}.
Тема: Базовые понятия ООП: класс, объект, атрибуты, методы, состояние объекта. Создание объектов. Примитивные и ссылочные типы. Конструктор. Ключевое слово this.
"Про объектно-ориентированное программирование можно рассказать просто и неправильно либо сложно и неправильно"
(с) Анекдот
Объектно-ориентированное программирование – это методология программирования, основанная на представлении программы в виде совокупности взаимодействующих объектов, каждый из которых является экземпляром определенного класса, а классы являются членами определенной иерархии наследования.
Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Фактически, объект является основным строительным блоком объектно-ориентированных программ.
Класс – шаблон для объектов. Каждый объект является экземпляром (instance) какого-либо класса («безклассовых» объектов не существует). В рамках класса задается общий шаблон, структура, на основании которой создаются объекты. Данные, относящиеся к классу, называются полями класса, а программный код для их обработки называется методами класса. Поля и методы иногда называют общим термином – члены класса.
Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом.
Объект состоит из следующих частей:
имя объекта;
состояние (переменные состояния). Данные, содержащиеся в объекте, представляют его состояние. В терминологии ООП эти данные называются атрибутами. Например, атрибутами работника могут быть: имя, фамилия, пол, дата рождения, номер телефона. В разных объектах атрибуты имеют разное значение. Фактически, в объектах определяются конкретные значения тех переменных (полей класса), которые были заявлены при описании класса;
методы (операции) – применяются для выполнения операций с данными, а также для совершения других действий. Методы определяют, как объект взаимодействует с окружающим миром.
Объекты могут отправлять друг другу сообщения. Сообщение (message) - это практически то же самое, что и вызов функции в обычном программировании. В ООП обычно употребляется выражение "послать сообщение" какому-либо объекту. Понятие "сообщение" в ООП можно объяснить с точки зрения ООП: мы не можем напрямую изменить состояние объекта и должны как бы послать сообщение объекту, что мы хотим как-то изменить его состояние. Очень важно понять, что объект сам меняет свое состояние, а мы можем только попросить его об этом с помощью отсылки сообщения.
В объектно-ориентированной программе весь код должен находиться внутри классов!
В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.
От языка С к объектно-ориентированному программированию
Давайте попробуем понять, что такое объект и класс с помощью языка программирования C.
Представим себе, что мы пишем программу для книжной лавки на языке C. В какой-то момент мы сталкиваемся с необходимостью хранить информацию о множестве книг: название книги, кто автор книги, год издания и стоимость книги. Как нам это запрограммировать?
Мы можем воспользоваться массивами и хранить данные о книгах в нескольких массивах.
int book_year[100];
int book_pages[100];
char book_author[100][50];
char book_title[100][100];
float book_price[100];
Теперь мы можем обратиться к i-му номеру каждого массива для получения информации об i-ой книге.
Какими недостатками обладает данный способ работы с данными? Такой подход может приводить к многочисленным ошибкам (например, ошибки при работе с индексами массивов), такие данные тяжело модифицировать (удаление книги приводит к необходимости смещать влево часть элементов массивов), такой код неудобно читать, поддерживать и модифицировать.
Но самое главное - мы мыслим в контексте структуры компьютера, а не решаемой задачи. Для нас книга - это некий единый объект, который имеет некоторые параметры (атрибуты): название, количество страниц и так далее. Мы же представляем атрибуты этого объекта в виде отдельных записей в разных массивах, потому что на языке C мы вынуждены мыслить в терминах имеющихся структур данных (массивов, очередей, деревьев), а не в терминах отдельных объектов и их взаимоотношений. Это затрудняет понимание решаемой задачи (управление книжной лавкой и продажей книг), увеличивает количество ошибок в программе и на некотором этапе мы вообще перестаем понимать, что происходит в программе.
Для хотя бы какого-то решения этой проблемы и облегчения труда программиста, на некотором этапе в язык C было введено понятие структуры (ключевое слово struct), которое вы должны были изучать в курсе "Алгоритмизация и программирование".
Структура в языке C - это тип данных, создаваемый программистом, предназначенный для объединения данных различных типов в единое целое. Таким образом, мы можем сгруппировать данные, которые относятся к одной книге, в одну структуру.
Сначала мы должны описать структуру (новый тип данных).
struct Book {
char author[50];
char title[100];
int year;
int pages;
float price;
};
Таким образом, некоторое понятие реального мира (то есть, книга вообще, как понятие) в программе описан структурой Book. Экземпляр этого понятия (какая-то конкретная книга) в программе будет представлен экземпляром структуры - переменной типа Book. Мы объявляем экземпляр структуры, после чего мы сможет заполнить поля структуры и работать с ней дальше в программе.
int main() {
// Объявляем экземпляр структуры
struct Book book1;
// Заполняем поля экземпляра
strcpy(book1.author,"Иванов А.А");
strcpy(book1.title,"Программирование на Java");
book1.pages = 255;
book1.price = 350.25;
book1.year = 2018;
// Теперь мы можем работать с созданным экземпляром структуры
printf("%s - %s, %d страниц", book1.author, book1.title, book1.pages);
return 0;
}
Использование структур решает проблему лишь частично. Мы сгруппировали данные, но функции для работы с нашей структурой находятся вне структуры.
Объект реального мира обладает не только атрибутами (автор книги, название книги, количество страниц и так далее), но и определенным поведением (книгу можно продать, купить, переместить на склад и так далее). При разработке сложных программных систем необходимо группировать не только данные, но и функции, которые работают с этими данными.
Пример использования объектного подхода
Рассмотрим простой пример объектно-ориентированной программы. Представим, что мы программируем графический редактор, который может рисовать различные фигуры. Подумав над задачей, мы приходим к выводу, что в нашей программе должны быть объекты, которые представляют различные фигуры. Итак приступим.
Сначала мы должны создать классы и описать внутри этих классов данные, которые относятся к фигурам и поведение фигур в виде набора методов (функций).
Опишем класс для фигуры «Треугольник». Какие характеристики могут быть у класса «Треугольник»? От чего зависит набор характеристик? В принципе, любой объект является бесконечно сложным и для его описания понадобится бесконечно большое количество характеристик. Но для нашего простого графического редактора важными будут следующие параметры: координаты трех точек и цвет фигуры.
Здесь вы должны понять очень важную вещь: набор данных в классе зависит от той программы, которую мы собираемся написать. Например, если бы мы писали более мощный графический редактор, в списке полей класса мы добавили бы отдельно цвет заливки и цвет линий фигуры, сам цвет мог быть не только сплошным, но и в виде какого-то узора и так далее.
Для указания класса в Java используется ключевое слово class, после чего идет название класса, далее мы ставим фигурные скобки и всё, что будет написано внутри фигурных скобок (переменные и функции) будет относиться к этому классу.
class Triangle {
}
Какие переменные и типы данных будут моделировать координаты точек и цвет? Для моделирования цвета воспользуемся обычным целым числом (очень часто цвета моделируются обычным целочисленным значением).
class Triangle {
// Цвет треугольника
int color;
}
Теперь попробуем подумать, а как нам смоделировать координаты трех точек? Если бы мы программировали на языке C, мы бы написали что-то вроде
class Triangle {
// Цвет треугольника
int color;
// Координаты точек треугольника
int x1,y1,x2,y2,x3,y3;
}
Но давайте подумаем - координаты каждой точки логически связаны между собой и когда мы будем, например, перемещать треугольник или будем менять размер треугольника, будут одновременно меняться две переменные, которые моделируют одну точку. С точки зрения объектной модели, лучшим вариантом будет предусмотреть отдельную сущность, отдельный класс, который моделирует точку на двумерной плоскости. Таким образом, мы сгруппируем данные и код, который будет эти данные изменять (например, менять значение X и Y).
class Point2D {
int x;
int y;
}
Теперь вернемся к классу Triangle и укажем, что в качестве координат у нас будут выступать три объекта класса Point2D.
class Triangle {
// Цвет треугольника
int color;
// Координаты трех точек
Point2D point1;
Point2D point2;
Point2D point3;
}
Теперь давайте определимся с методами классов – то есть, с тем поведением, которое будут осуществлять объекты этих классов.
Какое поведение может быть у объекта класса «точка»? Совершенно точно это будет метод «изменить координаты». То есть, наша точка как отдельный объект может вести себя следующим образом – менять свои координаты. Давайте запрограммируем этот метод.
class Point2D {
int x;
int y;
void changeCoordinates(int new_x, int new_y) {
x = new_x;
y = new_y;
}
}
Теперь давайте подумаем, какое поведение будет у треугольника? Что с ним можно сделать? Ну, например, можно его перекрасить, можно его передвинуть в другое место на плоскости, можно его нарисовать на каком-то полотне и так далее. Давайте опишем некоторые из этих методов.
class Triangle {
int color; // Цвет треугольника
// Координаты трех точек
Point2D point1;
Point2D point2;
Point2D point3;
// Меняем цвет фигуры
void changeColor(int new_color) {
color = new_color;
}
// Меняем расположение фигуры
// Для наглядности будем передавать входные параметры
// в виде 6 целых чисел
void move(int x1, int y1, int x2, int y2, int x3, int y3) {
point1.changeCoordinates(x1, y1);
point2.changeCoordinates(x2, y2);
point3.changeCoordinates(x3, y3);
}
}
Итак, мы описали треугольник, но мы определили только понятие «треугольник», у нас нет их физически, код класса - это просто описание. Если вы создали java-проект, у вас, по умолчанию, будет присутствовать класс Mainи функция main(). Пока что мы не будем объяснять, зачем нужен класс Main, а обратим внимание на метод main(). Этот метод является «точкой входа» в программу, с вызова этого метода начинается работа программы. Когда мы дойдем и выполним последнюю инструкцию внутри метода main(), приложение завершится.
Создадим внутри метода main() несколько объектов класса Triangle. Создание объектов мы будем рассматривать подробно в следующих лекциях, на данный момент мы просто скажем, что для создания объекта используется ключевое словоnew. После создания объекта, мы можем обращаться к объекту и вызывать методы у объектов (как мы вызывали функции в C).
public class Main {
public static void main(String[] args) {
// Создали один объект треугольника
Triangle triangle1 = new Triangle();
// Создали еще один объект треугольника
Triangle triangle2 = new Triangle();
// Для первого треугольника поменяли цвет
triangle1.changeColor(10000);
// Поменяли цвет для второго треугольника
triangle2.changeColor(20000);
// Создали объект первой точки треугольника
// и назначили точке какие-то координаты
triangle1.point1 = new Point2D();
triangle1.point1.changeCoordinates(140,180);
// Сделаем это же для первой точки второго треугольника
triangle2.point1 = new Point2D();
triangle2.point1.changeCoordinates(50,80);
}
}
Важно! Вы должны понять, что у разных объектов значения переменных будут разные!
То есть, где-то в оперативной памяти у нас создано два разных объекта. Внутри первого есть объект класса Point2D, внутри этого объекта есть две переменные типа int, у которых будут значения 140 и 180. Внутри же второго треугольника будет свой объект Point2D, внутри которого будут совершенно другие две переменные типа int, у которых будут значения 50 и 80. Это же будет касаться переменных color в двух разных объектах (рис.1.5).
Таким образом, когда мы описываем классы, мы заявляем, что объекты этих классов будут содержать определенные поля (набор переменных) и будут иметь набор методов (функций), которые можно вызвать у объекта. Каждый объект содержит свои переменные, но методы у них общие.
Базовые понятия ООП
Понятие объекта
Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Это позволят разграничить область применения методов. Объект – это строительный блок объектно-ориентированных программ. Объектно-ориентированная программа является, по сути, набором объектов. Объект состоит из трех частей:
имя объекта;
состояние (данные объекта, переменные состояния). Состояние объекта характеризуется перечнем всех свойств данного объекта и текущими значениями каждого из этих свойств;
методы (операции).
Данные объектов. Данные, содержащиеся в объекте, представляют его состояние. В терминологии ООП, эти данные называются атрибутами. Например, атрибутами работника могут быть имя, фамилия, пол, дата рождения, номер телефона и так далее. В разных объектах атрибуты имеют разное значение.
Поведение объектов. Поведение объекта – то, что он может сделать (в структурном программировании это реализовывалось функциями, процедурами, подпрограммами).
Сообщения – механизм коммуникации между объектами. Например, когда объект А вызывает метод объекта B, объект A отправляет сообщение объекту B. Ответ объекта B определяется его возвращаемым значением. Только «открытые» методы могут вызываться другим объектом.
Понятие класса
Каждый объект определяется общим шаблоном, который называется классом. В рамках класса задается общий шаблон, структура, на основе которой затем создаются объекты. Данные, относящиеся к классу, называются полями класса, а программный код для их обработки – методами класса. Поля и методы иногда называют общим термином – члены класса.
В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.
Согласно конвенции кода и правилам языка Java:
каждый класс должен содержаться в своем отдельном файле с расширением .java;
название файла должно совпадать с названием класса;
класс должен быть именем существительным;
имя класса должно его описывать;
имя класса начинается с большой буквы;
если имя состоит из нескольких слов, то каждое слово начинается с большой буквы.
Рассмотрим разницу между объектом и классом на примере. Определим класс Catи Dog. Описание класса производится через указание полей (данных) и методов класса. Для класса Catв качестве полей укажем name(кличку кота) и color(окрас). Для класса Dogзадаем поля name(кличка), color(окрас) и breed(порода).
Помимо полей, определим методы для этих классов. Методы – это то, что может делать объект класса (или что можно делать с объектом). Коты будут мяукать, и ловить мышей, а собаки лаять и вилять хвостом.
Таким образом, мы определили шаблоны, на основании которых впоследствии будут создаваться экземпляры классов или объекты. Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом. При создании объекта класса задаются конкретные значения для полей. Когда мы говорим о собаке или кошке вообще, как понятии, мы имеем в виду домашних животных, у которых есть имя, окрас и прочие характеристики. Это абстрактные понятия, которые соответствуют классу. А вот если речь идет о конкретном Шарике или Мурзике, то это уже объекты, экземпляры класса.
Синтаксис классов
Рассмотрим синтаксис описания классов в Java. Описание класса начинается с ключевого слова class. После этого следует имя класса и в фигурных скобках тело класса. Тело класса состоит из описания членов класса – полей и методов.
Для объявления класса служит ключевое слово class. Упрощенная форма определения класса имеет вид:
class имя_класса {
тип переменная_экземпляра_1;
тип переменная_экземпляра_2;
тип переменная_экземпляра_3;
...
тип переменная_экземпляра_N;
тип имя_метода_1(список параметров) {...}
тип имя_метода_2(список параметров) {...}
...
тип имя_метода_N(список параметров) {...}
}
Данные или переменные, определенные в классе, называются переменными экземпляра, поскольку каждый экземпляр класса (объект) содержит собственные копии этих переменных. Таким образом, данные одного объекта отделены и отличаются от данных другого объекта.
Код содержится в теле методов. В большинстве классов действия над переменными и доступ к ним осуществляют методы этого класса. Таким образом, методы определяют порядок использования данных класса.
Пример простого класса
Создадим класс Box, который описывает контейнер, допустим, на каком-то складе.
Box.java
public class Box {
double width;
double height;
double depth;
}
Класс Box определяет три переменные экземпляра: width (ширина), height (высота) и depth (глубина). В настоящий момент класс Box не содержит никаких методов.
Как мы уже говорили, класс определяет новый тип данных. В данном случае новый тип данных называется Box. Это имя будет использоваться для объявления объектов типа Box. Не следует забывать, что объявление class создает только шаблон, но не конкретный объект. Таким образом, приведенный выше код не приводит к появлению каких-нибудь объектов типа Box.
Чтобы действительно создать объект класса Box, нужно воспользоваться оператором new
Main.java
public class Main {
public static void main(String[] args) {
Box myBox = new Box();
}
}
После выполнения этого оператора объект myBox станет экземпляром класса Box. Таким образом, он обретет "физическое" существование.
Также следует напомнить, что каждый объект содержит собственную копию переменной экземпляра, которая определена в классе. Каждый объект типа Boxбудет содержать собственные копии переменных width, height и depth(рис. 4.2).
Изменения в переменных экземпляра одного объекта не влияют на переменные экземпляра другого объекта. Таким образом, каждый объект класса Box будет содержать собственные копии переменных width, height и depth. Для доступа к этим переменным служит оператор-точка (.). Эта операция связывает имя объекта с именем переменной экземпляра. Например, чтобы присвоить переменной width экземпляра myBox значение 100, нужно выполнить следующий оператор:
myBox.width = 100;
Этот оператор предписывает компилятору, что копии переменной width, хранящейся в объектe myBox, требуется присвоить значение 100. В общем, операция-точка служит для доступа как к переменным экземпляра, так и к методам в пределах объекта.
Ниже приведет пример программы, в которой используется класс Box
public class Main {
public static void main(String[] args) {
// Создаем объект типа Box
Box myBox = new Box();
// Присваиваем значения переменным экземпляра myBox
myBox.width = 10;
myBox.height = 20;
myBox.depth = 15;
// Рассчитываем объем коробки
double volume = myBox.width * myBox.height * myBox.depth;
System.out.println("Объем равен: " + volume);
}
}
public class Box {
double width;
double height;
double depth;
}
Как пояснялось ранее, каждый объект содержит собственные копии переменных экземпляра. Это означает, что при наличии двух объектов класса Box каждый из них будет содержать собственные копии переменных width, height и depth. Следует, однако, иметь ввиду, что изменения в переменных экземпляра одного объекта не влияют на переменные экземпляра другого. Например, в следующей программе объявлены два объекта класса Box:
public class Main {
public static void main(String[] args) {
Box myBox1 = new Box();
Box myBox2 = new Box();
// Присваиваем значения для mybox1
myBox1.width = 10;
myBox1.height = 20;
myBox1.depth = 15;
// Присваиваем значения для mybox2
myBox2.width = 3;
myBox2.height = 6;
myBox2.depth = 9;
double volume;
// объем первой коробки
volume = myBox1.width * myBox1.height * myBox1.depth;
// будет выведено 3000
System.out.println("Объем равен: " + volume);
// объем второй коробки
volume = myBox2.width * myBox2.height * myBox2.depth;
// будет выведено 162
System.out.println("Объем равен: " + volume);
}
}
Программа выводит следующий результат:
Объем равен: 3000.0
Объем равен: 162.0
Как видите, данные из объекта myBox1 полностью изолированы от данных, содержащихся в объекте myBox2.
Методы класса
Как упоминалось ранее, классы состоят из двух компонентов: переменных экземпляра и методов. Общая форма метода выглядит следующим образом:
[возвращаемый тип] имя ([список параметров]) {
[тело метода]
}
где возвращаемый тип означает конкретный тип данных, возвращаемый методом. Он может быть любым допустимым типом данных, в том числе и типом созданного класса. Если метод не возвращает значение, то его возвращаемым типом должен быть void. В качестве имени методов может быть любой допустимый идентификатор, кроме тех, которые уже используются другими элементами кода в текущей области действия. А список параметров обозначает последовательность пар "тип-идентификатор", разделенных запятыми. По существу, параметры - это переменные, которые принимают значения аргументов, передаваемых методу во время его вызова. Если у метода отсутствуют параметры, то список параметров оказывается пустым. Методы, возвращаемый тип которых отличается от void, возвращают значение вызывающей части программы с помощью оператора return.
Вернемся к нашему примеру с классом Box. Было бы логично, если бы расчет объема коробки выполнялся в классе Box, поскольку объем коробки зависит от ее размеров. Для этого добавим в класс Box метод getVolume()
В первой строке вызывается метод volume() для объекта myBox1. Следовательно, метод volume() вызывается по отношению к объекту myBox1, для чего было указано имя объекта, а вслед за ним - операция-точка. Таким образом, в результате вызова метода myBox1.volume() выводится объем коробки, определяемого объектом myBox1, а в результате вызова метода myBox2.volume() - объем коробки, определяемого объектом myBox2.
При вызове метода myBox1.volume() исполняющая система Jаvа передает управление коду, определенному в теле метода volume(). По окончании выполнения всех операторов в теле метода управление возвращается вызывающей части программы и далее ее выполнение продолжается со строки кода, следующей за вызовом метода. В самом общем смысле метод - это способ реализации подпрограмм в Java.
В методе volume() следует обратить внимание на еще одну очень важную особенность: ссылка на переменные экземпляра width, height и depth делается непосредственно без указания перед ними имени объекта или операции-точки. Когда в методе используется переменная экземпляра, определенная в его же классе, это делается непосредственно, без указания явной ссылки на объект и применения операции-точки . Это становится понятным, если немного подумать. Метод всегда вызывается по отношению к какому-то объекту его класса. Как только этот вызов сделан, объект известен. Таким образом, в теле метода вторичное указание объекта совершенно излишне. Это означает, что переменные экземпляра width, height и depth неявно ссылаются на копии этих переменных, хранящиеся в объекте, который вызывает метод volume().
Подведем краткие итоги. Когда доступ к переменной экземпляра выполняется из кода, не входящего в класс, где определена переменная экземпляра, следует непременно указать объект с помощью операции-точки. Но когда такой доступ осуществляется из кода, входящего в класс, где определена переменная экземпляра, ссылка на переменную может делаться непосредственно. Эти же правила относятся и к методам.
Следует обратить внимание, что метод getVolume() возвращает значение 3000, и это значение рассчитанного объема сохраняется в переменной vol. При обращении с возвращаемыми значениями следует принимать во внимание два важных обстоятельства:
тип данных, возвращаемых методом, должен быть совместим с возвращаемым типом, указанным в методе. Так, если какой-нибудь метод должен возвращать логический тип boolean, то возвратить из него целочисленное значение нельзя;
переменная, принимающая возвращаемое методом значение (например, vol), также должна быть совместима с возвращаемым типом, указанным для метода.
Создание объектов
Когда мы объявляем новый класс, то мы фактически создаем новый тип данных, который можно использовать для объявления объектов данного типа. Создание объектов класса представляет собой двухэтапный процесс. Сначала следует объявить переменную типа класса.
Box myBox;
Эта переменная является ссылочной, то есть она не содержит объект, а ссылается на него (примерно как указатель в C не содержит значение, а содержит адрес, то есть переменная ссылается на значение в памяти).
Затем нужно создать конкретный физический объект и получить на него ссылку. Эти операции выполняются с помощью оператора new. Этот оператор динамически (то есть, во время выполнения программы) резервирует память для объекта, инициирует процесс создания объекта и возвращает ссылку на него (ссылка представляет собой адрес объекта в памяти). Далее нам необходимо сохранить ссылку в переменной.
Box myBox;
mybox = new Box();
В первой строке переменная mybox объявляется как ссылка на объект типа Box. В данный момент mybox пока еще не ссылается на конкретный объект, значение переменной равно null. В следующей строке кода выделяется память для конкретного объекта, а переменной mybox присваивается ссылка на этот объект.
После выполнения второй строки кода переменную mybox можно использовать так, как если бы она была объектом типа Box. Но в действительности переменная mybox просто содержит адрес памяти конкретного объекта типа Box. Результат выполнения этих двух строк кода показан на рисунке.
Присвоение переменным ссылок на объекты
Какие действия выполняет приведенный ниже фрагмент кода?
Box b1 = new Box();
Box b2 = b1;
На первый взгляд, переменной b2 присваивается ссылка на копию объекта, на которую ссылается переменная b1. Таким образом, может показаться, что переменные b1 и b2 ссылаются на совершенно разные объекты, но это не так. После выполнения данного фрагмента кода обе переменные, b1 и b2, будут ссылаться на один и тот же объект. Таким образом, любые изменения, внесенные в объекте по ссылке в переменную b2, окажут влияние на объект, на который ссылается переменная b1, поскольку это один и тот же объект.
Передача аргументов подпрограмме
В общем случае, для передачи аргументов подпрограмме (в данном случае, методу) в языках программирования имеются два способа.
Первым способом является передача по значению. В этом случае значение аргумента копируется в параметр метода. Следовательно, изменения, вносимые в параметр метода, не оказывают никакого влияния на аргумент.
Вторым способом является передача по ссылке. В этом случае параметру передается ссылка на значение аргумента. Изменения, вносимые в параметр метода, будет оказывать влияние на аргумент, используемый при вызове.
Все аргументы в Java передаются по значению, но конкретный результат зависит от того, какой именно тип данных передается: примитивный или ссылочный.
Когда методу передается аргумент примитивного типа, его передача происходит по значению. В итоге создается копия аргумента, и все, что происходит с параметром, принимающим этот аргумент, не оказывает никакого влияния за пределами вызываемого метода.
int x = 10; // x равен 10
foo(x); // в метод копируется значение х
// работа метода не повлияет на переменную x
// она все равно будет равна 10
void foo(int x) {
x = x * 2;
}
При передаче объекта в качестве аргумента методу ситуация меняется коренным образом, поскольку объекты, по существу, передаются при вызове по ссылке. Не следует, однако , забывать, что при объявлении переменной типа класса создается лишь ссылка на объект этого класса. Таким образом, при передаче этой ссылки методу принимающий ее параметр будет ссылаться на тот же самый объект, на который ссылается и аргумент. По существу, это означает, что объекты действуют так, как будто они передаются методам по ссылке. Но изменения объекта в теле метода оказывают влияние на объект, указываемый в качестве аргумента.
Box mybox = new Box();
mybox.width = 100;
foo(mybox);
// переменная width объекта mybox будет равна 200
void foo(Box mybox) {
// В этом методе мы работаем с одним и тем же объектом
mybox.width = mybox.width * 2;
}
Конструктор
Основной причиной чрезмерных затрат в программировании является "небезопасное" программирование.
Основные проблемы с безопасностью относятся к инициализации и завершению. Очень многие ошибки при программировании на языке C обусловлены неверной инициализацией переменных. Это особенно часто происходит при работе с библиотеками, когда пользователи не знают, как нужно инициализировать компонент библиотеки или забывают это сделать.
В языке C++ впервые появляется понятие конструктора - специального метода, который вызывается при создании нового объекта.
В Java разработчик класса может в обязательном порядке выполнить инициализацию каждого объекта при помощи специального метода, называемого конструктором. Если у класса имеется конструктор, Java автоматически вызывает его при создании объекта, перед тем как пользователи смогут обратиться к этому объекту. Таким образом, инициализация объекта гарантирована.
Конструктор – это специальный метод, который вызывается при создании нового объекта.
Синтаксис конструктора отличается от синтаксиса обычного метода. Его имя совпадает с именем класса, в котором он находится, и он не имеет возвращаемого типа.
Как было сказано выше, оператор newдинамически выделяет оперативную память для создания объекта. Общая форма использования оператора newвыглядит следующим образом
переменная_типа_класса = new имя_класса();
Имя класса, за которым следуют круглые скобки, обозначает конструктор данного класса. Конструкторы являются важной частью всех классов и обладают множеством важных свойств
В большинстве классов, используемых в реальных программах, явно объявляются свои конструкторы в пределах определения класса
Инициализация всех переменных класса при каждом создании объекта – занятие довольно утомительное. В связи с этим, в Java разрешается выполнять собственную инициализацию при создании объектов. Такая инициализация осуществляется с помощью конструктора.
public class Box {
double width;
double height;
double depth;
public Box(double wd, double ht, double dt) {
width = wd;
height = ht;
depth = dt;
}
}
...
Box mybox = new Box(100, 200, 300);
Еще раз обратите внимание, что имя конструктора совпадает с именем класса, в котором он находится, а синтаксис аналогичен синтаксису метода. Также конструктор не имеет возвращаемого типа - даже типа void.
Большинство IDE для Java имеют механизм для генерации конструкторов. В IntelliJ IDEA нажмите комбинацию Alt+Insert находясь в окне редактирования java-файла. Откроется контекстное меню Generate, где вы можете выбрать генерацию конструктора, после чего указать поля для инициализации.
Теперь нам должно быть понятно, почему при создании нового объекта, после имени класса требуется указывать круглые скобки. В действительности оператор newвызывает конструктор класса.
Оператор newвызывает конструктор Box(). Но мы ранее не создавали этот конструктор, почему компилятор не выдал ошибку, когда мы запускали приложение?
Если в классе не определен конструктор, то в Java будет автоматически предоставлен конструктор по умолчанию.
Конструктор не получающий аргументов, называется конструктором по умолчанию (в документации Java он называется конструктор без аргументов).
Конструктор по умолчанию инициализирует все переменные экземпляра устанавливаемыми по умолчанию значениями, которые могут быть нулевыми, пустыми (null) и логическими (false) для числовых, ссылочных и логических типов соответственно. Зачастую конструктора по умолчанию оказывается достаточно для простых классов. Если же вы определите в классе хотя бы один конструктор, то конструктор по умолчанию создан не будет. Именно поэтому, следующий код выдаст ошибку.
Ключевое слово this
Представим, что у нас есть два объекта одного класса и для этих двух объектов вызывается один и тот же метод:
public class Box {
double width;
double height;
double depth;
public double getArea() {
return width * height * depth;
}
}
...
Box box1 = new Box();
Box box2 = new Box();
box1.getArea();
box2.getArea();
Если существует один метод getArea(), как метод узнает, для какого объекта он вызывается – для box_1 или дляbox_2?
Оказывается, при вызове метода getArea() (как и при вызове любого другого метода) передается скрытый первый аргумент – ссылка на используемый объект. Таким образом, вызовы методов на самом деле выглядят так:
Box.getArea(box1);
Box.getArea(box2);
Передача дополнительного аргумента относится к внутреннему синтаксису. При попытке явно воспользоваться ею компилятор выдаст сообщение об ошибке.
Предположим, во время выполнения метода нам необходимо получить ссылку на текущий объект. Так как эта ссылка передается компилятором скрытно, идентификатора для нее не существует. Но для решения этой задачи существует ключевое слово this.
Ключевое слово this может использоваться только внутри не-статического метода и предоставляет ссылку на объект, для которого был вызван метод.
Обращаться с ней можно точно так же, как и с любой другой ссылкой на объект. Для вызова метода класса из другого метода этого же класса, использовать ключевое слово thisне нужно.
Ключевое слово this чаще всего используется в ситуации, когда локальная переменная скрывает поле класса. В Java не допускается объявление двух локальных переменных с одним и тем же именем в той же самой области действия. Однако, мы можем объявить локальные переменные, имена которых совпадают с именами полей класса.
public class Box {
double width;
double height;
double depth;
// Пример сокрытия поля класса
void foo(double width) {
double height = 100;
// В консоль будет выведено значение локальных переменных
System.out.println(width);
System.out.println(height);
}
}
Когда имя локальной переменной совпадает с именем переменной экземпляра, локальная переменная скрывает поле класса.
Можно решить эту ситуацию путем изменения имен локальных переменных, но это некорректно с точки зрения хорошего стиля написания кода. Грамотным решением является использование ключевого слова this. Это позволит переменным иметь одинаковые названия, а к переменным экземпляра можно будет обратиться с помощью этого ключевого слова.
public class Box {
double width;
double height;
double depth;
// Пример сокрытия поля класса
void foo(double width) {
double height = 100;
// В консоль будет выведено значение локальных переменных
System.out.println(width);
System.out.println(height);
// Теперь мы обращаемся к переменным экземпляра
System.out.println(this.width);
System.out.println(this.height);
}
}
Иногда, во время выполнения метода необходимо получить ссылку на текущий объект, для которого был вызван метод. Так как ссылка на него передается скрытно, идентификатора для нее нет. Но для этого существует специальное ключевое слово – this. Ключевое слово thisпредоставляет ссылку на объект, для которого был вызван метод. Обращаться с ней можно как и с любой другой ссылкой на объект.
1. Принцип Separation of Concerns, контроллер и представление
Отделение графического интерфейса от бизнес-логики приложения. Язык FXML.
До этого момента мы создавали графический интерфейс напрямую в коде приложения, явно создавая объекты графического интерфейса, меняли их свойства, назначали слушатели событий и так далее.
Такой подход обладает рядом недостатков:
такой программный код превращается в череду созданий объектов и вызовов методов для их настройки, что затрудняет чтение кода и приводит к тому, что он выглядит не лучшим образом;
такой код тяжело отлаживать и исправлять ошибки при реализации графического интерфейса. Необходимо постоянно компилировать код и смотреть полученный результат в работающем приложении;
графический интерфейс, как правило, проектируется отдельным специалистом в области UI/UX, который может не знать языка, на котором этот интерфейс проектируется (в нашем случае, это Java). Кроме того, это приводит к тому, что java-программист, кроме программирования логики приложения, должен проектировать GUI, что приводит к дополнительным материальным затратам;
в случае необходимости изменения графического интерфейса (добавления нового функционала, изменения внешнего вида, анимации, эффектов), необходимо «перелопачивать» код и вносить туда изменения, что приводит к многочисленным ошибкам и тормозит процесс разработки программного обеспечения.
Основополагающим принципом разработки программного обеспечения является принцип separation of concerns
Исходя из этого, следует запомнить первое правило, которому должен следовать любой программист при разработке даже самого простого приложения: внешний вид (пользовательский интерфейс) приложения всегда должен быть максимально отделен от бизнес-логики работы приложения. Этот принцип еще кратко называют принципом отделения представления от содержания (эти принципы называются «Separation of concerns» или «Separation of presentation and content»).
В общем случае, принцип separation of concerns формулируется следующим образом - программа должна быть разделена на функциональные блоки, как можно меньше перекрывающие функции друг друга.
Повторим еще раз, что пользовательский интерфейс и внутренняя логика работы программы должны быть максимально независимы друг от друга. В идеале, вашей программе должно быть все равно – в каком виде она представлена пользователю – в виде GUI, консольного приложения, в окне браузера или еще как-то. В идеале, при изменении GUI, остальным классы программы остаются нетронутыми и изменения в них вносить не надо (в реальности, конечно, изменения вноситься будут, но они должны быть минимальны). Это помогает сделать программу более гибкой, а также помогает добиться разделения труда: программист пишет код, а дизайнер (верстальщик, специалист по UI/UX) занимается проектированием графического интерфейса.
Подавляющее большинство современных графических библиотек и фреймворков обладают тем или иным механизмом отделения представления от содержания.
Давайте разберемся, как это реализовано в JavaFX. Рассмотрим простой пример. Создадим пустое JavaFX-приложение
Main.java
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
В примере определен объект Stage, объект Scene и корневой элемент графа сцены. Очевидным следующим шагом было бы создание новых объектов графического интерфейса, установка свойств объектов и формирование графа сцены.
Но в нашем случае мы будем использовать специальный язык для проектирования графического интерфейса, который называется FXML.
Выполним несколько шагов, чтобы настроить работу IntelliJ IDEA для использования языка FXML:
зайдем в настройки IntelliJ IDEA (Ctrl+Alt+S или File -> Settings) и в пункте JavaFX укажем путь к exe-файлу установленного SceneBuilder;
перезапустим IntelliJ IDEA.
Откроем наш проект с JavaFX-приложением и создадим в проекте новый FXML файл sample.fxml.
Давайте разберемся, зачем мы создали этот файл и зачем нам нужен язык FXML. В JavaFX пользовательский интерфейс можно описать с помощью специального языка разметки, который называется FXML. Язык FXML является подмножеством языка XML и немного напоминает HTML.
Таким образом, вместо того, чтобы создавать объекты пользовательского интерфейса на языке Java в исходном коде программы, мы описываем граф сцены в виде fxml-файла, который выглядит определенным образом. Благодаря тому, что XML позволяет описать любые данные, мы можем очень точно описать объекты и их свойства, а древовидная структура XML идеально описывает граф сцены, который тоже имеет структуру дерева.
Откроем файл sample.fxml и посмотрим его содержимое
Как мы видим, текст файла напоминает формат XML, в котором присутствуют некоторые элементы языка Java (в частности, подключение библиотек с помощью import).
Опишем граф сцены из нескольких элементов, после чего разберем, что происходит в получившемся коде.
строка 1 – в ней содержится так называемая XML-декларация. Она описывает версию XML и кодировку;
строки 3 – 6 отдаленно напоминают команды импорта в java, которые заключены между символами <? и ?>. Такая конструкция называется инструкцией обработки (Processing Instruction, PI), фактически, это инструкция тому приложению, которое будет «читать» XML-файл. Фактически, это означает инструкцию – подключить те или иные библиотеки;
строки 8-10 определяет корневой элемент графа сцены – элемент HBox. В качестве атрибутов xmlns указаны пространства имен для тэгов FXML. Все остальные элементы размещаются внутри HBox, что в точности соответствует структуре дерева;
в строке 11 указан элемента <children>. Потомки того или иного элемента содержатся внутри элемента <children>, что напоминает метод getChildren(), который мы используем для добавления потомков элемента;
свойства элементов (высота, ширина, текст надписи и так далее) описываются в виде атрибутов тех или иных элементов.
Таким образом, данное описание довольно понятно и легко читается человеком, который хоть сколько-нибудь знаком с форматом XML и с элементами JavaFX.
Граф сцены, описанный в файле sample.fxml с помощью традиционного способа создания объектов Java в исходном коде выглядит следующим образом:
@Override
public void start(Stage primaryStage) throws Exception {
HBox hBox = new HBox();
hBox.setPrefHeight(400);
hBox.setPrefWidth(600);
Button button = new Button("Кнопка");
Label label = new Label("Текст");
VBox vBox = new VBox();
vBox.setPrefHeight(200);
vBox.setPrefWidth(100);
vBox.getChildren().add(label);
hBox.getChildren().addAll(button,vBox);
Scene scene = new Scene(hBox, 300, 200);
primaryStage.setTitle("JavaFX-приложение");
primaryStage.setScene(scene);
primaryStage.show();
}
Визуальные редакторы UI
Использование специального языка описания FXML дает нам возможность использовать специальные визуальные редакторы, которые позволяют нам проектировать граф сцены в визуальном режиме.
В качестве визуального редактора мы будем использовать редактор SceneBuilder, который мы установили и подключили выше.
При работе с fmxl-файлами вы можете легко переключаться между текстовым и визуальным представлением файла, редактировать код, который сгенерировал визуальный редактор и видеть результат без необходимости каждый раз компилировать и запускать приложение.
Использование редактора очень сильно облегчает и ускоряет разработку графических приложений. Кроме того, при работе с fxml очень удобно подключать css-стили и даже вызывать скрипты на различных языках программирования, например, например, Groovy, Clojure и даже JavaScript (данный вопрос в рамках этого курса не рассматривается).
Используя визуальный редактор, создадим граф сцены, который визуально будет выглядеть следующим образом
В формате XML этот код будет выглядеть следующим образом:
У нас есть готовый граф сцены, но что дальше? Как известно, в Java всё является объектами некоторых классов, в том числе и элементы графического интерфейса.
До этого мы просто создавали в исходном коде объекты нужных нам классов, после чего вызывали их методы для формирования нужного нам графа сцены. Но у нас только текстовое описание графа сцены в формате FXML, как его преобразовать в набор объектов, вызвать нужные методы этих объектов и сформировать граф сцены?
Генерация объектов графа сцены. ЗагрузчикFXML
Для решения вышеуказанной проблемы было придумано много различных методик и подходов, которые были реализованы в различных графических библиотеках в различных языках программирования.
Фреймворк JavaFX использует распространенный подход, который состоит в генерации объектов. Его суть состоит в следующем: в JavaFX существует специальный класс FXMLLoader (загрузчик FXML), который содержит статический метод load(). Этот метод реализует следующий функционал:
метод считывает fxml-файл, URL которого вы должны указать;
метод «парсит» fxml-файл (разбивает файл на отдельные элементы с атрибутами);
используя результаты парсинга и механизм рефлексии (механизм рефлексии в данном курсе не рассматривается), метод создает объекты, устанавливает их свойства и помещает один объект в состав других объектов и таким образом формирует нужный нам граф сцены;
в качестве возвращаемого значения, метод load() возвращает ссылку на корневую вершину (root node), которая прямо или опосредованно содержит в себе весь сгенерированный граф сцены.
Вернемся к нашему исходному коду. Используя полеченные знания о работе класса FXML, выполним следующие шаги:
ссылку на корневой элемент графа сцены передадим в созданный объект сцены.
Полученный код выглядит следующим образом.
Main.java
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root =
FXMLLoader.load(getClass().getResource("sample.fxml"));
Scene scene = new Scene(root, 300, 200);
primaryStage.setTitle("JavaFX-приложение");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Запустим приложение и убедимся, что сгенерированный граф сцены соответствует тому, что мы визуально спроектировали в SceneBuilder.
Таким образом, мы визуально спроектировали граф сцены для нашего окна, после чего класс FXMLLoader сгенерировал нужный граф сцены в объектном виде.
Но сразу же возникает следующая проблема, каким образом получить ссылки на эти сгенерированные объекты? Если мы хотим указать обработчик событий для элементов графического интерфейса или динамически изменить свойства некоторых элементов – нам нужна ссылка на объекты элементов графического интерфейса.
Когда мы создавали объекты самостоятельно в исходном коде классов, мы хранили ссылку на них и могли к ним обратиться. Но в нашем случае, объекты были сгенерированы в недрах класса FXMLLoader, упакованы один в другой и всё, что у нас есть – ссылка на корневой элемент графа сцены.
Мы могли бы попытаться получить ссылку на нужный элемент через объект корневого элемента графа сцены, но это сильно затруднит написание обработчиков событий и бизнес-логики приложения в целом.
Контроллер графа цены. Получения ссылок на объекты графа сцены.
Получение ссылки на сгенерированный элемент графического интерфейса – это одна из проблем, которая возникает, когда объекты графического интерфейса создаются не вручную. В различных графических библиотеках и в разных языках программирования эта проблема решается по-разному. Давайте рассмотрим, как эта проблема решается в JavaFX.
При описании интерфейса, в fxml-файле вы можете указать т.н. контроллер – специальный класс, который будет «управлять» объектами, которые описаны вfxml-файле. Почему этот класс называется контроллером и что такое вообще «контроллер» (вы, наверняка, слышали этот термин и даже что-то читали по этому поводу), мы рассмотрим при изучении архитектурного паттерна MVC.
Класс-контроллер это обычный java-класс, который должен реализовывать интерфейс javafx.fxml.Initializable, который определяет всего один метод initialize(). Создадим класс SampleController и пока оставим его пустым.
SampleController.java
public class SampleController implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
}
}
Дальнейший шаг – в fxml-файле необходимо указать, что для данного графа сцены будет использоваться этот класс контроллера. Эта информация будет считана FXMLLoader при парсинге fxml-файла.
Откроем файл sample.fxml и в корневом узле графа сцены укажем атрибут fx:controller = ”ua.opu.SampleController”.
Таким образом, мы заявляем, что для данного fxml-файла будет использоваться указанный класс контроллера. Этот атрибут можно использовать только в корневом узле fxml-файла.
Чтобы отличить один объект элемента UI от других, необходимо присвоить элементам UI различные идентификаторы, используя которые мы сможем обратиться к тому или иному элементу. Для того чтобы установить идентификатор для элемента, необходимо указать атрибут fx:id в коде либо в свойствах в визуальном редакторе.
Идем дальше. Теперь я хочу каким-то образом в классе контроллера получить ссылку на первые две кнопки и как-то ними дальше манипулировать. Как мне это сделать? Чтобы получить доступ к каким-то элементам, мне нужно этим элементам присвоить определенный id. Вы можете сделать это либо в визуальном редакторе (выделите нужный элемент, зайдите во вкладку Code: и установите значение в поле fx:id).
Для данного примера присвоим идентификаторы для двух кнопок: button1 и button2.
Нам необходимо получить ссылки на объекты этих кнопок. Для этого зайдем в класс контроллера и создадим два поля типа javafx.scene.control.Button, с названиями, которые точно совпадают с идентификаторами этих кнопок вfxml-файле. После чего, укажем перед каждым полем аннотацию @FXML. В итоге, класс контроллера будет иметь следующий вид
SampleController.java
public class SampleController implements Initializable {
@FXML
private Button button1;
@FXML
private Button button2;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
}
Еще раз обратите внимание, что fx:id и имя переменной должно совпадать точно и перед каждым объявлением должно быть указано @FXML.
Давайте разберемся, что происходит при указании контроллера и полей с аннотацией @FXML:
класс FXMLLoader парсит fxml-файл;
загрузчик «считывает» атрибут fx:controller и создает объект этого класса;
загрузчик парсит аннотации @FXML и внедряет в эти поля ссылки на созданные объекты (используется механизм рефлексии);
загрузчик вызывает метод initialize() (в этом методе мы прописываем все наши манипуляции с нужными элементами GUI).
Таким образом, если мы правильно указали fx:id и не ошиблись с классами и названиями полей, то в момент вызова метода initialize(), наши поля button1 и button2 будут содержать ссылки на наши две кнопки.
Добавим обработчик нажатия на кнопку 1
SampleController.java
public class SampleController implements Initializable {
@FXML
private Button button1;
@FXML
private Button button2;
@Override
public void initialize(URL location, ResourceBundle resources) {
button1.setOnAction(event -> {
new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
});
}
}
Всё прошло успешно, мы имеем ссылки на нужные нам элементы, и мы можем делать с ними что захотим.
Для второй кнопки реализуем слушатель иначе. В классе-контроллере создадим метод handleButton2() и тоже пометим его аннотацией @FXML.
SampleController.java
public class SampleController implements Initializable {
@FXML
private Button button1;
@FXML
private Button button2;
@FXML
protected void handleButton2(ActionEvent event) {
new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 2").showAndWait();
}
@Override
public void initialize(URL location, ResourceBundle resources) {
button1.setOnAction(event -> {
new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
});
}
}
То есть, мы видим, что мы можем помечать этой аннотацией не только поля, но и методы. Далее, зайдем в fxml-файл, в режим визуального редактора, выделите вторую кнопку, зайдите во вкладку Code: и в поле OnAction из выпадающего списка выберите нужный метод
В текстовом виде это выглядит следующим образом
Компилируем приложение и смотрим результат
Как вы уже, наверное, поняли, фактически это означает следующее «если для кнопки2 произошло событие Action – вызови метод handleButton2(). Таким образом, если у вас много слушателей, то удобнее просто закодировать методы и расставить нужные методы для нужных событий в нужных элементах, и не париться лишний раз с полями.
3. Клиент-серверная архитектура. Создание простой RESTful веб-службы с помощью Spring Boot.
Клиент-серверная архитектура
Сервер - компьютер или программа, которая управляет ресурсами (информация, файлы, база данных) называется сервером этого ресурса или просто сервером.
Архитектура "клиент-сервер" определяет общие принципы организации взаимодействия, где имеются серверы (узлы-поставщики некоторых специфичных функций и сервисов) и клиенты, (потребители этих сервисов).
Между клиентами и серверами должны быть установлены правила взаимодействия, которые называются протоколом взаимодействия или протоколом обмена. Каждая часть взаимодействует друг с другом, обмениваясь сообщениями в заранее согласованном формате.
В рамках данного курса рассматривается так называемая "трехзвенная архитектура"
Компоненты трехзвенной архитектуры:
клиент - этот компонент отвечает за представление данных конечному пользователю;
выделенный сервер приложений - здесь содержится бизнес-логика приложения;
сервер БД - предоставляет запрашиваемые данные.
Сервер приложений (application server) – сервисная программа, которая обеспечивает доступ клиентов к прикладным программам, выполняющимся на сервере.
Большинство серверов приложений имеют в своем составе веб-сервер. Это означает, что сервер приложений может делать все, на что способен веб-сервер. Кроме того, сервер приложений имеет компоненты и функции для поддержки сервисов уровня приложения, таких как пул соединений, поддержка транзакций и так далее.
Технология Java Servlets
На заре развития интернета не существовало технологий для создания динамических веб-страниц. В то время сайт представлял собой набор статических заранее написанных и сверстанных страниц с помощью языка разметки HTML. Если владелец сайта хотел обновить информацию на страничке, он делал это непосредственно на своем компьютере, после чего загружал на сервер обновленную версию HTML-страницы.
Среди предложенных решений по созданию динамических страниц, одной из первых была технология Java Servlets. В то время это была революционная технология, которая позволяла расширить возможности веб-серверов на основе модели запрос-ответ (request - response). Технология сервлетов позволяла веб-серверам обрабатывать HTTP-запросы и динамически генерировать веб-странички в зависимости от HTTP-запроса.
Несмотря на почтенный возраст, технология сервлетов претерпела серьезные изменения для того, чтобы соответствовать современной технологии разработки веб-приложений. На данный момент, технология сервлетов является наиболее часто используемой технологией для обработки HTTP запросов/откликов. Кроме того, сервлеты являются базой для почти всех Java-фреймворков, которые работают с HTTP протоколом (JSF, Struts, Spring MVC, BIRT и так далее).
Сервлет (Servlet), по сути, является классом Java, который используется для расширения возможностей сервером, предназначенных для размещения приложений. Сервлеты могут отвечать на запросы и генерировать отклики. Базовым классом для всех сервлетов является класс javax.servlet.GenericServlet. Этот класс определяет обобщенный, независимый от протокола сервлет.
Схема работы технологии сервлетов представлена на рисунке ниже
клиент (например, веб-браузер) передает HTTP-запрос веб-серверу. В случае, если от веб-сервера требуется предоставить статический файл или какой-то ресурс (например, изображение), то он просто возвращает требуемый статический файл или ресурс;
если веб-сервер не может самостоятельно обработать HTTP-запрос (например, пользователь передает какие-то данные либо требуется предоставить динамическую страницу и так далее), веб-сервер передает этот запрос web-контейнеру (его еще называют servlet-контейнером);
контейнер определяет – какой сервлет может выполнить этот запрос, создает объекты классов HttpServletRequest и HttpServletResponse, создает thread, создает объект класса сервлета и передает ему объекты классов HttpServletRequest и HttpServletResponse;
Контейнер вызывает метод сервлета service(), который вызывает соответствующий HTTP-запросу метод (например, если запрос был HTTPGET, то будет вызван метод doGet(), подробнее этот вопрос будет разбираться далее), которому, в качестве аргументов, передает объекты классов HttpServletRequest и HttpServletResponse;
Соответствующий метод (например, метод doGet()) возвращает динамическую страницу внутри объекта класса HttpServletResponse, ссылку на который имеет контейнер;
После этого поток завершается, контейнер конвертирует объект класса HttpServletResponse в HTTP-отклик (HTTP response) и отдает его веб-серверу, который возвращает его клиенту.
Фреймворк Spring
Spring – свободно-распространяемый легковесный фреймворк, призванный упростить разработку корпоративных и веб-приложений (можно использовать и для любых других типов приложений) на языке Java (является альтернативной стеку Jakarta EE).
В данный момент Spring представляет собой целый набор модулей, которые можно использовать выборочно для тех или иных проектов.
Дадим краткую характеристику некоторым модулямSpring:
SpringCore – ядро платформы, предоставляет базовые средства для создания приложений — управление компонентами (бинами, beans), внедрение зависимостей, MVC фреймворк, транзакции, базовый доступ к БД. В основном это низкоуровневые компоненты и абстракции. По сути, неявно используется всеми другими компонентами;
SpringMVC – обеспечивает архитектуру паттерна Model-View-Controller при помощи слабо связанных готовых компонентов для разработки веб-приложений;
Spring Data – обеспечивает доступ к данным: реляционные и нереляционные БД, KV хранилища и т.п.;
Spring Cloud – используется для микросервисной архитектуры;
Spring Security – авторизация и аутентификация, доступ к данным, методам и т.п. OAuth, LDAP, и различные провайдеры.
SpringBoot
Проект SpringBoot – решение, которое позволяет вам легко создавать полноценные приложения Spring, про которые можно сказать «просто запусти».
Spring Boot позволяет быстро создать и сконфигурировать (т.е. настроить зависимости между компонентами) приложение, упаковать его в исполняемый самодостаточный артефакт. Это то связующее звено, которое объединяет вместе набор компонентов в готовое приложение.
ОсобенностиSpring Boot:
создание полноценных Spring-приложений;
встроенный сервлет-контейнер (Tomcat или Jetty);
обеспечивает начальные pom-файлы для упрощения конфигурации Maven;
используется автоконфигурация, где это возможно;
используется принцип «convention over configuration». Для большинства конфигураций не нужно ничего настраивать.
Изучение фреймворка Spring лучше всего начать с установки требуемого программного обеспечения и разработки тестового приложения с помощью Spring Boot.
Веб-сервисы
Всемирная паутина является готовой платформой для создания и использования распределенных систем на основе веб-служб. Веб-сервер выступает в качестве сервера приложений, к которым обращаются не конечные пользователи, а сторонние приложения. Это позволяет многократно использовать функциональные элементы, устранить дублирование кода, упростить решение задач интеграции приложений.
Веб-служба или веб-сервис (web-service) – сетевая технология, обеспечивающая межпрограммное взаимодействие на основе веб-стандартов. W3C определяет веб-службу как «программную систему, разработанную для поддержки интероперабельного межкомпьютерного (machine-to-machine) взаимодействия через сеть».
К моменту появления веб-служб уже существовали технологии, позволяющие приложениям взаимодействовать на расстоянии, где одна программа могла вызвать какой-нибудь другой метод в другой программе, которая при этом могла быть запущена на компьютере, расположенном в другом городе или даже стране. Это сокращенно называется RPC(RemoteProcedureCalling– удаленный вызов процедур). В качестве примеров можно привести технологии CORBA, а для Java – RMI (Remote Method Invoking – удаленный вызов методов).
Идея веб-службы заключалась в создании такого RPC, который будет упаковываться в HTTP пакеты. Такой подход стал очень популярным, т.к. HTTP был хорошо известен, прост, понятен и обеспечивал лучшее «прохождение» через различные firewall`ы. Именно с появлением веб-сервисов развилась идея SOA– сервис-ориентированной архитектуры веб-приложений (Service Oriented Architecture).
Протокол HTTP
Протокол HTTP лежит в основе обмена данными в Интернете. HTTP является протоколом клиент-серверного взаимодействия, что означает инициирование запросов к серверу самим получателем (браузером или другим клиентским приложением).
Клиенты и серверы взаимодействуют, обмениваясь одиночными сообщениями (а не потоком данных). Сообщения, отправленные клиентом называются запросами, а сообщения, отправленные сервером, называются ответами.
HTTP - это клиент-серверный протокол, то есть запросы отправляются какой-то одной стороной - участником обмена (user-agent). Чаще всего в качестве участника выступает веб-браузер, но им может быть кто угодно.
Каждый запрос (request) отправляется серверу, который обрабатывает его и возвращает ответ (response).
Участник обмена (user agent) - это любой инструмент или устройство, действующее от лица пользователя.
На другой стороне коммуникационного канала расположен сервер, который обслуживает (serve) пользователя, предоставляя ему документы по запросу. С точки зрения конечного пользователя, сервер всегда является некой одной виртуальной машиной, полностью или частично генерирующий документ, хотя фактически он может быть группой серверов, между которыми балансируется нагрузка, то есть перераспределяются запросы различных пользователей, либо сложным программным обеспечением, опрашивающим другие компьютеры.
HTTP-запросы (HTTP-request)
Пример HTTP запроса
Запросы содержат следующие элементы:
HTTP-метод, обычно глагол подобно GET, POST или существительное, как OPTIONS или HEAD, определяющее операцию, которую клиент хочет выполнить. Обычно, клиент хочет получить ресурс (используя GET) или передать значения HTML-формы (используя POST), хотя другие операции могут быть необходимы в других случаях;
путь к ресурсу;
заголовки (опционально), предоставляющие дополнительную информацию для сервера;
для некоторых методов, таких как POST, тело метода, которое содержит отправленный ресурс.
HTTP-ответы
Пример HTTP-ответа
Ответы содержат следующие элементы:
версию HTTP-протокола;
HTTP код состояния, сообщающий об успешности запроса или причине неудачи;
сообщение состояния - краткое описание кода состояния;
Код состояния - это трехзначное число, которое отдает сервер на запрос клиента и благодаря которому корректируется дальнейшая обработка запрашиваемого документа. За числом всегда идет краткое пояснение кода на английском языке, отделенное пробелом - первичная инструкция клиенту.
Классы состояния - группа кодов, объединенных определенными признаками. На класс состояния указывает первая цифра в коде.
Выделяют пять классов:
1ХХ - информационные кода. Они отвечают за процесс передачи данных. Это временные коды, они информируют о том, что запрос принят и обработка будет продолжаться;
2ХХ - успешная обработка. Запрос был получен и успешно обработан сервером;
3ХХ - перенаправление (редирект). Эти ответы сервера гласят, что нужно предпринять дальнейшие действия для выполнения запроса. Например, сделать запрос по другому адресу;
4ХХ - ошибка клиента. Это значит, что запрос не может быть выполнен на стороне клиента;
5ХХ - ошибка сервера. Эти коды возникают из-за ошибок на стороне сервера. В данном случае клиент сделал все правильно, но сервер не может выполнить запрос. Для кодов этого класса сервер обязательно показывает сообщение, что не может обработать запрос и по какой причине.
Протоколы веб-сервисов
На сегодняшний день наибольшее распространение получили следующие протоколы реализации веб-служб:
SOAP (Simple Object Access Protocol) – тройка стандартов SOAP/WSDL/UDDI. Сообщения упаковываются в виде структуры, которая называется конверт (envelope), которая включает идентификатор сообщения, заголовок и тело сообщения;
REST (Representational State Transfer) – архитектурны стиль, который использует концепцию ресурсов и определяет операции через методы HTTP-протокола;
XML-RPC (XML Remote Procedure Call) – вызов удаленных процедур, использующий XML для кодирования своих сообщений и HTTP в качестве транспортного механизма.
ПротоколREST
Передача состояния представления (Representational State Transfer (REST)) является архитектурным стилем, в котором веб-службы рассматриваются, как ресурсы и могут быть идентифицированы Унифицированными идентификаторами ресурсов (Uniform Resource Identifiers (URI)).
Веб-службы, разработанные в стиле REST и с учетом ограничений REST, известны как RESTfulвеб-службы.
Каждая единица информации в REST называется ресурсом и имеет однозначный URI, который является ее, своего рода, первичным ключом. То есть, например, третья книга с книжной полки будет иметь URI /book/3, а 35ая страница в этой книге – /book/3/page/35/. Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /book/3/page/35/ – это может быть и HTML, и отсканированная копия книги в виде jpeg-файла и документ Microsoft Word.
Над ресурсами выполняется ряд простых четко определенных операций. В качестве протокола передачи данных используется stateless-протокол, обычно HTTP.
При использовании протокола HTTP действия над данными выполняются с помощью HTTP-методов: GET (получить), PUT (добавить, заменить), POST (добавить, изменить, удалить), DELETE (удалить). Таким образом, действия CRUD (Create-Read-Update-Delete) могут выполняться как со всеми 4-мя методами, так и только с помощью GET и POST. Примеры запросов:
GET /book/ – получить список всех книг;
GET /book/3 – получить книгу номер 3;
PUT /book/ – добавить книгу (данные в теле запроса);
POST /book/3 – изменить книгу (данные в теле запроса);
DELETE /book/3 – удалить книгу.
Как правило, необязательно поддерживать все методы, но, как правило, веб-служба должна поддерживать:
GET – используется для получения существующих ресурсов;
POST – используется для создания/обновления нового ресурса;
PUT – используется для обновления/замены ресурса;
DELETE – используется для удаления ресурса.
Кроме этого, служба может поддерживать такие методы как PATCH (обновление части ресурса), HEAD (возвращение заголовка ресурса, т.е. метаданных) и т.д.
Практическая часть
Установка программного обеспечения
Для выполнения домашнего задания нам понадобится следующее программное обеспечение:
Создание Spring Boot проекта
Существует несколько способов создать Spring Boot проект. Из наиболее простых способов можно выделить:
создание проекта средствами IDE.
Создадим проект с помощью мастера Intellij IDEA. Создадим новый Spring Boot проект (выберите пункт Spring Initializr). Необходимо указать JDK, метаданные проекта, а также выбрать из списка модулей нужные нам модули Spring
Для выполнения задания нам необходимо выбрать web-модуль. Панель выбранных компонентов будет иметь следующий вид:
После окончания работы мастера создания проектов, мы получим стартовый проект Spring Boot. Рассмотрим структуру проекта и обозначим ключевые файлы:
HotelApplication.java - стартовый класс Spring Boot приложения;
application.properties - файл с настройками приложения. В нем можно переопределить настройки по умолчанию;
pom.xml - POM-файл проекта. Используется сборщиком Maven.
POM-файл (Project Object Model) – это XML-файл, который содержит информацию о деталях проекта, и конфигурации для создания проекта на Maven. Он всегда находится в базовом каталоге проекта. Во время выполнения задач, Maven ищет pom-файл в базовой директории проекта. Он читает его и получает необходимую информацию, после чего выполняет задачи.
Корневым элементом является элемент <project>. Внутри тега project содержится основная и обязательная информация о проекте.
Зависимости (dependency) – это те библиотеки, которые непосредственно используются в проекте для компиляции кода или его тестирования.
Мы создаем RESTful веб-службу с помощью Spring Boot, поэтому нам нужно «подтянуть» для нашего проекта различные Spring-модули (библиотеки с классами, jar-файлы).
В обычных проектах нам бы было необходимо добавлять каждую зависимость вручную, но Spring Boot позаботился о нас и предоставил нам своего рода «мета-зависимости». Смысл их в том, что Spring Boot понимает, что если вы создаете web-приложение то вам нужен примерно одинаковый набор jar-файлов, поэтому чтобы не писать каждый jar-файл отдельно, мы указываем одну зависимость, а она уже «подтянет» за нас другие отдельные зависимости для создания веб-приложения.
Запуск приложения
Теперь давайте сразу запустим приложение. Убедимся, что приложение запущено успешно
перейдем в браузер и попробуем зайти на сайт.
Как видите, Spring Boot приложение успешно запущено. Так как Spring Boot берет на себя большую часть рутинной работы по созданию и запуску приложения, давайте разберемся, что же происходит, когда мы запускаем приложение:
Устанавливается конфигурация приложения по умолчанию;
Запускается контекст приложенияSpring(Springapplicationcontext) – это контейнер для кода, который работает на сервере (службы, контроллеры и т.д.). Все приложения Spring имеют этот контекст, который запускается при запуске приложения. Spring Boot создает этот контекст при запуске приложения;
Выполняется сканирование пути к классам (class path scan). Чтобы добавить код в Spring Boot, необходимо создать свои классы и аннотировать их определенным образом. Например, если вы хотите добавить контроллер, вы создаете класс и аннотируете его с помощью аннотации @Controller и так далее. То есть, вы как бы помечаете ваши классы, что это контроллер, это сервис, это еще что-то. Spring сканирует эти классы и, в зависимости от нашего маркера, он работает с этими классами по-разному. То есть Spring сканирует ваш код и ищет классы с этими аннотациями (помимо маркеров, обычно в аннотациях содержатся другие метаданные, которые дают уточняющую информацию для Spring);
Запускается Tomcat-сервер. Мы как раз зашли на сервер через URL и получили страницу с ошибкой, так как на сервере не был предусмотрен обработчик запроса с таким URL. Мы не скачивали Tomcat и не устанавливали его – все за нас сделал Spring Boot.
Структура enterprise-приложения Spring
Простое приложение Spring имеет трехслойную структуру:
Weblayer – верхний слой приложения. Он отвечает за обработку ввода пользователя и возврат корректного ответа. Также веб-слой отвечает за обработку исключений, которые могут выбрасываться в других слоях приложения. Так как веб-слой является точкой входа в приложение, он также отвечает за аутентификацию и является первой линией защиты приложения;
Servicelayer – слой сервисов, находится ниже веб-слоя. Этот слой содержит сервисы приложения и инфраструктуры. Сервисы приложения предоставляют публичный API сервисного слоя. Они также отвечают за транзакции и авторизацию. Инфраструктурные сервисы содержат код для взаимодействия с внешними ресурсами, такими как файловая система, базы данных, почтовые сервера и так далее. Часто эти сервисы используются несколькими сервисами приложения;
Repositorylayer – самый нижний слой приложения. Он отвечает за взаимодействие с используемыми хранилищами данных.
Разработка Web Layer
Для обработки запросов и возврата данных необходимо предусмотреть соответствующие контроллеры REST-запросов, которые и будут составлять наш веб-слой.
Контроллер – это java-класс, методы которого призваны обрабатывать HTTP-запросы. Отличие обычного контроллера от REST-контроллера заключается в том, что в REST-контроллере каждый метод класса возвращает данные вместо представления. Рассмотрим пример простого REST-контроллера. Создадим в проекте пакет controllers, внутри которого создадим класс HelloController.
HelloController.java
@RestController
public class HelloController {}
Обратите внимание, что мы пометили класс аннотацией @RestController. Таким образом, мы даем знать Spring, что это не просто класс, а контроллер REST-запросов. В классе создадим метод, который будет возвращать строку.
HelloController.java
@RestController
public class HelloController {
public String hello() {
return "hello";
}
}
Говорят, что методы контроллера «отображаются» на HTTP-запросы. Это значит, что при поступлении определенного HTTP-запроса (с определенным URL и HTTP-методом), будет вызван определенный метод контроллера, который вернет некоторые данные. Этим данные будут упакованы в HTTP-ответ и высланы обратно клиенту.
Нам необходимо сделать так, чтобы наш созданный метод был вызван, когда на сервер поступит HTTP-запрос с определенным URL, например http://localhost:8080/hello. Для этого необходимо пометить метод аннотацией @GetMapping c параметром (“/hello”) – часть URL, на который будет отображаться данный метод.
HelloController.java
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
Для каждого из четырех основных HTTP-метода предусмотрена своя аннотация (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping). Метод, помеченный определенной аннотацией, обрабатывает запросы только с определенным HTTP-методом.
Что произошло? Строка «hello» была помещена в тело HTTP-ответа, браузер получил text/plain с содержимым «hello» и просто вывел его на экран.
Передача параметров в REST-запросах
Очень часто клиенту необходимо вместе с запросом передать некоторые параметры запроса, которые уточняют и конкретизируют запрос.
Параметры запроса можно передать несколькими способами. Рассмотрим следующие способы:
указание параметра в URL-пути (localhost:8080/rooms/256);
указание параметра в строке запроса, которая идет после URL-пути и отделяется символом ? (localhost:8080/rooms?id=256¶m2=value2);
передача параметров в теле запроса (часто используется для передачи заполненной пользователем формы или передачи данных в формате JSON).
Рассмотрим, каким образом можно получить и обработать параметры запроса, переданные тем или иным способом.
Указание параметра в URL-пути
При создании endpoint, в аннотации необходимо указать вариативную часть и назначить ей идентификатор
@GetMapping("/room/{id}")
public void getRoomById() {
}
Далее необходимо предусмотреть входной аргумент метода, куда Spring запишет значение вариативной части и указать аннотацию @PathVariable для этой переменной. Также необходимо указать идентификатор, который вы указали в аннотации @GetMapping.
@GetMapping("/room/{id}")
public void getRoomById(@PathVariable(value = "id") int roomId) {
// ...
}
В рамках одного запроса может быть несколько вариативных частей, которые можно считать и обработать
@GetMapping("/room/{id1}/{id2}")
public void getRoomById(@PathVariable(value = "id1") int blockId, @PathVariable(value = "id2") int roomId) {
// ...
}
Указание параметра в строке запроса
В этом случае, для каждого параметра запроса создается входной аргумент, указывается аннотация @RequestParam, а также указывается имя параметра.
// запрос: http://localhost:8080/room?room_id=250&block_id=10
@GetMapping("/room")
public void getRoomById(@RequestParam(value = "room_id") int roomId, @RequestParam(value = "block_id") int blockId) {
// ...
}
Передача параметров в теле запроса
Если в качестве клиента выступает браузер пользователя, данные от клиента на сервер передаются в виде полей формы, которые заполняет пользователь браузера. В этом случае параметры передаются в теле запроса с помощью метода POST.
Форма может иметь следующие MIME-типы:
multipart/form-data: каждое значение посылается как блок данных ("body part"), с заданными пользовательским клиентом разделителем ("boundary"), разделяющим каждую часть. Эти ключи даются в заголовки Content-Disposition каждой части text/plain.
Для обработки данных формы необходимо создать входной аргумент для каждого параметра, для каждого входного аргумента указать аннотацию @RequestParam, а также имя параметра.
@PostMapping("/book")
public void bookRoom(@RequestParam(value = "room_id") int room_id,
@RequestParam(value = "firstname") String firstname,
@RequestParam(value = "lastname") String lastname,
@RequestParam(value = "days") int days) {
// ...
}
Существует несколько более простых способов получения данных формы, но в данном курсе они не рассматриваются. Вышеуказанный способ является самым простым и понятным на данном этапе изучения курса.
Передача данных между клиентом и сервером.
Так как язык Java является ОО языком, нам было бы удобно работать с входящими и исходящими данными в объектном виде - было бы здорово, если бы REST-контроллер возвращал бы данные в виде объекта некоторого класса, а не в виде набора полей со значениями. Также было бы здорово, чтобы мы могли просто возвращать клиенту объект или коллекцию объектов некоторых классов без необходимости формировать Map из полей и значений.
Для реализации этого функционала, в Spring используется механизм сериализации и десериализации.
Сериализация - это преобразование объекта в последовательность байтов, так что объект можно легко сохранить в постоянное хранилище или передать по каналу связи. Затем поток байтов можно десериализовать - преобразовать в реплику исходного объекта.
Язык Java предоставляет стандартный механизм Java Serialization API для создания сериализуемых объектов, однако, он нам не подходит, так как ограничивает возможности для использования различных языков и технологий на стороне клиента и сервера.
Мы можем использовать сторонние библиотеки для сериализации объекта с помощью формата XML или JSON.
Библиотека Jackson позволяет гибко настроить процесс сериализаци и десериализации, однако, в рамках данного курса мы будем использовать стандартные механизмы сериализации и десериализации, чтобы уделять этому процессу как можно меньше внимания.
Рассмотрим ситуацию, когда нам необходимо вернуть клиенту данные в объектном виде. Создадим класс с несколькими полями, создадим объект и вернем его в качестве результата GET-запроса.
@GetMapping("/roominfo")
public Room getRoomInfoById(@RequestParam(value = "room_id") int roomId) {
return new Room(roomId, "Отличная комната!", 2, 200);
}
Room.java
public class Room {
private Integer id;
private String roomInfo;
private Integer roomCapacity;
private double price;
public Room(Integer id, String roomInfo, Integer roomCapacity, double price) {
this.id = id;
this.roomInfo = roomInfo;
this.roomCapacity = roomCapacity;
this.price = price;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRoomInfo() {
return roomInfo;
}
public void setRoomInfo(String roomInfo) {
this.roomInfo = roomInfo;
}
public Integer getRoomCapacity() {
return roomCapacity;
}
public void setRoomCapacity(Integer roomCapacity) {
this.roomCapacity = roomCapacity;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
Обратите внимание, что в код класса Room не зря были включены геттеры и сеттеры. Их наличие обязательно для сериализации и десериализации!
Используем Postman для эмуляции клиента, сделаем GET-запрос и получим следующий результат
Как мы видим, поля объекта были сериализованы с помощью формата JSON. Теперь клиент, после получения этих данных, сможет с помощью процесса десериализации получить объект и удобно работать с ним.
Теперь рассмотрим обратную ситуацию. Клиент делает POST-запрос и передает в теле запроса данные о новом студенте.
На стороне сервера создаем класс Student с соответствующими полями.
Student.java
public class Student {
private String firstname;
private String lastname;
private String phone;
private int age;
public Student(String firstname, String lastname, String phone, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.phone = phone;
this.age = age;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"firstname='" + firstname + '\'' +
", lastname='" + lastname + '\'' +
", phone='" + phone + '\'' +
", age=" + age +
'}';
}
}
Создаем конечную точку для обработки запроса. Обратите внимание, что мы используем аннотацию @RequestBody.
@PostMapping("/student")
public void addStudent(@RequestBody Student student) {
System.out.println(student);
}
Конфигурация HTTP-response с помощью класса ResponseEntity
Далеко не всегда ответ сервера состоит в возврате какого-то значения или какого-то объекта. Очень часто необходимо вернуть ответ с определенным HTTP-кодом и сообщением об ошибке, указать определенный заголовок и так далее.
В этом случае необходимо использовать класс ResponseEntity. Класс ResponseEntity является оберткой для ответа и дополнительно для HTTP заголовков и кода статуса. Он является обобщенным, что позволяет использовать любой тип в качестве тела ответа.
@GetMapping("/student")
public ResponseEntity<?> getStudentById(@RequestParam(value = "id") int studentId) {
if (studentId < 1) {
return ResponseEntity.badRequest().body("Invalid id");
} else {
return ResponseEntity.ok(new Student("Ivan", "Ivanov", "223322", 20));
}
}
2. Паттерн MVC
Паттерн MVC можно описать следующим образом
Шаблон проектирования MVC предполагает разделение данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: Модель, Представление и Контроллер – таким образом, что модификация каждого компонента может осуществляться независимо.
В данном определении под компонентом следует понимать часть кода (как правило, это отдельный класс), каждая из которых играет одну из ролей Контроллера, Модели или Представления, где Модель служит для извлечения и манипуляций данными приложения, Представление отвечает за видимое пользователю отображение этих данных, а Контроллер управляет всем этим оркестром
Архитектурная концепция (архитектурный паттерн) MVC позволяет разделить программу на три отдельных компонента, которые могут быть реализованы следующим образом:
1. Пользователь взаимодействует с представлением. Представление – «окно», через которое пользователь воспринимает модель. Когда вы делаете что-то с представлением (скажем, щелкаете на кнопке воспроизведения), представление сообщает контроллеру, какая операция была выполнена. Контроллер должен обработать это действие.
2. Контроллер обращается к модели с запросами об изменении состояния. Контроллер получает действия пользователя и интерпретирует их. Если вы щелкаете на кнопке, контроллер должен разобраться, что это значит и какие операции с моделью должны быть выполнены при данном действии.
3. Контроллер также может обратиться к представлению с запросами об изменении. Когда контроллер получает действие от представления, в результате его обработки он может обратиться к представлению с запросом на изменение (скажем, заблокировать некоторые кнопки или команды меню).
4. Модель оповещает представление об изменении состояния. Когда в модели что-то изменяется (вследствие действий пользователя или других внутренних изменений – скажем, перехода к следующей песне в списке), модель оповещает представление об изменении состояния.
5. Представление запрашивает у модели информацию состояния. Представление получает отображаемую информацию состояния непосредственно от модели. Например, когда модель оповещает представление о начале воспроизведения новой песни, представление запрашивает название песни и отображает его. Представление также может запросить у модели информацию состояния в результате запроса на изменение состояния со стороны контроллера.
Модель (Model) – содержит бизнес-логику приложения и включает методы выборки, обработки и предоставления конкретных данных, что зачастую делает ее очень «толстой», что вполне нормально. Модель не должна напрямую взаимодействовать с пользователем. Модель:
предоставляет (представлению и контроллеру):
данные;
методы работы с данными:
запросы к базам данных;
валидация данных;
бизнес-логика (если модель «активна»).
нуждается в следующем:
в представлении (не может самостоятельно демонстрировать данных и результаты их обработки);
в контроллере (не имеет точек взаимодействия с пользователем).
может иметь множество различных представлений и контроллеров;
отвечает на запросы изменением состояния. При этом, в модель может быть встроено автоматическое оповещение «наблюдателей».
Представление (View) – используется для задания внешнего вида отображения данных, полученных из контроллера и модели. Представление не должно обращаться к базе данных, этим должны заниматься модели. Также, представление не должно работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер. Представление:
отвечает за получение необходимых данных от модели и отправку их пользователю;
не обрабатывает введенные данные пользователя;
может влиять на состояние модели через отправку ей сообщений (вызовы методов).
Контроллер (Controller) – связующее звено, соединяющее модели, представления и другие компоненты в рабочее приложение. Контроллер отвечает за обработку действий пользователя. В хорошо спроектированном MVC-приложении контроллеры обычно очень «тонкие» и содержат только несколько десятков строк кода. Логика контроллера довольно типична и большая ее часть выносится в базовые классы. Модели, наоборот, очень «толстые» и содержат большую часть кода, связанную с обработкой данных, так как структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения. Контроллер:
обеспечивает «связь» между пользователем и системой, контролирует и направляет:
данные от пользователя к системе;
реакцию системы – пользователю.
использует модель и представление для необходимого действия;
в случае «пассивной» модели – реализует бизнес-логику.
Рассмотрим пример реализации паттерна MVC на примере просто приложения. Приложение выводит на экран список студентов, добавляет нового студента и удаляет выделенного студента из списка.
Для начала, создадим новый JavaFX проект. Шаблон JavaFX проекта содержит три файла. Давайте определимся, к какой категории относится тот или иной файл.
Файл sample.fxml, очевидно, относится к представлению (View). Он задает внешний вид приложения.
Класс Main (файл Main.java) относится к контроллеру. Этот класс можно назвать контроллером приложения. Он содержит метод Application.launch(), который запускает JavaFX приложение, настраивается окно, создается объект сцены, устанавливается нужный fxml файл для генерации графа сцены для данного окна.
Класс Controller (файл Controller.java) очевидно является контроллером. Это контроллер окна, который содержит код для обработки событий, связанный с данным окном.
Создадим соответствующие пакеты и распределим файлы по своим пакетам. Не забудьте изменить путь к файлу fxml.
Теперь создадим класс Student, который будет содержать данные и логику для объекта студента. Очевидно, что этот класс относится к классу модели.
Спроектируем UI для нашего приложения. Для этого отредактируем файл sample.fxml. За вывод списка студентов будет отвечать элемент ListView. Кроме него предусмотрены две кнопки для добавления и удаления студента.
Добавим обработчики нажатий на кнопки в контроллер окна Controller.java.
Теперь вернемся к классам модели. Очевидно, что в данном приложении объекты класса Student должны быть организованы в некоторую коллекцию. Кроме того, мы должны предоставить метод добавления и удаления студентов из коллекции.
Так как «сборка» отдельных частей приложения осуществляется в контроллере – организаторе и посреднике между различными компонентами приложения, самым простым вариантом было бы просто создать пустую коллекцию студентов, в которую можно было бы напрямую добавлять или удалять студентов.
Так как мы оставляем возможности для будущего расширения программы, в которой могут появиться новые окна (например, отдельное окно для добавления студента), коллекция со студентами должна быть доступна во всем приложении.
Таким образом, первый кандидат на хранении коллекции со студентами – контроллер приложения, класс Main. Код для такого варианта будет примерно следующим:
Методы addStudent() и deleteStudent() будут вызываться из контроллера окна вместе с объектом для добавления или удаления.
Такой подход оправдан только, если мы точно знаем, что программа дальше не будет расширяться и наша модель будет, фактически, состоять из одного класса Student. Но мы хотим оставить задел на будущее, когда у нас может появиться класс студенческой группы, класс с оценками, класс учебной дисциплины, данные могут записываться и считываться из файлов, передаваться по сети и так далее.
В случае расширения программы в контроллер приложения будут добавляться все новые и новые ссылки на различные объекты и коллекции, а также появляться новые методы для управления этими объектами и коллекциями.
Поэтому, лучшим вариантом будет применение паттерна проектирования «Фасад», который позволит перенести весь будущий функционал в слой модели.
Принцип работы паттерна «Фасад» можно объяснить следующим образом: фасад – это объект некоторого класса, который предоставляет простой (но урезанный) интерфейс работы со сложной системой объектов. Таким образом, если наша модель будет усложняться, и будут добавляться новые классы, то будет усложняться внутреннее строение фасада, а внешне он будет предоставлять те же самые простые методы для работы со сложной моделью.
Создадим класс StudentsFacade. Класс будет содержать коллекцию со студентами, а также предоставлять публичные методы добавления и удаления студента.
Теперь создадим объект фасада в контроллере приложения и создадим методы, которые будут вызывать методы фасада.
Следующий шаг – необходимо написать обработчик кнопки «Добавить». При нажатии на кнопку необходимо создать объект класса Student и вызвать метод Main.addStudent(), которому необходимо передать созданный объект.
Так как обработчик нажатия на кнопку находится в контроллере окна, мы должны из контроллера окна (Controller.java) вызвать метод контроллера окна (Main.java). Чтобы это сделать, мы должны передать контроллеру окна ссылку на контроллер приложения.
Давайте разберемся, как это сделать. Для начала, откроем контроллер приложения и немного изменим загрузку fxml файла.
Если ранее мы использовали статический метод FXMLLoader.load(), то теперь мы создадим объект класса FXMLLoader, укажем в конструкторе ресурс fxml файла и вызовем метод load() у созданного объекта.
Эти манипуляции нужны нам для того, чтобы вызвать у объекта метод getController(), который вернет нам ссылку на созданный объект класса Controller. Таким образом, мы получим ссылку на объект контроллера окна.
Далее, в контроллере окна необходимо добавить поле типа Main и публичный метод, который будет записывать в поле переданную ссылку на объект контроллера приложения.
Таким образом, контроллер окна будет иметь ссылку на объект контроллера приложения. Теперь мы можем приступать к написанию кода для обработчика кнопки «Добавить». Для упрощения кода, будем генерировать значения полей для объекта студента.
Итак, на данный момент мы реализовали этапы 1 – 3 схемы работы паттерна MVC: пользователь взаимодействует с представлением (нажимает на кнопку «Добавить»), контроллер обращается к модели с запросами об изменении состояния. Но нам необходимо оповестить представление о том, что модель изменилась, чтобы представление обновилось (на экране появилась новая запись о студенте).
Для начала, реализуем взаимодействие модели и представления без использования встроенных средств JavaFX. Для этого нам необходимо разобраться с паттерном «Наблюдатель» (Observer).
Паттерн «Наблюдатель» (Observer)
Паттерн Observer определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически.
Паттерн определяет объект Subject, хранящий данные (модель) и объекты Observer, которые реализуют функциональность представлений. При создании наблюдатели Observer регистрируются у объекта Subject. Когда объект Subject изменяется, он извещает об этом всех зарегистрированных наблюдателей. После этого каждый обозреватель запрашивает у объекта Subject ту часть состояния, которая необходима для отображения данных.
Итак, реализуем этот паттерн в нашем исходном коде. В качестве Subject у нас выступает объект класса StudentsFacade, который хранит нужные данные. Укажем в исходном коде, что класс StudentsFacade наследуется от класса java.util.Observable. Класс Observable содержит готовый функционал для регистрации наблюдателей и для их оповещения об изменении модели. В классе StudentsFacade есть два метода, которые меняют состояние объекта – метод addStudent() добавляет новый объект студента в коллекцию, метод deleteStudent() извлекает из коллекции объект студента. Таким образом, при вызове этих методов, мы должны оповестить всех наблюдателей о том, что произошло изменение данных и оповестить их об этом и передать нужные данные для отображения.
Далее необходимо указать, что используемый для вывода студентов элемент ListView реализует интерфейс java.util.Observer, а также необходимо запрограммировать реакцию на извещение об изменении модели.
Когда в классе StudentsFacade будет выполняться метод addStudent() или deleteStudent() и выполнится команда notifyObservers(), у объекта класса ListViewObservers будет вызван метод update(), в котором будет передано два параметра – ссылка на объект, который вызвал метод и переданные данные, если они есть (иногда необходимо передать в Observer сам факт изменений, тогда вызывается метод notifyObservers() без параметров).
Мы проверяем – от кого произошло событие, и обновляем список с помощью пришедших данных (очищаем список от существующих элементов и добавляем новые).
Теперь необходимо заменить класс ListView на ListViewObserver. Регистрация Observer будет происходить в классе контроллера приложения
Нам осталось только реализовать функционал для кнопки «Удалить» и наше небольшое приложение теперь полностью готово.
Тестируем работу приложения.
Мы реализовали паттерн «Наблюдатель» вручную, но Java FX уже имеет готовый функционал для реализации этого паттерна. Для этого в JavaFX предусмотрен целый набор классов Observable*, которые предоставляют различные значения и коллекции уже со встроенной поддержкой паттерна Observer. Давайте реализуем 4-5 этап паттерна MVC с помощью встроенных классов Observable.
Для начала перейдем в класс StudentsFacade и изменим класс коллекции с List на ObservableList. Классу StudentsFacade теперь нет нужды наследоваться от класса java.util.Observable, так как этот функционал теперь реализует непосредственно коллекция со студентами. Кроме того, добавим геттер для коллекции (хотя это не очень согласуется с принципом инкапсуляции).
В классе Main заменим метод bindObserverToFacade() на setItemsForListView(). Обратите внимание, что элемент ListView содержит метод setItems(), который принимает на вход объект класса ObservableList. При вызове метода регистрация и обновление спискового элемента происходит без нашего участия – мы просто должны вызвать метод setItems() и передать ему ObservableList.
Заменим использование ListViewObserver на использование стандартного элемента ListView и изменим содержимое класса Controller.
Запустим приложение и убедимся, что его работа не изменилась, но мы избавились от необходимости создавать свой подкласс спискового элемента и вручную добавлять блок оповещения наблюдателей и вручную прописывать реакцию на оповещение об изменении данных.
4. Внедрение зависимостей (Dependency Injection)
Любое мало-мальски серьезное приложение состоит из нескольких классов, которые взаимодействуют друг с другом, чтобы реализовывать бизнес-логику. Обычно, каждый объект отвечает за получение ссылок на другие объекты, с которыми он взаимодействует (такие другие объекты называются зависимостями, dependencies). Такой подход может привести к созданию тесно связанного кода, который тяжело тестировать.
Рассмотрим небольшой участок кода, который состоит из класса User и класса Sender.
В результате мы получим тесно связанный код – класс User теперь напрямую зависит от класса Sender. Таким образом, если мы создадим класс EmailSender, который будет отсылать сообщения по электронной почте, то чтобы использовать объект класса EmailSender, нам придется изменять код класса User. К тому же, тестирование метода sendMessage() будет затруднительным.
Безусловно, мы не можем избежать связывания вообще, т.к. объектно-ориентированное программирование подразумевает взаимодействие множества объектов различных классов, программа из одного класса не имеет смысла. С другой стороны, нам необходимо избегать тесного связывания (tight coupling) классов, так как такой код тяжело повторно использовать, тестировать и тяжело понять, как это всё вместе работает.
В противовес тесному связыванию кода существует принцип слабо связного (loose coupling) кода. Слабая связность означает, что изменения, вносимые в один класс, повлекут за собой небольшие изменения в другие классы, что упростит тестирование, рефакторинг, повторное использование кода. Приложение с использованием принципа слабо связного кода легче модифицируется и поддерживается.
Инверсия контроля
Одним из приемов для написания слабо связного кода является принцип инверсии управления (Inversion of Control, IoC). Он заключается в том, что жизненным циклом (созданием, вызовом методов и уничтожением) ваших объектов управляете не вы сами, а некий сторонний код. Отсюда и термин «инверсия» – не я управляю кодом, а сторонний код управляет моими классами. Он решает, когда создавать объекты моих классов, когда вызывать их методы и когда уничтожать объекты.
На принципе инверсии управления базируется работа всех фреймворков.
Отличие библиотеки от фреймворка состоит в том, что библиотека – это по существу набор функций, организованных в классы, которые вы можете вызывать по мере надобности. Каждый вызов выполняет некоторую работу и возвращает управление обратно пользователю.
С другой стороны, фреймворк воплощает в себе некоторый абстрактный дизайн приложения со своим поведением. Для того, чтобы использовать его, вы должны добавить свой код в различные места фреймворка, либо через наследование, либо подключив свой собственный класс. Код фреймворка впоследствии будет вызывать ваш код.
Внедрение зависимости
Одной из реализаций принципа инверсии управления является внедрение зависимости (Dependency Injection, DI). Это принцип заключается в том, что зависимости класса не создаются или ищутся в самом классе, а внедряются (inject) извне некоторым другим внешним источником (например, каким-то другим объектом). В статье Мартина Фаулера «Inversion of Control Containers and the Dependency Injection pattern» этот объект называется «сборщиком» (an assembler), а сейчас его обычно называют контейнером (container) или IoC-контейнером (IoC-container).
В общем случае, IoC-контейнер – это некоторый программный код (фреймворк, отдельный класс), который осуществляет внедрение зависимостей в приложении и, насколько это возможно, упрощает данный процесс.
Как правило, внедрение зависимости осуществляется через:
конструктор класса (constructor injection);
поле класса (field injection);
входной аргумент метода (method injection), то есть через сеттер.
Внедрение через статические поля и методы не рекомендуется.
Фреймворк Spring, прежде чем стать многофункциональной платформой, изначально разрабатывался как IoC-контейнер для упрощения разработки JavaEE-приложений.
В приложениях на основе фреймворка Spring прикладные объекты располагаются внутри контейнера Spring. Как показано на рисунке, контейнер создает объекты, связывает их друг с другом, конфигурирует и управляет их полным жизненным циклом, от зарождения до самой их смерти (или от оператора new до вызова метода finalize()).
Классы, которыми управляет Spring-контейнер, называются бинами (bean) или компонентами. Контейнер создает, связывает между собой, а также уничтожает бины.
Фреймворк Spring имеет не один контейнер. В его состав входят несколько реализаций контейнера, которые подразделяются на два разных типа.
Фабрики компонентов (bean factories) (определяются интерфейсом org.springframework.beans.factory.BeanFactory) – самые простые из контейнеров, обеспечивающие базовую поддержку DI.
Контекст приложений (application contexts) (определяется интерфейсом org.springframework.context.ApplicationContext) основан на понятии фабрик компонентов и реализует прикладные службы фреймворка, такие как возможность приема текстовых сообщений из файлов свойств и возможность подписывать другие программные компоненты на события, возникающие в приложении.
С фреймворком Spring можно работать, используя и фабрики компонентов, и контексты приложений, но для большинства приложений фабрики компонентов часто оказываются слишком низкоуровневым инструментом. Поэтому контексты приложений выглядят более предпочтительно, чем фабрики компонентов.
В составе Spring имеется несколько разновидностей контекстов приложений. Три из них используются наиболее часто:
ClassPathXmlApplicationContext – загружает определение контекста из XML-файла, расположенного в библиотеке классов (classpath), и обрабатывает файлы с определениями контекстов как ресурсы;
FileSystemXmlApplicationContext – загружает определение контекста из XML-файла в файловой системе;
XmlWebApplicationContext – загружает определение контекста из XML-файла, содержащегося внутри веб-приложения.
Давайте перепишем наш код, чтобы подготовить его к использованию IoC-контейнера Spring. Руководствуясь принципом Dependency Inversion (не путать с Dependency Injection, это разные принципы), создадим интерфейс Sender, чтобы не привязываться к конкретной реализации отправщика сообщений.
Создадим класс TwitterSender, который реализует данный интерфейс.
Модифицируем класс User
Обратите внимание на разницу – мы теперь не сами создаем объект зависимости, а получаем его «извне» с помощью аргумента конструктора либо с помощью сеттера. Использование интерфейса позволяет легко использовать разные реализации отправщик сообщений. Еще одним бонусом является удобство проведения тестирования методов класса User, так как вместо настоящего отправщика сообщений мы можем подставить специальный мок-объект (mock object), который будет имитировать работу настоящего отправщика.
Внедрение зависимости (wiring)
Итак, у нас есть соответствующие классы, теперь необходимо связывать это все воедино с помощью IoC-контейнера. Каким образом передать объект TwitterSender объекту User?
Процесс создания связи между компонентами приложения обычно называют wiring (в русской версии книги Spring in Action этот термин переводят как связывание, не путайте с сильным и слабым связыванием, которое переводится как tight coupling и loose coupling).
Подключим библиотеки Spring, которые нужны для связывания компонентов. Если вы используете Maven в качестве сборщика, от откройте pom-файл и добавьте следующие зависимости (на момент проведения занятия актуальная версия библиотек была 5.0.7, в вашем случае актуальная версия может быть другой)
Важное отступление. Вы можете не добавлять библиотеку spring-core в pom-файл явно, код все равно будет работать. Это связано с тем, что spring-context не может работать без spring-core и Maven автоматически загрузит spring-core в любом случае, укажете ли вы ее в pom-файле или нет. В этом случае библиотека spring-core называется транзитивной зависимостью.
Транзитивная зависимость - это зависимость, которая требуется для работы вашей прямой зависимости.
Такой механизм позволяет избежать ручного добавления в pom-файл всего графа зависимостей - вы просто указываете прямые зависимости, а Maven сделает все остальное.
Итак, вернемся к связыванию компонентов.
Важный момент, который необходимо запомнить при работе с контейнером - любой контейнер необходимо сконфигурировать. То есть, на плечи разработчика ложится обязанность указать контейнеру, какие компоненты создать и как их связать вместе.
Spring предлагает три способа связывания компонентов:
явная конфигурация с помощью XML-файлов;
явная конфигурация с помощью классов Java;
неявное обнаружение бинов и автоматическое связывание.
В данном случае нет "самого лучшего" способа связывания, все три способа имеют право на жизнь. В данном занятии мы рассмотрим конфигурацию с помощью классов Java и автоматическое связывание.
Конфигурация с помощью классов Java
Для начала создадим класс, в котором будет осуществляться конфигурация. Создадим пакет config и класс AppConfig. Так как в Spring может использоваться несколько способов связывания компонентов, то желательно пометить класс аннотацией @Configuration - такая аннотация говорит контейнеру, что этот класс является классом конфигурации.
Конфигурация в классе осуществляется с помощью методов и аннотаций. Добавим в класс следующий метод
Пометив метод аннотацией @Bean, мы говорим что данный метод возвращает объект, который который должен быть зарегистрирован как бин в контексте приложения Spring (то есть, в нашем IoC-контейнере). Таким образом, мы фактически объявляем бин в нашем контейнере. Название бина будет совпадать с названием метода, в нашем случа бин будет называться twitterSender.
Теперь добавим еще один метод
Объявляем еще один бин User и в методе осуществляем связывание бинов. В нашем случае мы осуществляем связывание через конструктор (constructor injection).
Таким образом, мы объявили два бина - twitterSender и user, после чего связали их с помощью constructor injection.
Теперь модифицируем класс Main, создадим контейнер и попробуем использовать класс User.
Итак, сначала мы создали объект контейнера. В качестве реализации мы используем класc AnnotationConfigApplicationContext, который является реализацией интерфейса ApplicationContext, которая позволяет регистрировать аннотированные классы конфигурации. В нашем случае классом конфигурации является класс AppConfig, объявленный с помощью аннотации @Configuration. После того как вы зарегистрируете указанный класс, также регистрируются все типы bean-компонентов, возвращаемые с помощью методов, которые аннотируются с помощью @Bean.
После создания контейнера и загрузки конфигурации, используем класс User. Обратите внимание, что мы не сами создаем объект класса User и внедряем зависимости, а мы просто получаем объект из контейнера, с помощью метода getBean(). После того, как мы получили ссылку на объект, вызываем метод send() и получаем работающий класс User. Проверим работу приложения.
Таким образом мы реализовали связывание бинов с помощью контейнера Spring и конфигурации с помощью Java-классов. Теперь давайте рассмотрим автоматическое связывание.
Автоматическое связывание
Способ автоматического связывания является наиболее простым в использовании.
Автоматическое связывание в Spring реализуется с помощью двух механизмов:
сканирование компонентов (component scanning) – механизм, с помощью которого Spring обнаруживает и создает экземпляры компонентов;
автосвязывание (autowiring) – механизм, с помощью которого Spring автоматически «удовлетворяет» зависимости компонентов (to satisfy a dependency).
Совместная работа этих механизмов обеспечивает минимальное явное конфигурирование контейнера.
Перепишем наш код для использования автоматического связывания. Для того, чтобы механизм сканирования компонентов обнаружил наши классы-бины, необходимо пометить их с помощью аннотации @Component.
Тот участок кода, где контейнеру необходимо осуществить внедрение зависимости, аннотируется с помощью аннотации @Autowired. В рамках данного примера мы решили, что внедрение зависимости происходит в методе (method injection). Обратите внимание, что это не обязательно должен быть сеттер, хотя это крайне желательно
Когда мы осуществляли конфигурацию с помощью Java-класса, мы явно указывали классы компонентов и явно создавали объекты бинов.
Однако Spring способен автоматически отсканировать пакеты проекта, обнаружить бины и создать их экземпляры. Этот механизм называется сканирование компонентов (component scanning). По умолчанию, механизм сканирования компонентов отключен. Чтобы его включить, вернемся в конфигурационный класс AppConfig и укажем аннотацию @ComponentScan перед объявлением класса.
Прежде всего, удалим из класса AppConfig написанные ранее методы - они теперь не нужны.
Также обратите внимание, что в скобках я указал базовый пакет, где необходимо осуществить сканирование. Механизм сканирования компонентов будет искать компоненты в этом и в дочернем пакетах. Также вы можете указать параметр basePackages и перечислить пакеты для сканирования.
Запустим приложение и убедимся, что автоматическое связывание работает корректно.
Разрешение зависимости (Dependency Resolution)
Использование автоматического связывания (связывание компонентов реализуется с помощью механизмов Spring) может привести к ситуации, когда будет существовать несколько бинов, которые могут быть использованы для связывания.
Пока что у нас был только один класс, который реализовывал интерфейс Sender. А что, если их будет два? Создадим класс EmailSender
Запустим приложение
Сообщение при исключении четко описывает проблему: есть два бина, которые можно внедрить в класс User и Spring не знает, какой из них следует внедрить и закрывается с исключением.
Чтобы избавиться от данной проблемы, можно дать указания контейнеру, какой из компонентов следует выбрать в том или ином случае (ищите информацию по аннотации @Qualifier).
В нашем примере воспользуемся аннотацией @Conditional, чтобы решить проблему нескольких кандидатов на связыванеие.
Аннотация @Conditional перед объявлением класса бина означает, что бин будет доступен для регистрации в контейнере только, когда будет удовлетворено некоторое условие. В нашем случае, для каждого кандидата мы создадим отдельный класс - реализацию интерфейса Condition, в котором реализуем специальный метод. Если метод вернет true, значит условие выполнено и компонент можно зарегистрировать.
Прежде всего воспользуемся механизмом properties в Java. Создадим ресурс app.properties с содержимым
В классе Main создадим объект Properties и загрузим файл
Теперь у нас есть публичное статическое поле config, в котором хранятся свойства.
Создадим классы условий
В классах компонентов укажем аннотацию @Conditional и класс условия.
Теперь запустим приложение
Если мы изменим в app.properties значение с email на twitter и снова запустим приложение, то получим
Таким образом, проблема нескольких кандидатов решена.
7. Работа со Spring Security, часть 1
Spring Security – это фреймворк обеспечения безопасности, предоставляющий возможность декларативного управления безопасностью приложений на основе фреймворка Spring.
Создадим новый проект, который включает модуль Spring Security или добавим в существующий проект зависимость
При попытке перейти на любой URL-адрес проекта нас перенаправит на форму ввода логина и пароля
По умолчанию, логином является user, а пароль генерируется каждый раз при старте приложения.
Если вы ввели правильно логин и пароль, то сервер переадресует вас на указанный URL.
В файле application.properties вы можете указать желаемый логин и пароль для пользователя по умолчанию (в данной лекции будет использоваться конфигурация с помощью языка yaml).
Фреймворк Spring Security "из коробки" предоставляет вам возможность простой версии так называемой form-based аутентификации. Если быть точнее, то по умолчанию, Spring Security реализует следующее поведение:
добавляет обязательный процесс аутентификации для всех URL;
добавляет форму для входа;
обрабатывает ошибки формы ввода;
создает пользователя по умолчанию и генерирует пароль.
Основные понятия, связанные со Spring Security:
Authentication
Authorization
Principal - текущий залогиненный пользователь или текущий залогиненный аккаунт (если у одного физического лица или программы есть несколько аккаунтов, то тогда ему будет соответствовать несколько возможных principal`ов). Иногда, в общем случае, principal - это субъект, который принимает участие в осуществлении процедур безопасности. В качестве principal могут выступать люди, компьютеры, службы, процессы или их группа;
Granted Authority - ;
Role - .
Настройка процесса аутентификации в Spring
Для того, чтобы сконфигурировать процесс аутентификации, необходимо создать объект AuthenticationManager, в котором следует указать требуемые параметры аутентификации. Объект типа AuthenticationManager обычно настраивают с помощью builder`а, который имеет тип AuthenticationManagerBuilder.
Добавим класс SecurityConfig, который наследуется от класса WebSecurityConfigurerAdaper. Также укажем аннотации @Configuration (это означает, что данный класс является конфигурационным) и @EnableWebSecurity (это означает, что данный класс является содержит настройки для защиты веб-приложения).
Переопределим метод configure(), который принимает на вход объект типа AuthenticationManagerBuilder (обратите внимание, что нам нужна именно эта версия перегруженного метода).
Для начала укажем, что источник аутентификации это жестко прописанные пользователи (так называемая inMemoryAuthentication(). Далее указываем логин, пароль и роль пользователя.
Если необходимо указать несколько пользователей, после параметров первого пользователя вызываете метод and() после чего указываете параметры следующего пользователя.
Хеширование паролей
Хранить пароли без хеширования является грубейшим нарушением правил безопасности, поэтому нам необходимо добавить процесс хеширования пароля в нашу систему.
Не будем вдаваться в подробности различных алгоритмов хеширования пароля, просто скажем, что на даннай момент рекомендуемым является алгоритм Bcrypt. Для обеспечения хеширования, вы можете поступить несколькими способами.
Первый способ - создайте Bean, который будет возвращать объект Encoder`а и добавьте его как метод конфигурационного класса.
Далее найдите в интернете генератор хеша с помощью алгоритма Bcrypt, скопируйте хеш для вашего пароля в метод password.
Если не хотите использовать Bean для хеширования пароля, можете в начале хеша добавить обозначение, что это хеш для алгоритма bcrypt.
Настройка процесса авторизации
Добавим класс контроллера
Изменим SecurityConfig
Изменим formLogin() на httpBasic().
Создадим MyUserDetailsService
Создадим MyUserDetails
Добавим в pom.xml
Настроим подключение к БД
Добавим сущность User
Изменим MyUserDetailsService
Создадим UserRepository
Изменим MyUserDetails
8. Работа со Spring Security, часть 2
Добавляем в pom-файл поддержку jwt и jaxb
Создадим класс JwtUtils, добавим приватный ключ и методы для создания токена
Добавим метод для валидациия токена и сопутствующие ему методы
Создадим классы-обертки для входящего логина и пароля и для исходящего jwt
Создадим класс REST-контроллера, добавим метод для создания токена. Логика работы метода следующая:
Производится аутентификация по пришедшему логину и паролю;
Если аутентификация прошла успешно, то получаем UserDetails из БД по username;
Генерируем токен с помощью данных UserDetails;
Возвращаем токен как объект AuthResponse.
Настраиваем конфигурационный класс SecurityConfig. Добавляем bean для AuthenticationManager, а также доступ к URL. Для того, чтобы сервер не генерировал новую сессию, устанавливаем sessionCreationPolicy.
В качестве клиента используем Postman.
Для начала попробуем сделать запрос с некорректным логином и паролем
На стороне сервера было выброшено исключение
Теперь попробуем ввести логин и пароль существующего пользователя. В ответ мы получим jwt, который будем использовать для последующих запросов.
Аутентификация с помощью jwt
Добавим в класс-контроллер endpoint, для доступа к которому требуется аутентификация. Входной аргумент типа Principal хранит информацию об аутентифицированном пользователе. Получаем username пользователя и извлекаем из БД информацию о нем.
Редактируем конфигурационный класс SecurityConfig, добавляем требование аутентификации для endpoint /helloworld.
Для того чтобы провести аутентификацию с помощью jwt, создадим отдельный фильтр, который потом встроим в filter chain, которая используется в Spring Security.
Логика работы нашего фильтра следующая:
Считываем заголовок GET-запроса с ключом "Authorization";
Проверяем, есть ли в начале заголовка слово "Bearer ";
Извлекаем из jwt значение username;
Извлекаем из БД пользователя по полученному username и осуществляем валидацию токена;
Если jwt провел валидацию, то аутентифицируем пользователя;
Передаем управление дальше по цепочке фильтров.
Отредактируем конфигурационный класс SecurityConfig, добавим использование фильтра в цепочке фильтров (мы указываем, что наш фильтр должен быть встроен в цепочку ДО фильтра UsernamePasswordAuthenticationFilter, который является стандартным фильтром для аутентификации).
6. Фреймворк Spring MVC
1. Теоретические сведения
Spring MVC – веб-фреймворк, призванный упростить разработку веб-приложений. Опираясь на шаблон модель–представление–контроллер (Model-View-Controller, MVC), фреймворк Spring MVC помогает строить веб-приложения, столь же гибкие и слабо связанные, как сам фреймворк Spring.
Схема работы фреймворкаSpringMVC
Схема работы фреймворка выглядит следующим образом:
Краткое описание схемы работы Spring MVC звучит следующим образом:
вначале DispatcherServlet (диспетчер сервлетов) получает запрос, далее он смотрит свои настройки, чтобы понять какой контроллер использовать (на рисунке HandlerMapping);
после получения имени контроллера запрос передается на обработку в этот контроллер (на рисунке Controller). В контроллере происходит обработка запроса и обратно посылается ModelAndView (модель — сами данные; view (представление) — как эти данные отображать);
DispatcherServlet на основании полученного ModelAndView, должен определить, какое представление будет выводить данные. Для этого используется арбитр представлений (ViewResolver), который на основании полученного логического имени представления возвращает ссылку на файл View;
в представление передаются данные (Model) и обратно, если необходимо, посылается ответ от представления.
Давайте рассмотрим этот процесс более подробно:
Когда запрос покидает браузер, он несет в себе информацию о требовании пользователя. По крайней мере, запрос будет нести в себе запрошенный URL. Но он может также нести дополнительные данные, такие как информация из формы, заполненной пользователем;
Первой остановкой на пути запроса является DispatcherServlet. Как и большинство веб-фреймворков на языке Java, фреймворк Spring MVC пропускает все входящие запросы через единственный сервлет входного контроллера. Входной контроллер (frontcontroller) является типичным шаблоном проектирования веб-приложений, где единственный сервлет берет на себя ответственность за передачу всех запросов остальным компонентам приложения, выполняющим фактическую их обработку. В Spring MVC входным контроллером является DispatcherServlet;
Задача контроллера DispatcherServlet состоит в том, чтобы передать запрос контроллеру Spring MVC. Контроллер – это компонентSpring, обрабатывающий запрос. Но приложение может иметь несколько контроллеров, и входному контроллеру DispatcherServlet требуется помощь, чтобы определить, какому контроллеру передать запрос. Поэтому контроллер DispatcherServlet консультируется c одним или несколькими механизмами отображения (Handler Mapping) и выясняет, какой контроллер будет обрабатывать тот или иной запрос. При принятии решения механизм отображения в первую очередь руководствуется адресом URL в запросе;
Как только будет выбран соответствующий контроллер, DispatcherServlet отправляет запрос в путь к выбранному контроллеру. Достигнув контроллера, запрос отдаст часть своего груза (информацию, отправленную пользователем) и терпеливо будет ждать, пока контроллер обработает эту информацию. (На самом деле хорошо спроектированный контроллер сам почти не занимается обработкой информации, вместо этого он делегирует ответственность за обработку одному или нескольким служебным объектам);
В результате работы контроллера часто появляется некоторая информация, которая должна быть передана назад пользователю и отображена в браузере. Эта информация называется моделью (Model). Но отправки обратно необработанной информации недостаточно, перед отправкой ее следует представить в удобном для пользователя формате, обычно в HTML. Для этого информация должна быть передана в одно из представлений (View), которыми обычно являются JSP-страницы;
Последнее, что должен сделать контроллер, – упаковать вместе модель и имя представления для отображения результатов в браузере. Затем он отсылает запрос вместе с моделью и именем представления обратно входному контроллеру DispatcherServlet;
Чтобы контроллер не оказался тесно связанным с каким-либо конкретным представлением, имя представления, возвращаемое входному контроллеру DispatcherServlet, не определяет JSP-страницу непосредственно. Фактически оно даже не предполагает, что представление вообще является страницей JSP. Оно является лишь логическим именем представления, используемым затем для поиска фактического представления. Чтобы отобразить логическое имя представления в ссылку на конкретную реализацию, входной контроллер DispatcherServlet обратится к арбитру представлений (viewresolver);
Теперь, когда контроллер DispatcherServlet определил, какое представление будет отображать результаты, работа запроса подошла к концу. Его конечная остановка – реализация представления (возможно, страница JSP), куда он доставит модель данных. На этом работа запроса заканчивается. На основе модели данных представление создаст отображение страницы, которое будет отправлено обратно клиенту с другим (не таким трудолюбивым) курьером – объектом ответа.
Рассмотренную выше схему работы фреймворка можно также представить следующей диаграммой
2. Создание и настройка Spring Boot проекта
Создадим новый Spring Boot проект, выберем следующие модули
В новом проекте обратите внимание на структуру папок. В папке resources\templates будут содержаться html-файлы с использованием шаблонизатора Thymeleaf.
Создадим файл index.html. Обратите внимание, что в теге html необходимо указать пространство имен th для подключения тегов Thymeleaf.
В проекте Spring Boot MVC страница index будет автоматически передана при переходе на URL "/". Запустим проект и зайдем в браузер.
3. Создание контроллера, переход между страницами
Создадим две html-страницы для нашего проекта.
Создадим класс контроллера, который обрабатывает GET запросы с URL "/" и "/add_student".
В файле index.html добавим ссылку для кнопки "Добавить студента" . Для формирования ссылки используем тег th:href. Для указания пути относительно домена используем комбинацию @{~}.
Проверим работу приложения в браузере
Нажмем на кнопку "Добавить студента"
4. Получение данных формы
Для обработки формы необходимо выполнить следующую последовательность действий:
Создать объект, поля которого будут содержать данные формы. В нашем случае создадим класс Student;
Передать пустой объект Student при переходе на страницу формы;
В полях формы настроить соответствие между полями формы и полями объекта класса Student;
После отсылки формы заполненный объект Student передается в теле HTTP-запроса с методом POST, после чего заполненный объект можно обработать, сохранить в базе данных и так далее.
Создадим класс Student
Изменим метод контроллера, который обрабатывает URL "/add_student". Передадим пустой объект студента
Изменим файл add_student.html. В полях формы добавим привязку к полям объекта, в теге формы укажем название объекта (исходя из метода контроллера он должен называться "student", а также укажем URL для отправки данных формы).
Создадим метод контроллера, который принимает POST-запрос с URL "/add_student". Данный метод будет обрабатывать результат заполнения формы.
Изменим класс Student, превратив его в сущность
Создадим интерфейс репозитория
Добавим класс сервиса для работы с DAO
Изменим класс контроллера. После получения результатов заполнения формы, данные будут сохраняться в базе данных.
5. Вывод данных из БД
ля вывода данных на странице index, необходимо выполнить следующие действия:
обратиться к базе данных для получения списка студентов;
передать список студентов в View;
предусмотреть вывод полей каждого студента в нужных ячейках таблицы.
Модифицируем метод контроллера, который отвечает за обработку запроса "\"
Модифицируем страницу index.html. Добавим вывод полей каждого объекта типа Student в таблице
Запустим приложение и посмотрим на результат. Изначально таблица студентов пустая
Добавляем нового студента
После добавления студента, нас перенаправляют на страницу index
6. Валидация данных формы
Создадим проект с индексной страницей, на которой расположена форма добавления нового студента.
Изначально, форма не имеет средств валидации, то есть мы не можем отследить корректность заполнения формы.
Spring предоставляет несколько инструментов для реализации валидации формы, воспользуемся библиотекой Bean Validation API
Добавим библиотеку в список зависимостей в файле pom.xml
Будем использовать механизм встроенных ограничений. Алгоритм использования встроенных ограничений следующий - с помощью аннотаций необходимо указать над полем класса-сущности требуемые параметры валидации и другие параметры. В нашем случае, сущностью выступает класс Student. Добавим необходимые ограничения для полей сущности.
Как мы видим, все достаточно просто и наглядно.
Далее, нам необходимо модифицировать контроллеры и реализовать следующий функционал:
указать, что объект типа Student должен пройти валидацию;
получить результаты валидации объекта;
если объект не прошел валидацию - не добавлять объект в хранилище, выдать сообщение об ошибке в консоль.
Нам необходимо модифицировать метод контроллера, который обрабатывает данные формы. Указываем аннотацию @Valid, которая говорит о том, что полученный объект необходимо подвергнуть валидации. Далее указываем аргумент типа BindingResult, который хранит информацию о результате валидации. С помощью метода hasErrors() получаем результат валидации объекта.
При попытке отправить пустую форму, получаем сообщение в консоли
Последний шаг - необходимо предоставить пользователю информацию о том, что то или иное поле формы не прошло валидацию.
Самый простой способ проинформировать пользователь - показать сообщение об ошибке около поля, которое не прошло валидацию. Чтобы реализовать данный функционал, перейдем в шаблон index.html.
Рассмотрим поле "Фамилия". Сообщение об ошибке мы разместим снизу поля. Добавим соответствующий элемент <small> в HTML-макет.
Используем тег th:if. Если выражение внутри тега равно true, то элемент <small> будет показан на экране, если false - будет скрыт.
Выражение ${fields.hasErrors('lastName)} означает, есть ли ошибки валидации для поля lastName? Если ошибки есть - поле будет показано. Текст ошибки выводим с помощью атрибута th:errors.
Добавляем элементы для вывода ошибок для остальных полей формы. Проверяем результат
Ниже представлен листинг классов и файлов
7. Загрузка файлов на сервер
5. Абстрактные классы и интерфейсы. Механизм обратного вызова
Интерфейсы и абстрактные классы улучшают структуру кода и способствуют отделению интерфейса от реализации.
Абстрактный класс
Вернемся к примеру с классами фигур
Методы базового класса Shape всегда были "фиктивными". Попытка вызова метода из класса Shape привела бы к ошибке в программе. Такая логика написания кода связана с тем, что класс Shape нам нужен лишь для того, чтобы определить общий интерфейс всех классов, производных от него, а уже производные классы переопределяли эти методы и реализовывали их по-своему.
Если в программе определяется такой абстрактный базовый класс вроде Shape, создание объектов такого класса практически всегда бессмысленно. Абстрактный класс создается для работы с набором классов через общий интерфейс. А если Shape только выражает интерфейс, а создание объектов такого класса не имеет смысла, лучше всего запретить пользователю создавать такие объекты, так как попытка работать с этим классом приведет к ошибке в программе.
В языке Java для решения подобных задач применяются абстрактные методы. Абстрактный метод является незавершенным, он состоит только из объявления и не имеет тела. Приведем пример абстрактного метода:
Класс, содержащий один или более абстрактных метода, называется абстрактным классом. Такие классы также должны помечаться ключевым словом abstract (в противном случае, компилятор выдает сообщение об ошибке)
Компилятор запрещает создавать объекты абстрактного класса.
Если вы объявите не абстрактный класс, производный от абстрактного класса, то вы обязаны переопределить все абстрактные методы базового класса. Если это не будет сделано, то производный класс будет считаться абстрактным, и компилятор заставит пометить новый класс ключевым словом abstract.
Объявление класса как abstract не подразумевает, что все его методы должны быть абстрактными.
Класс можно обозначить как абстрактный даже когда в нем нет ни одного абстрактного метода. Это бывает полезно, когда необходимо просто запретить создание экземпляров этого класса.
Создавать абстрактные классы необходимо, так как они подчеркивают абстрактность класса, а также сообщают и пользователю класса и компилятору, как следует с ним обходиться. Кроме этого, абстрактные классы играют полезную роль при рефакторинге программ, потому что они позволяют легко перемещать общие методы вверх по иерархии классов.
Интерфейс
Ключевое слово interface становится следующим шагом на пути к абстракции. Ключевое слово interface используется для создания классов, вообще не имеющих реализации. Создатель интерфейса определяет имена методов, списки аргументов и типы возвращаемых значений, но не тела методов. Интерфейс описывает форму, но не реализацию.
Ключевое слово interface фактически означает "Именно так должны выглядеть все классы, которые реализуют данный интерфейс". Поэтому любой код, использующий конкретный интерфейс, знает только то, какие методы вызываются для этого интерфейса, но не более того. Интерфейс определяет своего рода "протокол взаимодействия" между классами.
Кроме этого, в отличие от абстрактного класса, интерфейс позволяет реализовать своего рода, множественное наследование.
Чтобы создать интерфейс, используйте ключевое слово interface вместо class. Как и в случае с классами, перед ключевым словом interface указывается модификатор доступа (public, protected и так далее). Интерфейс также может содержать поля, они автоматически являются статическими (static) константами (final).
Для создания класса, реализующего определенный интерфейс (или несколько интерфейсов), используется ключевое слово implements. Фактически это означает "интерфейс определяет форму, а здесь будет показано, как это работает".
В классе, который реализует интерфейс, реализуемые методы должны быть объявлены как public.
Неважно, приводите ли вы преобразование к "обычному" классу с именем Shape, к абстрактному классу Shape или к интерфейсу Shape - действие будет одинаковым.
Когда метод работает с классом вместо интерфейса, мы ограничены использованием базового класса и его подклассами. Это исключает возможность использовать метод для класса, который не входит в эту иерархию. Интерфейс, в значительной степени, ослабляет это ограничение. В результате код становится более универсальным.
Применение интерфейсных ссылок
В Java допускается объявлять переменные ссылочного интерфейсного типа, то есть переменные, хранящие ссылки на объекты классов, которые реализуют определенный интерфейс.
Такая переменная может ссылаться на любой объект класса, который реализует интерфейс. При вызове метода для объекта по интерфейсной ссылке выполняется вариант этого метода, реализованный в классе данного объекта. Этот механизм аналогичен тому, когда ссылочная переменная суперкласса ссылается на объект подкласса.
Реализация нескольких интерфейсов
Так как интерфейс по определению не имеет реализации, нет ничего, что могло бы помешать совмещению нескольких интерфейсов. При объявлении класса, который реализует несколько интерфейсов, имена интерфейсов перечисляются вслед за ключевым словом implements и разделяется запятыми.
Реализация механизма обратного вызова с помощью интерфейсов
Механизм обратного вызова (callback) является ключевым в программировании. При обратном вызове, программист задает действия, которые должны выполняться всякий раз, когда происходит некоторое событие. Например, можно задать действие, которое должно быть выполнено после нажатия на кнопку или при выборе определенного пункта меню.
Приведем небольшой пример. В стандартной библиотеке классов Java нам доступен класс javax.swing.Timer, который используется для отсчета интервалов времени.
Устанавливая таймер, мы задаем интервал времени и указываем, что должно произойти по его истечении. Как указать таймеру, что он должен делать по истечении времени?
Для этого в Java существует механизм обратного вызова. Он заключается в том, что программист должен передать объекту таймера объект некоторого типа. После этого таймер вызывает у объекта некоторый метод.
Разумеется, таймер должен знать, какой метод объекта он должен вызвать и этот метод в объекте гарантированно должен быть реализован. Для этого таймеру нужно указать объект класса, который реализует интерфейс ActionListener. Этот интерфейс входит в состав стандартной библиотеки и выглядит следующим образом
По истечении заданного интервала времени таймер вызывает метод actionPerformed() и передает ему объект класса Event (класс Event описывает событие в Java):
Как мы видим, конструктор класса Timer запрашивает задержку и объект, у которого будет вызван метод actionPerformed.
Создадим класс, который будет реализовывать интерфейс ActionListener
Как мы видим, данный класс ничего кроме реализации интерфейса не делает. То есть он нужен только для одной цели - он содержит метод, который будет вызван таймером после задержки. Такие классы и их объекты называют слушателями. Слушатель - это объект, который, как бы, "слушает" события, которые происходят с другим объектом. И когда это "слушаемое" событие происходит, вызывается указанный в интерфейсе метод этого объекта.
Создадим слушатель MessageDialogPoster и передадим этот объект таймеру
Запустим приложение и посмотрим на результат.
Обратите внимание, что метод actionPerformed принимает на вход объект класса ActionEvent. При вызове метода actionPerformed(), таймер передает в метод объект класса ActionEvent, который содержит различную информацию о событии. Таким образом, мы можем запрограммировать те или иные действия, в зависимости от параметров события.
Рассмотрим еще один пример, на этот раз будем использовать кнопку. Создадим объект окна, объект кнопки и добавим кнопку в окно.
По умолчанию, при нажатии на кнопку ничего не происходит. По аналогии с таймером, нам необходимо передать кнопке слушатель, который реализует определенный интерфейс. Кнопка гораздо более сложный объект, чем простой таймер, поэтому событий, которые привязаны к кнопке, может быть гораздо больше. Для каждого типа событий, кнопка принимает свой слушатель, который реализует свой определенный интерфейс.
С помощью такого многообразия интерфейсов, мы можем обработать самые разнообразные события, которые могут случиться с кнопкой.
В данном случае нас интересует метод addActionListener(), который принимает объект слушателя, который реализует интерфейс ActionListener
С помощью этого метода, мы передаем кнопке объект класса MouseClickHandler. Когда произойдет какое-то событие, кнопка возьмет этот переданный объект и вызовет метод actionPerformed() этого объекта.
Запустим приложение и нажмем кнопку.
Та
Заочники (осень 2022)
Материалы для освоения дисциплины
Для освоения дисциплины можно воспользоваться материалами для студентов очной формы (раздел "Осень 2020" на сайте). Также для скачивания доступен конспект лекций
Рекомендуемая литература:
Индивидуальная работа
Курсовая работа
Задание на курсовую работу выбирается студентом произвольно из представленных ниже вариантов задания.
Общие требования к приложениям:
приложения должны иметь консольный либо графический интерфейс;
для хранения данных можно использовать базы данных либо хранить данные локально в коллекциях.
Список тем
Приложение "Турфирма"
Требуется разработать программную систему, предназначенную для работников туристической фирмы. Такая система должна обеспечивать хранение сведений об имеющихся в продаже путевках и о клиентах фирмы. Сведения о путевке включают ее стоимость, время отправления (начало) и возвращения, экскурсии и прочие услуги, например, в стоимость путевки полностью или частично может входить питание. Путевка предполагает посещение одного населенного пункта. Туристическая фирма продает стандартные путевки. Каждый клиент покупает одну путевку.
Возможные сущности и их свойства:
В программе должен быть реализован следующий функционал:
добавление, редактирование, удаление клиента;
добавление, редактирование, удаление путевки;
создание, редактирование, удаление продажи.
Также программа должна выводить в консоль следующую информацию:
сколько продано путевок в определенный город (название города вводится пользователем);
сколько продано путевок за определенный месяц (месяц вводится пользователем);
общая стоимость проданных путевок за определенный месяц (месяц вводится пользователем).
Приложение "Сеть аптек"
Требуется разработать программную систему, предназначенную для директора сети аптек. Такая система должна обеспечивать хранение сведений об аптеке, об имеющихся в ней поставках товаров и поставщиках. Каждая аптека осуществляет поставку у различных поставщиков, предпочитая при этом закупать одни виды товара у одних поставщиков, а другие у других.
Поставки, имеющиеся в аптеке, характеризуются наименованием, ценой, сроком годности и количеством. Директор аптеки закупает недостающие товары партиями у поставщиков и списывает просроченные партии товаров.
Возможные сущности и их свойства:
В программе должен быть реализован следующий функционал:
Также программа должна выводить в консоль следующую информацию:
сколько выполнено поставок определенного товара за прошедший месяц (название товара вводится пользователем);
какие партии товара близки к списанию (до конца срока годности осталось меньше недели от текущей даты);
общая сумма поставок для выбранной аптеки от выбранного поставщика (аптека и поставщик вводится пользователем).
Приложение "Деканат"
Требуется разработать программную систему, предназначенную для работника деканата. Такая система должна обеспечивать хранение сведений о группах и студентах, а также о результатах текущей сессии.
Возможные сущности и их свойства:
В программе должен быть реализован следующий функционал:
добавление, редактирование, удаление студента;
добавление, редактирование, удаление группы;
создание, редактирование, удаление дисциплины;
выставление оценки по дисциплине конкретному студенту.
Также программа должна выводить в консоль следующую информацию:
средний балл в группе по определенному предмету (предмет вводится пользователем);
список студентов по всем группам, которые подлежат отчислению (не сдано более двух дисциплин);
по какой дисциплине больше всего неудовлетворительных оценок.
Приложение "СТО"
Требуется разработать программную систему, предназначенную для диспетчера СТО. Такая система должна обеспечивать хранение сведения о клиентах, о работниках и об автомобилях, которые они ремонтируют в текущий момент. Клиент - это человек, который хотя бы раз воспользовался услугами СТО. О клиенте должны храниться сведения с указанием автомобилей, которые он сдавал в ремонт.
Возможные сущности и их свойства:
В программе должен быть реализован следующий функционал:
добавление, редактирование, удаление клиента;
добавление, редактирование, удаление работы;
создание, редактирование, удаление работника;
создание, редактирование, удаление автомобиля.
Также программа должна выводить в консоль следующую информацию:
общая сумма выполненных работ по каждому работнику;
автомобили какой модели ремонтируются чаще всего (первые 5 моделей);
какой клиент заплатил больше всего (первые 5 клиентов).
Студент может предложить свою тему в качестве курсовой работы и обсудить ее с преподавателем.
К защите допускается курсовая работа с произвольной темой, которая была предварительно согласована с преподавателем.
5. Интеграция приложения с СУБД
В Spring имеется набор модулей для интеграции с различными технологиями хранения данных. Spring позволяет избавить разработчика от рутины при разработке программного кода, реализующего доступ к данным. Вместо возни с низкоуровневым доступом к данным можно положиться на Spring, который выполнит эту работу за вас, и сконцентрироваться на управлении данными в самом приложении.
Что такоеJDBC, драйвер,JPA,ORMи как это все между собой соотносится?
Как правило, каждая система управления базами данных (MySQL, PostgreSQL и так далее) имеет свой протокол взаимодействия с клиентами. Чтобы работать с базой данных, клиент должен соблюдать протокол взаимодействия с базой данных.
Чтобы программист не тратил время на самостоятельную реализацию протокола при разработке очередного приложения, разработчик сервера баз данных сам предоставляет всем желающим программный код, который общается с базой данных на понятном этой базе протоколе. Такой программный код и называется драйвером базы данных. Драйвер реализует протокол общения с БД и предоставляет API, которое позволяет нам общаться с базой данных, не вдаваясь в детали реализации протокола.
Как раз для этого разработчики Java предоставили стандарт JDBC (Java DataBase Connectivity) – специальное API, которое используется приложениями Java для взаимодействия с базой данных. Стандарт JDBC позволяет отправлять запросы к базе данных для выполнения операций выбора, вставки, обновления и удаления.
Если разработчики СУБД хотят, чтобы их база данных использовалась Java-разработчиками, они предоставляют JDBC-драйвер для их базы данных. Разработчики Java подключат драйвер и используют его для общения с той или иной базой данных. Если, в какой-то момент, разработчики захотят сменить СУБД, они просто меняют драйвер старой базы на драйвер новой. Благодаря стандарту JDBC, ничего менять в коде работы с базой данных не требуется.
Что такое и зачем нужна технологияORM?
При написании объектно-ориентированного кода, который взаимодействует с базой данных, у разработчика возникает несколько проблем:
данные в программе и в базе данных используют разные парадигмы (объектно-ориентированная и реляционная соответственно). Работу по преобразованию данных из одной парадигмы в другую ложатся на плечи программиста, что влечет за собой лишнюю работу и может приводить к ошибкам в процессе преобразования;
программисту желательно абстрагироваться от конкретной схемы хранения данных. То есть, программисту желательно работать не с реляционной базой данных, а просто с некоторым «хранилищем», а конкретная реализация этого «хранилища» может быстро и безболезненно меняться.
Для устранения этих проблем используется технология ORM(Object-Relational Mapping, «объектно-реляционное отображение») — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных».
Проще говоря, ORM– это прослойка, посредник между базой данных и объектным кодом. Используя ORM, программист не занимается формированием SQL-запросов и не думает в терминах «таблица», «записи» и «реляционные отношения», а просто работает с «хранилищем объектов» – он может туда записывать и получать объекты, не заботясь о подробностях их хранения.
В Java предусмотрен специальный стандарт JPA (Java Persistence API), который использует концепцию ORM. Существует несколько реализаций этого интерфейса, например, Hibernate, OpenJPA, EclipseLink и другие.
SpringDataJPA – обертка над JPA в Spring, которая предоставляет много полезных «фишек» разработчику. Она позволяет легче создавать Spring-управляемые приложения, которые используют новые способы доступа к данным, например нереляционные базы данных, map-reduce фреймворки, cloud сервисы, а так же уже хорошо улучшенную поддержку реляционных баз данных.
ТерминологияJPA
Основное понятие JPA – сущность (Entity). Сущность – это Java-класс, который представляет бизнес-логику приложения и определяет данные, которые будут храниться в базе данных и извлекаться из нее.
Как правило, класс сущности представляет таблицу в базе данных, поля или свойства класса представляют собой колонки в таблице, а объект сущности представляет собой одну запись в таблице.
Важным моментом при работе с JPA являются аннотации, коих здесь будет очень много. Разберемся с некоторыми из них:
@Entity – позволяет серверу узнать, что это не просто какой-то класс, а сущность;
@Id – помечает первичный ключ в таблице. Вопрос составных ключей в данном занятии не рассматривается;
@Table – позволяет настраивать отображение класса в таблицу. В данном случае, мы можем указать, какое имя будет иметь соответствующая таблица в базе данных;
@GeneratedValue – указывает, что данное поле является генерируемым значением. Очень часто этой аннотацией помечают первичные ключи, чтобы они генерировались автоматически при добавлении новых записей в таблицу;
@Column – позволяет настраивать отображение колонки в таблице. В данном случае, мы можем указать, какое имя будет иметь соответствующая колонка в таблицу.
Репозитории. Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.
В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.
Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.
Практическая часть
Установим СУБД Postgres и запустим pgAdmin 4.
Создадим пользователя ejournal_user, после чего создадим базу данных для нашего приложения.
Добавляем в pom.xml зависимости для работы с Spring Data JPA и JDBC драйвер для Postgres.
Далее необходимо настроить подключение к СУБД и нужной базе данных.
Для настройки приложения Spring воспользуемся языком YAML. Для этого удалим файл resources/application.properties и создадим вместо него файл application.yml.
Создадим класс сущности Student
Для уменьшения количества кода, мы будем использовать плагин Lombok.
Проект Lombok — это плагин компилятора, который добавляет в Java новые «ключевые слова» и превращает аннотации в Java-код, уменьшая усилия на разработку и обеспечивая некоторую дополнительную функциональность.
Lombok преобразует аннотации в исходном коде в Java-операторы до того, как компилятор их обработает: зависимость lombok отсутствует в рантайме, поэтому использование плагина не увеличит размер сборки.
При использовании Lombok наш исходный код не будет валидным кодом Java. Поэтому потребуется установить плагин для IDE, иначе среда разработки не поймёт, с чем имеет дело. Lombok поддерживает все основные Java IDE. Интеграция бесшовная. Все функции вроде «показать использования» и «перейти к реализации» продолжают работать как и раньше, перемещая вас к соответствующему полю/классу.
Далее подключим библиотеку в pom.xml.
Вернемся в класс Student, добавим аннотацию для геттеров, сеттеров, а также конструктор со всеми параметрами.
Программирование слоя сервисов
Service – это Java класс, который содержит в себе основную бизнес-логику. В основном сервис использует готовые DAO/Repositories или же другие сервисы, для того чтобы предоставить конечные данные для пользовательского интерфейса. Сервисы, как правило, вызываются контроллерами или другими сервисами.
Объект службы создается контейнером Spring, каждая служба является «одиночкой» (синглтоном), который создается в момент запуска приложения и уничтожается в момент закрытия приложения. Обратите внимание на аннотацию @Service. Этой аннотацией мы сообщаем контейнеру Spring, что это не просто класс, а класс сервиса.
Итак, мы создали службу, у которой есть два публичных метода. Первый метод добавляет нового студента, второй метод возвращает список всех студентов. В дальнейшем служба будет обращаться к объекту репозитория за данными, а пока что оставим код таким, какой он есть.
Вернемся к созданию веб-слоя. Создадим класс контроллера, создадим две конечные точки: для добавления студента и для получения списка всех студентов.
Обратите внимание, что мы не создаем объект службы, а получаем его «извне» с помощью аннотации @Autowired. Контейнер Spring «внедрит» ссылку на объект службы в поле service. Подробнее про внедрение зависимостей будет изложено в следующем занятии.
Работа с репозиторием
Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.
В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.
Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.
Теперь перейдем в класс службы и создадим ссылку на объект репозитория.
Обратите внимание, что мы не создавали класс, который реализует интерфейс StudentRepository, тогда откуда мы его получим объект интерфейсного типа? Дело в том, что Springсгенерирует класс за нас. Этот сгенерированный класс будет иметь набор стандартных операций для работы с сущностями. В нашем случае, это операция findAll(), которая возвращает все сущности в таблице student.
Запустим сервер и выполним два клиентских запроса - один на создание студента, второй - на получение списка всех студентов.
Добавляем нового студента
Теперь получим список всех студентов.
Реализация отношения "один-ко-многим"
Как мы знаем, важной составляющей реляционных баз данных является отношения между таблицами "один-к-одному", "один-ко-многим", "многие-ко-многим".
Реализуем отношение "один-ко-многим". Создадим сущность Group - студенческая группа. В студенческой группе может быть от 0 до N студентов.
Прежде всего перейдем в сущность Student. Добавим поле group, который будет ссылаться на студенческую группу, в которой будет состоять студент. Так как в группе может быть много студентов, указываем аннотацию @ManyToOne. Также указываем аннотацию @JoinColumn, которая указывает на имя колонки, которая будет содержать Foreign Key.
Технология ORM позволяет создавать двусторонние связи между таблицами. В этом случае, при выдаче JSON, может возникнуть бесконечный цикл. Чтобы его избежать, укажем аннотацию @JsonIgnore. В этом случае, колонка group будет проигнорирована в процессе сериализации\десериализации.
Далее создадим сущность Group.
Обратите внимание, что отношение один-ко-многим мы моделируем с помощью обычной коллекции. Указываем аннотацию @OneToMany, также в свойстве mappedBy указываем, какое поле "держит" отношение со стороны студента.
Далее модифицируем класс контроллера. Создадим конечные точки для добавления новой группы, а также для получения списка всех групп. Также модифицируем конечную точку для добавления студента, чтобы указать id группы, в которую необходимо добавить студента.
Теперь создадим репозиторий для сущности Group.
Далее модифицируем класс сервиса. Добавим методы для добавления новой группы, а также для получения списка всех групп. Также модифицируем метод добавления новой группы. Метод работает следующим образом: получаем объект группы по id, после чего добавляем ссылку на группу в поле group объекта Student.
Запустим приложение и проверим его работу. Сначала добавим группу, после чего получим список групп.
скачаем и установим приложение SceneBuilder по ссылке ;
вызовем статический метод FXMLLoader.load(), на вход которого передадим URL ресурса, в качестве которого выступает наш fxml-файл (что такое ресурс и как нам получить URL ресурса подробнее читайте здесь );
Более подробно про клиент-серверное взаимодействие читайте здесь -
Информация о сервере приложений - .
Отличия веб-сервера и сервера приложений - Подробнее про сервлеты и контейнеры сервлетов -
На данный момент актуальной версией технологии является версия 4.0, спецификацию технологии смотрите .
JDK - ;
приложение Postman - ;
среда разработки, которая поддерживает Spring (например, IntelliJ IDEA Ultimate Edition или дистрибутив Eclipse под названием Spring Tool Suite – ) либо любая другая IDE с поддержкой Java и Maven.
генерация готового проекта на сайте (проект Spring Initializr);
Запустим сервер, заходим на и видим строку с ответом.
application/x-www-form-urlencoded: значения кодируются в кортежах с ключом, разделенных символом '&', с '=' между ключом и значением. Не буквенно-цифровые символы - : это причина, по которой этот тип не подходит для использования с двоичными данными (вместо этого используйте multipart/form-data);
Использование формата JSON () является более предпочтительным. Для сериализации и десериализации в Spring по-умолчанию используется библиотека Jackson.
Подробную информацию по поводу ResponseEntity читайте здесь - .
Подробно читайте про инверсию контроля здесь или здесь .
Статью Мартина Фаулера (читать обязательно) читайте здесь или здесь (, ).
Неплохой материал по поводу конфигурации с помощью классов можно почитать и .
Информацию по поводу использования библиотеки можно найти (см. раздел 8 мануала)
public class Controller implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
}
@FXML
public void add(ActionEvent event) {
// Обработчик кнопки "Добавить"
}
@FXML
public void delete(ActionEvent event) {
// Обработчик кнопки "Удалить"
}
}
Main.java
public class Main extends Application {
private List<Student> list;
@Override
public void init() throws Exception {
list = new ArrayList<>();
}
public void addStudent(Student student) {
list.add(student);
}
public void deleteStudent(Student student) {
list.remove(student);
}
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
StudentFacade.java
public class StudentFacade {
private List<Student> studentList;
public StudentFacade() {
studentList = new ArrayList<>();
}
public void addStudent(Student student) {
studentList.add(student);
}
public void deleteStudent(Student student) {
studentList.remove(student);
}
}
Main.java
public class Main extends Application {
private StudentFacade facade;
@Override
public void init() throws Exception {
facade = new StudentFacade();
}
public void addStudent(Student student) {
facade.addStudent(student);
}
public void deleteStudent(Student student) {
facade.deleteStudent(student);
}
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
@FXML
public void add(ActionEvent event) {
// Создаем объект студента
Student student = new Student(
String.valueOf(new Random().nextInt(100)),
String.valueOf(new Random().nextInt(100)),
String.valueOf(new Random().nextInt(100))
);
// Вызываем метод добавления студента
main.addStudent(student);
}
StudentFacade.java
public class StudentFacade extends Observable {
private List<Student> studentList;
public StudentFacade() {
studentList = new ArrayList<>();
}
public void addStudent(Student student) {
studentList.add(student);
setChanged();
notifyObservers(studentList);
}
public void deleteStudent(Student student) {
studentList.remove(student);
setChanged();
notifyObservers(studentList);
}
}
public class ListViewObserver extends ListView<Student> implements Observer {
@Override
public void update(Observable o, Object arg) {
if (o instanceof StudentFacade) {
// В параметре arg передан список для отображения
List<Student> list = (List<Student>) arg;
getItems().clear();
getItems().addAll(list);
}
}
}
Main.java
public void bindObserverToFacade(Observer o) {
facade.addObserver(o);
}
Controller.java
public class Controller implements Initializable {
private Main main;
@FXML
private ListViewObserver list;
public void getMainController(Main main) {
this.main = main;
main.bindObserverToFacade(list);
}
...
}
public class User {
public void sendMessage(String message, String target) {
Sender sender = new Sender();
sender.send(message, target);
}
}
public class Sender {
public void send(String message, String target) {
System.out.println("Tweet: " + message + " to " + target);
}
}
public interface Sender {
void sendMessage(String message, String target);
}
public class TwitterSender implements Sender {
public void sendMessage(String message, String target) {
System.out.println("Tweet: " + message + " is sending to " + target);
}
}
public class User {
private Sender sender;
public User(Sender sender) {
this.sender = sender;
}
public void setSender(Sender sender) {
this.sender = sender;
}
public void send(String message, String target) throws NullPointerException {
if (sender != null) {
sender.sendMessage(message, target);
} else {
throw new NullPointerException("Sender object is null");
}
}
}
@Configuration
public class AppConfig {
@Bean
public TwitterSender twitterSender() {
return new TwitterSender();
}
}
@Configuration
public class AppConfig {
@Bean
public User user() {
return new User(twitterSender());
}
@Bean
public TwitterSender twitterSender() {
return new TwitterSender();
}
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(AppConfig.class);
User user = context.getBean(User.class);
user.send("Hello!", "Nick");
}
}
июл 01, 2018 2:37:09 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4534b60d: startup date [Sun Jul 01 14:37:09 EEST 2018]; root of context hierarchy
Tweet: Hello! is sending to Nick
@Component
public class TwitterSender implements Sender {...}
@Component
public class User {...}
@Component
public class User {
private Sender sender;
@Autowired
public void setSender(Sender sender) {
this.sender = sender;
}
}
@Configuration
@ComponentScan("app")
public class AppConfig {}
июл 01, 2018 4:13:21 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:13:21 EEST 2018]; root of context hierarchy
Tweet: Hello! is sending to Nick
@Component
public class EmailSender implements Sender {
public void sendMessage(String message, String target) {
System.out.println("Email: " + message + " to: " + target);
}
}
at app.Main.main(Main.java:28)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'app.model.Sender' available: expected single matching bean but found 2: emailSender,twitterSender
sender.type = email
public class Main {
public static final Properties config = new Properties();
static {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try (InputStream resourceStream = loader.getResourceAsStream("app.properties")) {
config.load(resourceStream);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class TwitterSenderCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return Main.config.getProperty("sender.type").matches("twitter");
}
}
public class EmailSenderCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return Main.config.getProperty("sender.type").matches("email");
}
}
@Component
@Conditional(value = TwitterSenderCondition.class)
public class TwitterSender implements Sender {...}
@Component
@Conditional(value = EmailSenderCondition.class)
public class EmailSender implements Sender {...}
июл 01, 2018 4:57:53 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:57:53 EEST 2018]; root of context hierarchy
Email: Hello! to: Nick
июл 01, 2018 4:58:56 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:58:56 EEST 2018]; root of context hierarchy
Tweet: Hello! is sending to Nick
public class Student {
private long id;
private String lastName;
private String firstName;
private String patronymic;
private String email;
private String phone;
private String address;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getPatronymic() {
return patronymic;
}
public void setPatronymic(String patronymic) {
this.patronymic = patronymic;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Student() {}
public Student(String lastName, String firstName, String patronymic, String email, String phone, String address) {
this.lastName = lastName;
this.firstName = firstName;
this.patronymic = patronymic;
this.email = email;
this.phone = phone;
this.address = address;
}
}
StudentController.java
@Controller
public class StudentController {
...
@GetMapping("/add_student")
public String addStudent(Model model) {
model.addAttribute("student", new Student());
return "add_student";
}
}
public class Student {
// Имя должно быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "First name should be from 2 to 50 characters")
private String firstName;
// Фамилия должна быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "Last name should be from 2 to 50 characters")
private String lastName;
// Возраст должен быть целым числом от 13 до 65
@Range(min = 13, max = 65, message = "Student age should be from 13 to 65 years")
private int age;
// Для валидации электронной почты используем регулярное выражение
@Pattern(regexp = "^[\\\\w!#$%&’*+/=?`{|}~^-]+(?:\\\\.[\\\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\\\.)+[a-zA-Z]{2,6}$",
message = "Invalid email format")
private String email;
...
}
@GetMapping("/")
public String addStudent(Model model) {
model.addAttribute("student", new Student());
return "index";
}
@PostMapping("/")
public String processAddStudentForm(@Valid Student student, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
System.out.println("Validation has been failed!");
return "index";
}
System.out.println(student);
list.add(student);
return "redirect:/";
}
public class Student {
// Имя должно быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "First name should be from 2 to 50 characters")
private String firstName;
// Фамилия должна быть длиной от 2 до 50 символов
@Size(min = 2, max= 50, message = "Last name should be from 2 to 50 characters")
private String lastName;
// Возраст должен быть целым числом от 13 до 65
@Range(min = 13, max = 65, message = "Student age should be from 13 to 65 years")
private int age;
// Для валидации электронной почты используем регулярное выражение
@Pattern(regexp = "^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$",
message = "Invalid email format")
private String email;
public Student(String firstName, String lastName, int age, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.email = email;
}
public Student() {
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Controller
public class StudentController {
private List<Student> list = new ArrayList<>();
@GetMapping("/")
public String addStudent(Model model) {
model.addAttribute("student", new Student());
return "index";
}
@PostMapping("/")
public String processAddStudentForm(@Valid Student student, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "index";
}
System.out.println(student);
list.add(student);
return "redirect:/";
}
}
@Entity
@Table(name = "students")
@Data
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private int age;
}
Service.java
@org.springframework.stereotype.Service
public class Service {
public void addStudent(Student student, int id) {
// Добавление нового студента
}
public List<Student> getAllStudents() {
// Получение списка студентов
}
}
Controller.java
@RestController
public class Controller {
@Autowired
private Service service;
@PostMapping("/student")
public void addStudent(@RequestBody Student student) {
service.addStudent(student);
}
@GetMapping("/student")
public List<Student> getAllStudents() {
return service.getAllStudents();
}
}
@Entity
@Table(name = "groups")
@Data
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
@OneToMany(mappedBy = "group")
private List<Student> studentList;
}
StudentRepository.java
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
Service.java
@org.springframework.stereotype.Service
public class Service {
@Autowired
private StudentRepository studentRepo;
public void addStudent(Student student) {
studentRepo.save(student);
}
public List<Student> getAllStudents() {
return studentRepo.findAll();
}
}
Student.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstname;
private String lastname;
private int age;
@ManyToOne (fetch = FetchType.LAZY,optional = false)
@JoinColumn(name = "group_id",nullable = false)
@JsonIgnore
private Group group;
}
Group.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "groups")
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(mappedBy = "group", cascade = CascadeType.ALL)
private List<Student> students;
public void addStudent(Student student) {
students.add(student);
}
}
Controller.java
@RestController
public class Controller {
@Autowired
private Service service;
@PostMapping("/student/{group_id}")
public void addStudent(@RequestBody Student student, @PathVariable(name = "group_id") int group_id) {
service.addStudent(student, group_id);
}
@GetMapping("/student")
public List<Student> getAllStudents() {
return service.getAllStudents();
}
@PostMapping("/group")
public void addGroup(@RequestBody Group group) {
service.addGroup(group);
}
@GetMapping("/group")
public List<Group> getAllGroups() {
return service.getAllGroups();
}
}
GroupRepository.java
public interface GroupRepository extends JpaRepository<Group, Integer> {}
Service.java
@org.springframework.stereotype.Service
public class Service {
@Autowired
private StudentRepository studentRepo;
@Autowired
private GroupRepository groupRepo;
public void addStudent(Student student, int id) {
Group g = groupRepo.getOne(id);
student.setGroup(g);
studentRepo.save(student);
}
public List<Student> getAllStudents() {
return studentRepo.findAll();
}
public void addGroup(Group group) {
groupRepo.saveAndFlush(group);
}
public List<Group> getAllGroups() {
return groupRepo.findAll();
}
}
2. Знакомство с языком Java
Основы синтаксиса Java
При разработке языка Java был взят за основу синтаксис языков С и C++, поэтому, многие аспекты синтаксиса языка покажутся вам знакомыми.
Комментарии
В Java, как и в C, существуют однострочные и блоковые комментарии. Однако, кроме этого, согласно конвенции Oracle, существуют другие виды комментариев: copyright-блок вверху, doc-комментарии, TODO-комментарии, комментарии после statement`ов, комментарии для комментирования кода. Ознакомьтесь с принятыми правилами использования комментариев, на защите лабораторных они будут требоваться в обязательном порядке, согласно принятым конвенциям.
package ai181.ivanov;
/**
* Это специальный комментарий для документирования (Doc comment).
* С помощью специальной утилиты (javadoc), такие комментарии можно превратить в HTML-странички,
* которые вместе создают документацию к вашей программе.
* Для удобства, здесь можно использовать специальные ссылки {@link Main} и <i>HTML-тэги</i>
*/
public class Main {
/**
* Этот метод является "точкой входа" приложения. В проекте может быть только один метод с такой сигнатурой
* @param args аргументы при запуске приложения
*/
public static void main(String[] args) {
// Это однострочный комментарий (single line comment)
/*
* Это комментарий в виде блока (block comment)
*/
// TODO: это специальный TODO-комментарий. Тут можно описать, что нужно доделать на каком-то участке кода
// FIXME: это тоже TODO-комментарий, обычно тут мелкие баги и что нужно исправить на данном участке кода
// TODO:2018-09-01:NickGodov: дописать вывод данных в файл
int my_variable = 5; /* Комментарии после statement должны быть выровнены слева */
int my_other_variable = 999; /* с помощью табуляции */
// Если нужно закомментировать код, то каждая строчка комментируется однострочным комментарием
// int my_old_variable = 100;
// int my_other_old_variable = 200;
// Перед комментарием принято оставлять пустую строку
int number = 924;
}
}
Переменные
Переменные чувствительны к регистру (var и Var – две разные переменные), могут быть бесконечной длины, состоять из букв юникода и цифр юникода, символов _ и $.
Первый символ переменной может быть буквой, символом _ или $ (использовать _ или $ первым символом КРАЙНЕ НЕ РЕКОМЕНДУЕТСЯ, они существуют для специальных ситуаций, которые нас сейчас не интересуют, так что считайте, что начинаться переменная может только с буквы юникода). Крайне не рекомендуется использовать буквы национальных алфавитов, кириллицу, транслит. Только латинские буквы, названия на английском. Также, названия переменных не должны совпадать со списком зарезервированных слов
abstract
continue
for
new
switch
assert
default
goto
package
synchronized
boolean
do
if
private
this
break
double
implements
protected
throw
byte
else
import
public
throws
case
enum
instanceof
return
transient
catch
extends
int
short
try
char
final
interface
static
void
class
finally
long
strictfp
volatile
const
float
native
super
while
Приведенный ниже блок кода даст вам общее представление о том, как надо называть переменные
int spd = 25; // ПЛОХО: Можно, но не рекомендуется, т.к. название не информативно
int carminspd = 25; // ПЛОХО: Не экономьте на названиях переменных!
int carMinSpeed = 25; // ХОРОШО: Название переменной говорит само за себя
int s = 0; // МОЖНО: однобуквенные допускаются, только если это какие-то короткоживущие бросовые переменные
int speed = 150; // ХОРОШО: Нормально, понятно, что переменная отвечает за скорость
int Speed = 150; // ПЛОХО: Крайне не рекомендуется, переменные не должны начинаться с капса
int SPEED = 150; // ПЛОХО: Крайне не рекомендуется, полностью капсом пишутся константы
const int MIN_SPEED = 25; // НЕЛЬЗЯ: в Java const не используется (хотя это зарезервированное слово)
final int MIN_SPEED = 25; // ХОРОШО: в Java для констант используется final
int моя_переменная = 356; // ПЛОХО: Только латиница
int DŽDžDzȯȺώϷЂ = 145; // ПЛОХО: Только латиница
int translit_epta = 29; // ПЛОХО: Транслит - это полный моветон, только английский!
int $myvar = 100; // ПЛОХО: Теоретически можно, но НЕ РЕКОМЕНДУЕТСЯ
int _myvar = 100; // ПЛОХО: Теоретически можно, но НЕ РЕКОМЕНДУЕТСЯ
int 2pac = 0; // НЕЛЬЗЯ: с цифры начинать нельзя
int %d = 5; // НЕЛЬЗЯ: с других знаков начинать нельзя
int 'f' = 5; // НЕЛЬЗЯ: с кавычек начинать нельзя
// Если название переменной состоит из двух слов
int max_speed = 150; // ПЛОХО: Использовать _ для отделения слов не в константах не рекомендуется
int MaxSpeed = 150; // ПЛОХО: Крайне не рекомендуется, переменные не должны начинаться с заглавной буквы
int maxSpeed = 150; // ХОРОШО: Вот так нормально, используется lowerCamelCase
final int MAX_SPEED = 150; // ХОРОШО: Константы пишутся капсом, каждое слово отделяется _
Типы данных
В языке Java существуют примитивные типы (аналогичны типам данных в C) и ссылочные (или объектные) типы данных. На данный момент нас интересуют только примитивные типы данных.
Java – строго типизированный язык программирования. Это значит, что переменная, перед использованием, должна быть объявлена и ей должен быть присвоен тип, который нельзя поменять. Также, при выполнении операций присваивания, компилятор проверяет соответствие типов (какого-то механизма автоматического приведения типов у Java нет).
Всего существуют восемь примитивных типов данных: int, long, short, byte, double, float, char, boolean. Их очень легко запомнить:
4 типа для целых чисел («короткое short», «среднее int», «длинное long» и байт);
2 типа для чисел с плавающей запятой (старая парочка double и float);
2 «специальных» типа – символ и булевый тип.
Type
Min
Max
RAM
Default
Объявления и литералы
byte
-128
127
8 bit
0
byte b = 100;
short
-32,768
32,767
16 bit
0
short b = 10000;
int
-2^31
-2^31-1
32 bit
0
int a = 15;
int aHex = 0xaa;
int aBin = 0b0001111;
(это же справедливо и для byte,short,long, если соблюдать диапазоны)
long
-2^63
-2^63-1
64 bit
0L
long number = 10000L;
double
4.9^-324
~1.8^308
32 bit
0.0d
double d = 6.6;
float
~1.4^-45
~3.4^38
64 bit
0.0f
float f = 5.5f;
char
0
65535
16 bit
'\u0000'
char c = ‘f’;
char c = 63;
char c = '\u2422';
boolean
false
true
1 bit
false
boolean b = true;
Операторы ветвления
Операторы ветвления в C и Java практически идентичны
int a = 5;
int b = 4;
int min;
// Так нужно оформлять обычный if
if (a >= b) {
min = b;
}
// Так нужно оформлять if-else
if (a >= b) {
min = b;
} else {
min = a;
}
// Так нужно оформлять if-else if-else
if (a > b) {
min = b;
} else if (a < b) {
min = a;
} else {
min = a;
}
// В Java используется тернарный оператор
min = (a >= b) ? b : a;
// Это равнозначно следующему выражению
if (a >=b) {
min = b;
} else {
min = a;
}
// Так оформляется switch
switch (a) {
case 1:
// что-то делаем
break;
case 2:
// делаем что-то другое
break;
default:
// это выполняется в том случае, если ни одно из кейсов не выполнился
break;
}
Циклы
Работа с циклами в Java мало чем отличается от языка C
int progression = 0;
// Так офориляется for
for (int i=0; i < 5; i++) {
progression +=i;
}
// ПЛОХО: так офорилять циклы не рекомендуется
for (int i=0; i < 5; i++) progression +=i;
// ПЛОХО: так офорилять циклы тоже не рекомендуется
for (int i=0; i < 5; i++)
progression +=i;
// Пустой for
for (int j=0; j < 10; j++);
// Так оформляется while
int iterator = 0;
while (iterator < 10) {
// делаем что-то в цикле
iterator++;
}
// Так оформляется do-while
int loops = 10;
do {
// что-то делаем
loops--;
} while (loops > 0);
// Также, в Java есть аналог foreach
int[] array = { 1, 2, 3, 4, 5 };
int sum = 0;
for(int i : array) {
sum += i;
}
// Этот же цикл можно представить обычным for`ом
for(int i = 0; i < 5; i++) {
sum += array[i];
}
Массивы
Работа с массивами в Java несколько отличается от работы с массивами в C, в основном, из-за механизма выделения памяти под массивы.
// Объявление массивов
/*
* ХОРОШО: согласно всем соглашениям по коду и различным рекомендациям, квадратные скобки
* ставят ПОСЛЕ ТИПА ДАННЫХ
*/
int[] goodArray;
/*
* ПЛОХО: компилятор не выдаст ошибку, но такой синтаксис делает код менее читабельным
*/
int badArray[];
/*
* НЕЛЬЗЯ: при объявлении массива нельзя указать его размерность.
* Java не выделит память, пока массив не будет инициализирован
*/
int[5] anotherBadArray;
// Объявления многомерных массивов
int [][] twoDimensionalArray;
int [][][] threeDimensionalArray;
// Инициализация массивов
goodArray = new int[10]; // Инициализируем массив с 10 элементами
goodArray[0] = 15; // Присваиваем значение первому элементу массива
goodArray[1] = 25; // Присваиваем значение второму элементу массива
twoDimensionalArray = new int [5][4]; // Двумерный массив 5х
twoDimensionalArray[0] = new int[4];
twoDimensionalArray[1] = new int[8]; // ПЛОХО: Компилятор проглотит, но по факту выделится место всего под 4 инта
twoDimensionalArray[0][0] = 1; // Присваиваем значение
twoDimensionalArray[1][5] = 5; // НЕЛЬЗЯ: Компилятор выдаст ошибку
System.out.print(twoDimensionalArray[1][6]); // НЕЛЬЗЯ: Компилятор выдаст ошибку
// Объявление с инициализацией
int[] quickArray = {1, 2, 3 ,4}; // Объявляем и сразу заполняем данные. Компилятор выделит место под 4 инта
quickArray[5] = 6; // НЕЛЬЗЯ: Компилятор выдаст ошибку, т.к. индекс выходит за пределы массива
int[][] quick2DArray = {
{1 ,2 ,3},
{1, 3, 4}
};
Методы
Так как Java является объектно-ориентированным языком, функции здесь называются методами (на данный момент мы будем считать, что методы и функции выполняют одну и ту же роль, но методы могут находиться только внутри классов).
/*
* Синтаксис функции:
* [1-модификаторы доступа] [2-тип возвращаемого значения] [3-имя]([4-аргументы]) [5-список исключений] {
* 6- тело функции
* }
*
* 1 - модификаторы доступа: на данный момент они нас не интересует, можно ничего не писать или писать private
* 2 - тип возвращаемого значения - тип данных либо void, если функция ничего не возвращает
* 3 - ограничения как и на имена переменных, но есть дополнительные правила наименования, о них ниже
* 4 - список аргументов через запятую. Например (int a, double b). Если нет аргументов - пустые скобки
* 5 - исключение пока не рассматриваем, если их нет, то просто ничего не пишут
* 6 - тело функции, в нем происходит выполнение функции. Если есть возвращаемый тип данных - должен быть return
*/
private int findMinimum(int a, int b) {
int min;
min = (a < b) ? a : b;
return min;
}
/*
* Название метода начинается с маленькой буквы, если несколько слов - используется lowerCamelCase.
* Первое слово должно быть глаголом (т.к. метод, как правило, "что-то делает"), остальные слова могут быть
* прилагательными, существительными и тд. Символ _ крайне желательно не использовать (кроме юнит-тестов)
*/
// ХОРОШО: с маленькой буквы, первое слово - глагол
private void drawCircle() {}
// ПЛОХО: Символы $ и _ не используем
private void $er () {}
// ПЛОХО: я сказал "не используем"!
private void draw_circle () {}
// ПЛОХО: первое слово с маленькой буквы
private void Draw() {}
// ПЛОХО: первое слово должно быть глаголом
private void circle() {}
Возвращаемся в Heroku, скачиваем и настраиваем Heroku CLI
Заходим в командую строку и логинимся на heroku с помощью команды heroku login
C:\Users\nickg>heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/browser/630e1a02-f90e-4217-8170-cd71d7fc700e
Logging in... done
Logged in as hodovychenko@opu.ua
Переходим в директорию проекта, после чего инициализируем git-репозиторий с помощью команды git init
C:\Users\nickg>cd d:\springdemo\ejo
d:\springdemo\ejo>git init
Initialized empty Git repository in d:/springdemo/ejo/.git/
Далее устанавливаем удаленный репозиторий
d:\springdemo\ejo>heroku git:remote -a opnu-ej
set git remote heroku to https://git.heroku.com/opnu-ej.git
Начинаем отслеживать файлы проекта с помощью команды git add ., после чего делаем коммит с помощью команды git commit -am "initial commit".
d:\springdemo\ejo>git add .
warning: LF will be replaced by CRLF in .gitignore.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in .mvn/wrapper/MavenWrapperDownloader.java.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in .mvn/wrapper/maven-wrapper.properties.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in mvnw.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in mvnw.cmd.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in pom.xml.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in src/main/java/com/example/ejo/EjoApplication.java.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in src/test/java/com/example/ejo/EjoApplicationTests.java.
The file will have its original line endings in your working directory
d:\springdemo\ejo>git commit -am "initial commit"
[master (root-commit) 162bc53] initial commit
16 files changed, 886 insertions(+)
create mode 100644 .gitignore
create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java
create mode 100644 .mvn/wrapper/maven-wrapper.jar
create mode 100644 .mvn/wrapper/maven-wrapper.properties
create mode 100644 mvnw
create mode 100644 mvnw.cmd
create mode 100644 pom.xml
create mode 100644 src/main/java/com/example/ejo/Controller.java
create mode 100644 src/main/java/com/example/ejo/EjoApplication.java
create mode 100644 src/main/java/com/example/ejo/Group.java
create mode 100644 src/main/java/com/example/ejo/GroupRepository.java
create mode 100644 src/main/java/com/example/ejo/Service.java
create mode 100644 src/main/java/com/example/ejo/Student.java
create mode 100644 src/main/java/com/example/ejo/StudentRepository.java
create mode 100644 src/main/resources/application.yml
create mode 100644 src/test/java/com/example/ejo/EjoApplicationTests.java
Теперь можно пушить проект на удаленный репозиторий с помощью команды git push heroku master.
d:\springdemo\ejo>git push heroku master
Enumerating objects: 32, done.
Counting objects: 100% (32/32), done.
Delta compression using up to 6 threads
Compressing objects: 100% (23/23), done.
Writing objects: 100% (32/32), 54.39 KiB | 9.06 MiB/s, done.
Total 32 (delta 1), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Java app detected
remote: -----> Installing JDK 1.8... done
remote: -----> Executing: ./mvnw -DskipTests clean dependency:list install
remote: [INFO] Scanning for projects...
...
remote: [INFO] Replacing main artifact with repackaged archive
remote: [INFO]
remote: [INFO] --- maven-install-plugin:2.5.2:install (default-install) @ ejo ---
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.pom
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.pom (4.0 kB at 450 kB/s)
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.jar
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar (38 kB at 1.1 MB/s)
remote: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.15/plexus-utils-3.0.15.jar
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.jar (155 kB at 2.0 MB/s)
remote: [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.15/plexus-utils-3.0.15.jar (239 kB at 3.0 MB/s)
remote: [INFO] Installing /tmp/build_bee447e74484e663663b1d166f8e9456/target/ejo-0.0.1-SNAPSHOT.jar to /app/tmp/cache/.m2/repository/com/example/ejo/0.0.1-SNAPSHOT/ejo-0.0.1-SNAPSHOT.jar
remote: [INFO] Installing /tmp/build_bee447e74484e663663b1d166f8e9456/pom.xml to /app/tmp/cache/.m2/repository/com/example/ejo/0.0.1-SNAPSHOT/ejo-0.0.1-SNAPSHOT.pom
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] BUILD SUCCESS
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] Total time: 20.474 s
remote: [INFO] Finished at: 2019-11-26T21:46:52Z
remote: [INFO] ------------------------------------------------------------------------
remote: -----> Discovering process types
remote: Procfile declares types -> (none)
remote: Default types for buildpack -> web
remote:
remote: -----> Compressing...
remote: Done: 84.1M
remote: -----> Launching...
remote: Released v5
remote: https://opnu-ej.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/opnu-ej.git
* [new branch] master -> master
d:\springdemo\ejo>
Теперь попробуем осуществить REST-запросы к серверу. Добавим группу, добавим студента, получим список групп.
7. Аргументы переменной длины. Принцип абстракции. Дополнительные принципы ООП.
Ссылка на конспект лекции
1. Обобщенные типы. Автоупаковка и автораспаковка.
Ссылка на конспект лекции
Дополнительные задания и литература
Литература
Видеолекции
Мэтт Вайсфельд - страницы 23 - 36.
Шилдт - страницы 37 - 55.
Необязательно! Шилдт - страницы - 58 - 125. (Глава 2 и 3). В 2 и 3 главе дано краткое описание примитивным типам данных, ветвлениям и циклам.
Гради Буч - страницы 33 - 73, 106 - 119.
Эккель, страницы - 40 - 46, 70 - 86.
Задание
1. CodingBat
Warmup 1, 2;
String 1, 2, 3;
Array 1, 2, 3;
Logic 1, 2.
1. Базовые сведения об ООП
В рамках данной темы поднимается вопрос сложность программного обеспечения как основного вызова при разработке ПО, понятию ОО программирования, приводится пример использования объектного подхода.
Базовые сведения об ООП
1.1 Сложность программного обеспечения
"Если бы строители строили здания так же, как программисты пишут
программы, первый залетевший дятел разрушил бы цивилизацию"
(с) Второй закон Вейнберга
Одним из основных вызовов при разработке программного обеспечения является сложность программ. Этот вызов иногда называют «кризисом программного обеспечения».
Не всё программное обеспечение является сложным, существует большое количество «простых» программ, которые разрабатываются и поддерживаются одним человеком. Такие программы, как правило, имеют очень ограниченный функционал и используются в течение короткого периода времени. С написанием таких программ не возникает много проблем, поэтому для их написания вы можете использовать практически любые языки программирования, технологии и методы разработки программных продуктов.
Наибольшие проблемы возникают при разработке корпоративного программного обеспечения (enterprise software). Такие системы используются довольно долго (годами и десятилетиями), и от их корректной работы зависят тысячи и даже миллионы людей. Это могут быть, например, системы управления воздушным транспортом, железнодорожными перевозками, банковские системы, системы коммунальных платежей, онлайн-игры, популярные веб-сайты и веб-службы и так далее.
Важнейшей особенностью корпоративной программы является ее высокая сложность. Одному программисту не под силу решить все проблемы, связанные с проектированием такой системы. Грубо говоря, сложность промышленных программ превышает интеллектуальные возможности отдельного человека.
Некоторые специалисты считают что популярные операционные системы являются самой сложной рукотворной системой, которую когда-либо создавало человечество.
Со времени возникновения области разработки программного обеспечения, человечество накопило достаточно знаний, чтобы проектировать даже самые сложные системы программного обеспечения, но мы до сих пор сталкиваемся с огромным количеством проблем. В чем же дело?
При анализе сложных систем, мы обнаруживаем много составных частей, которые взаимодействуют друг с другом разными довольно запутанными способами, причем части и способы их взаимодействия могут быть совершенно разными. При проектировании и организации сложных систем разработчику необходимо думать сразу о многом. Например, система управления воздушным транспортом должны одновременно контролировать состояние многих самолетов, учитывая, например, их местоположение, скорость и курс. К сожалению, один человек не может охватить все эти детали одновременно.
Таким образом, у нас возникает проблема сложности – программное обеспечение становится всё более сложным, а способности справиться с этой сложностью остаются ограниченными. Как же решить эту проблему?
1.2 Декомпозиция программных систем
Одним из способов справиться со сложностью программных систем, это декомпозиция. При проектировании сложного программного обеспечения, необходимо разделять его на всё меньшие и меньшие части, каждую из которых можно обрабатывать независимо друг от друга. Таким образом, вместо работы над всей программной системой сразу, мы будем работать с ее отдельными частями.
Одним из методов декомпозиции является алгоритмическая декомпозиция (вы сталкивались с таким видом декомпозиции на первом курсе, когда изучали дисциплину «Алгоритмизация и программирование»). Алгоритмическая декомпозиция выполняется методом «сверху вниз», где каждый модуль системы выполняет один из этапов общего процесса. На рисунке 1.1 приведена часть программы, которая обновляет содержимое основного файла.
Другим видом декомпозиции называется объектно-ориентированная декомпозиция, которая вам пока неизвестна.
При использовании этого вида декомпозиции, вместо разделения системы на этапы, например, «Прочитать отформатированное обновление» и «Добавить контрольную сумму», мы определяем такие объекты, как «Основной файл» и «Контрольная сумма», которые создаются при анализе предметной области. На рисунке 1.2 приведен пример объектно-ориентированной декомпозиции для той же части программы.
В случае объектно-ориентированной декомпозиции, мир представляет собой совокупность автономных агентов, которые взаимодействуют друг с другом и обеспечивают более сложное поведение системы. Действие «Прочитать отформатированное обновление» больше не является независимым алгоритмом, это действие представляет собой операцию, связанную с объектом «Файл обновлений». В результате выполнения этой операции возникает другой объект – «Обновление карты». Таким образом, каждый объект в такой схеме реализует свое собственное поведение, и каждый из них моделирует некоторый объект реального мира.
С такой точки зрения объект является материальной сущностью, обладающей определенным поведением. Получая сообщения, объекты выполняют определенные операции. Такая композиция основана на объектах, поэтому и называется объектно-ориентированной.
Так какой же метод декомпозиции следует использовать? Использовать оба метода одновременно нельзя – сначала следует произвести декомпозицию либо по алгоритмам, либо по объектам.
Многолетний опыт разработчиков программного обеспечения явно показывает, что объектно-ориентированная декомпозиция имеет много чрезвычайно важных преимуществ над алгоритмической. Декомпозицию следует начинать с объектов, поскольку она облегчает упорядочение сложных систем, таких как программное обеспечение, компьютеры, растения, галактики и крупные общественные институты.
уменьшается размер систем за счет повторного использования общих механизмов;
объектно-ориентированные системы являются более гибкими и легче эволюционируют со временем;
снижается риск, возникающий при создании сложной программной системы;
объектно-ориентированная декомпозиция позволяет лучше справиться со сложностью, характерной для систем программного обеспечения.
1.3 Объектная модель проектирования
Объектно-ориентированная технология основана на использовании так называемой объектной модели проектирования, или просто объектной модели. К основным принципам этой модели относятся абстракция, инкапсуляция, модульность, иерархия, контроль типов, параллелизм и персистентность.
Языки, которые реализуют объектную модель, называют объектными или объектно-ориентированными. Пример структуры программ, написанных на объектно-ориентированных языках программирования, представлен на рисунке 1.3.
Основным элементом в этих языках является модуль, который представляет собой логическую связанную совокупность объектов и классов (понятие класса будет рассмотрено ниже). Такая структура является графом, а не деревом, как в случае использования алгоритмических языков. Кроме того, в ОО языках исключены глобальные данные. Данные и операции объединяются таким образом, что основными логическими конструктивными элементами ОО систем теперь являются объекты и классы, а не алгоритмы.
Объектная модель допускает масштабирование. В крупных системах образуются целые кластеры, образующие слои. Пример структуры крупных систем приведен на рисунке 1.4.
1.4 Понятие объектно-ориентированного программирования
Объектно-ориентированное программирование – это методология программирования, основанная на представлении программы в виде совокупности взаимодействующих объектов, каждый из которых является экземпляром определенного класса, а классы являются членами определенной иерархии наследования.
Объект – структура, которая объединяет данные и методы, которые эти данные обрабатывают. Фактически, объект является основным строительным блоком объектно-ориентированных программ.
Класс – шаблон для объектов. Каждый объект является экземпляром (instance) какого-либо класса («безклассовых» объектов не существует). В рамках класса задается общий шаблон, структура, на основании которой создаются объекты. Данные, относящиеся к классу, называются полями класса, а программный код для их обработки называется методами класса. Поля и методы иногда называют общим термином – члены класса.
Разница между классом и объектом такая же, как между абстрактным понятием и реальным объектом.
Объект состоит из следующих частей:
имя объекта;
состояние (переменные состояния). Данные, содержащиеся в объекте, представляют его состояние. В терминологии ООП эти данные называются атрибутами. Например, атрибутами работника могут быть: имя, фамилия, пол, дата рождения, номер телефона. В разных объектах атрибуты имеют разное значение. Фактически, в объектах определяются конкретные значения тех переменных (полей класса), которые были заявлены при описании класса;
методы (операции) – применяются для выполнения операций с данными, а также для совершения других действий. Методы определяют, как объект взаимодействует с окружающим миром.
Объекты могут отправлять друг другу сообщения. Сообщение (message) - это практически то же самое, что и вызов функции в обычном программировании. В ООП обычно употребляется выражение "послать сообщение" какому-либо объекту. Понятие "сообщение" в ООП можно объяснить с точки зрения ООП: мы не можем напрямую изменить состояние объекта и должны как бы послать сообщение объекту, что мы хотим как-то изменить его состояние. Очень важно понять, что объект сам меняет свое состояние, а мы можем только попросить его об этом с помощью отсылки сообщения.
В объектно-ориентированной программе весь код должен находиться внутри классов!
В классе описываются, какого типа данные относятся к классу, а также то, какие методы применяются к этим данным. Затем, в программе на основе того или иного класса создается экземпляр класса (объект), в котором указываются конкретные значения полей и выполняются необходимые действия над ними.
Структуры в языке C как прототип класса
Давайте попробуем понять, что такое объект и класс с помощью языка программирования C.
Представим себе, что мы пишем программу для книжной лавки на языке C. В какой-то момент мы сталкиваемся с необходимостью хранить информацию о множестве книг: название книги, кто автор книги, год издания и стоимость книги. Как нам это запрограммировать?
Мы можем воспользоваться массивами и хранить данные о книгах в нескольких массивах.
int book_year[100];
int book_pages[100];
char book_author[100][50];
char book_title[100][100];
float book_price[100];
Теперь мы можем обратиться к i-му номеру каждого массива для получения информации об i-ой книге.
Какими недостатками обладает данный способ работы с данными? Такой подход может приводить к многочисленным ошибкам (например, ошибки при работе с индексами массивов), такие данные тяжело модифицировать (удаление книги приводит к необходимости смещать влево часть элементов массивов), такой код неудобно читать, поддерживать и модифицировать.
Но самое главное - мы мыслим в контексте структуры компьютера, а не решаемой задачи. Для нас книга - это некий единый объект, который имеет некоторые параметры (атрибуты): название, количество страниц и так далее. Мы же представляем атрибуты этого объекта в виде отдельных записей в разных массивах, потому что на языке C мы вынуждены мыслить в терминах имеющихся структур данных (массивов, очередей, деревьев), а не в терминах отдельных объектов и их взаимоотношений. Это затрудняет понимание решаемой задачи (управление книжной лавкой и продажей книг), увеличивает количество ошибок в программе и на некотором этапе мы вообще перестаем понимать, что происходит в программе.
Для хотя бы какого-то решения этой проблемы и облегчения труда программиста, на некотором этапе в язык C было введено понятие структуры (ключевое слово struct), которое вы должны были изучать в курсе "Алгоритмизация и программирование".
Структура в языке C - это тип данных, создаваемый программистом, предназначенный для объединения данных различных типов в единое целое. Таким образом, мы можем сгруппировать данные, которые относятся к одной книге, в одну структуру.
Сначала мы должны описать структуру (новый тип данных).
struct Book {
char author[50];
char title[100];
int year;
int pages;
float price;
};
Таким образом, некоторое понятие реального мира (то есть, книга вообще, как понятие) в программе описан структурой Book. Экземпляр этого понятия (какая-то конкретная книга) в программе будет представлен экземпляром структуры - переменной типа Book. Мы объявляем экземпляр структуры, после чего мы сможет заполнить поля структуры и работать с ней дальше в программе.
int main() {
// Объявляем экземпляр структуры
struct Book book1;
// Заполняем поля экземпляра
strcpy(book1.author,"Иванов А.А");
strcpy(book1.title,"Программирование на Java");
book1.pages = 255;
book1.price = 350.25;
book1.year = 2018;
// Теперь мы можем работать с созданным экземпляром структуры
printf("%s - %s, %d страниц", book1.author, book1.title, book1.pages);
return 0;
}
Использование структур решает проблему лишь частично. Мы сгруппировали данные, но функции для работы с нашей структурой находятся вне структуры.
Объект реального мира обладает не только атрибутами (автор книги, название книги, количество страниц и так далее), но и определенным поведением (книгу можно продать, купить, переместить на склад и так далее). При разработке сложных программных систем необходимо группировать не только данные, но и функции, которые работают с этими данными.
Пример использования объектного подхода
Рассмотрим простой пример объектно-ориентированной программы. Представим, что мы программируем графический редактор, который может рисовать различные фигуры. Подумав над задачей, мы приходим к выводу, что в нашей программе должны быть объекты, которые представляют различные фигуры. Итак приступим.
Сначала мы должны создать классы и описать внутри этих классов данные, которые относятся к фигурам и поведение фигур в виде набора методов (функций).
Опишем класс для фигуры «Треугольник». Какие характеристики могут быть у класса «Треугольник»? От чего зависит набор характеристик? В принципе, любой объект является бесконечно сложным и для его описания понадобится бесконечно большое количество характеристик. Но для нашего простого графического редактора важными будут следующие параметры: координаты трех точек и цвет фигуры.
Здесь вы должны понять очень важную вещь: набор данных в классе зависит от той программы, которую мы собираемся написать. Например, если бы мы писали более мощный графический редактор, в списке полей класса мы добавили бы отдельно цвет заливки и цвет линий фигуры, сам цвет мог быть не только сплошным, но и в виде какого-то узора и так далее.
Для указания класса в Java используется ключевое слово class, после чего идет название класса, далее мы ставим фигурные скобки и всё, что будет написано внутри фигурных скобок (переменные и функции) будет относиться к этому классу.
class Triangle {
}
Какие переменные и типы данных будут моделировать координаты точек и цвет? Для моделирования цвета воспользуемся обычным целым числом (очень часто цвета моделируются обычным целочисленным значением).
class Triangle {
// Цвет треугольника
int color;
}
Теперь попробуем подумать, а как нам смоделировать координаты трех точек? Если бы мы программировали на языке C, мы бы написали что-то вроде
class Triangle {
// Цвет треугольника
int color;
// Координаты точек треугольника
int x1,y1,x2,y2,x3,y3;
}
Но давайте подумаем - координаты каждой точки логически связаны между собой и когда мы будем, например, перемещать треугольник или будем менять размер треугольника, будут одновременно меняться две переменные, которые моделируют одну точку. С точки зрения объектной модели, лучшим вариантом будет предусмотреть отдельную сущность, отдельный класс, который моделирует точку на двумерной плоскости. Таким образом, мы сгруппируем данные и код, который будет эти данные изменять (например, менять значение X и Y).
class Point2D {
int x;
int y;
}
Теперь вернемся к классу Triangle и укажем, что в качестве координат у нас будут выступать три объекта класса Point2D.
class Triangle {
// Цвет треугольника
int color;
// Координаты трех точек
Point2D point1;
Point2D point2;
Point2D point3;
}
Теперь давайте определимся с методами классов – то есть, к тому поведению, которое будут осуществлять объекты этих классов.
Какое поведение может быть у объекта класса «точка»? Совершенно точно это будет метод «изменить координаты». То есть, наша точка как отдельный объект может вести себя следующим образом – менять свои координаты. Давайте запрограммируем этот метод.
class Point2D {
int x;
int y;
void changeCoordinates(int new_x, int new_y) {
x = new_x;
y = new_y;
}
}
Теперь давайте подумаем, какое поведение будет у треугольника? Что с ним можно сделать? Ну, например, можно его перекрасить, можно его передвинуть в другое место на плоскости, можно его нарисовать на каком-то полотне и так далее. Давайте опишем некоторые из этих методов.
class Triangle {
int color; // Цвет треугольника
// Координаты трех точек
Point2D point1;
Point2D point2;
Point2D point3;
// Меняем цвет фигуры
void changeColor(int new_color) {
color = new_color;
}
// Меняем расположение фигуры
// Для наглядности будем передавать входные параметры
// в виде 6 целых чисел
void move(int x1, int y1, int x2, int y2, int x3, int y3) {
point1.changeCoordinates(x1, y1);
point2.changeCoordinates(x2, y2);
point3.changeCoordinates(x3, y3);
}
}
Итак, мы описали треугольник, но мы определили только понятие «треугольник», у нас нет их физически, код класса - это просто описание. Если вы создали java-проект, у вас, по умолчанию, будет присутствовать класс Mainи функция main(). Пока что мы не будем объяснять, зачем нужен класс Main, а обратим внимание на метод main(). Этот метод является «точкой входа» в программу, с вызова этого метода начинается работа программы. Когда мы дойдем и выполним последнюю инструкцию внутри метода main(), приложение завершится.
Создадим внутри метода main() несколько объектов класса Triangle. Создание объектов мы будем рассматривать подробно в следующих лекциях, на данный момент мы просто скажем, что для создания объекта используется ключевое словоnew. После создания объекта, мы можем обращаться к объекту и вызывать методы у объектов (как мы вызывали функции в C).
public class Main {
public static void main(String[] args) {
// Создали один объект треугольника
Triangle triangle1 = new Triangle();
// Создали еще один объект треугольника
Triangle triangle2 = new Triangle();
// Для первого треугольника поменяли цвет
triangle1.changeColor(10000);
// Поменяли цвет для второго треугольника
triangle2.changeColor(20000);
// Создали объект первой точки треугольника
// и назначили точке какие-то координаты
triangle1.point1 = new Point2D();
triangle1.point1.changeCoordinates(140,180);
// Сделаем это же для первой точки второго треугольника
triangle2.point1 = new Point2D();
triangle2.point1.changeCoordinates(50,80);
}
}
Важно! Вы должны понять, что у разных объектов значения переменных будут разные!
То есть, где-то в оперативной памяти у нас создано два разных объекта. Внутри первого есть объект класса Point2D, внутри этого объекта есть две переменные типа int, у которых будут значения 140 и 180. Внутри же второго треугольника будет свой объект Point2D, внутри которого будут совершенно другие две переменные типа int, у которых будут значения 50 и 80. Это же будет касаться переменных color в двух разных объектах (рис.1.5).
Таким образом, когда мы описываем классы, мы заявляем, что объекты этих классов будут содержать определенные поля (набор переменных) и будут иметь набор методов (функций), которые можно вызвать у объекта. Каждый объект содержит свои переменные, но методы у них общие.
5. Абстрактные классы и интерфейсы. Механизм обратного вызова.
Ссылка на конспект лекции
3. Паттерн Итератор. Компараторы. Потоки в Java (Streams API)
2. Коллекции объектов.
Ссылка на конспект лекции
1. Исключения в Java. Обработка исключений.
Перечисления
Механизм перечислений был добавлен в JDK 5 версии, он позволяет удобно хранить и работать с так называемыми "категориальными данными".
Категориальные данные - это данные с ограниченным числом уникальных значений или категорий. Приведем примеры категориальных данных:
месяцы года (12 значений: январь, февраль и так далее);
пол (мужской, женский, андроген, бесполый и так далее);
вероисповедание (православие, католицизм, ислам и так далее);
день недели (понедельник, вторник, среда и так далее).
Рассмотрим пример. Для информационной
системы «Электронный институт» необходимо создать класс сотрудника кафедры «DepartmentMember», в котором, среди прочих атрибутов, есть атрибут «должность» (position). С целью упрощения, предположим, что сотрудники кафедры могут занимать одну из следующих должностей:
«Инженер» (Engineer);
«Ассистент» (Assistant);
«Старший преподаватель» (Lecturer);
«Доцент» (Senior Lecturer);
«Профессор» (Professor).
Каким образом закодировать данное значение? Самый первый и очевидный вариант - просто использовать тип данных String.
Данное решение имеет несколько очень серьезных недостатков. Вы никак не можете обеспечить корректность указания должностей и контролировать создание нового объекта класса.
public static void main(String[] args) {
DepartmentMember member1 = new DepartmentMember(
"Иван",
"Иванов",
"доцент"
);
DepartmentMember member2 = new DepartmentMember(
"Иван",
"Иванов",
"старший преподаватель"
);
}
Должность старшего преподавателя, например, может быть записана как "ст. преп.", "ст. преподаватель", "ст. пр." и так далее, не говоря про возможные орфографические ошибки. С точки зрения формальной логики, это все разные категории должностей и вы не сможете группировать или фильтровать сотрудников кафедры по полю "должность".
Другой вариант - это создать несколько статических целочисленных констант, которые можно использовать вместо строк.
class DepartmentMember {
public static final int ENGINEER = 0;
public static final int ASSISTANT = 1;
public static final int LECTURER = 2;
public static final int SENIOR_LECTURER = 3;
public static final int PROFESSOR = 4;
private String firstName;
private String lastName;
private int position;
public DepartmentMember
(String firstName, String lastName, int position) {
this.firstName = firstName;
this.lastName = lastName;
this.position = position;
}
}
Теперь мы сможем использовать именованные константы вместо строго фиксированных строк.
public static void main(String[] args) {
DepartmentMember member1 = new DepartmentMember(
"Иван",
"Иванов",
DepartmentMember.ENGINEER
);
DepartmentMember member2 = new DepartmentMember(
"Петр",
"Петров",
DepartmentMember.PROFESSOR
);
}
Использование именованных констант позволяет отчасти избавиться от недостатков предыдущего подхода, но и этот вариант не является приемлемым. Ничто не мешает пользователю класса при создании объекта указать любое целое числа вместо именованной константы, осуществлять проверку ввода – достаточно трудоемкая задача, если в классе предусмотрено более одного категориального значения будет тяжело определять какие конкретно именованные константы необходимо использовать. К тому же, при использовании конструктора или сеттера поля «должность», нет никакого указания на необходимость использования именованных констант, программист сам должен догадаться о наличии нужных констант или читать сопроводительную документацию.
class DepartmentMember {
public static final int ENGINEER = 0;
public static final int ASSISTANT = 1;
public static final int LECTURER = 2;
public static final int SENIOR_LECTURER = 3;
public static final int PROFESSOR = 4;
public static final int FULL_TIME = 100;
public static final int PART_TIME = 101;
private String firstName;
private String lastName;
private int position;
private int contractType;
public DepartmentMember
(String firstName, String lastName, int position, int contractType) {
this.firstName = firstName;
this.lastName = lastName;
this.position = position;
this.contractType = contractType;
}
}
Подход с использованием именованных констант напоминает улучшенную версию использования механизма специальных кодов и его не следует использовать при разработке коммерческих программ.
Корректным вариантом решения данной проблемы является использование специального механизма перечислений (enumeration). По сути, перечисление – это тип, ссылочная переменная которого может принимать одно из нескольких заранее определенных значений. Реализуем класс «DepartmentMember» с помощью механизма перечислений:
class DepartmentMember {
private String firstName;
private String lastName;
private Position position;
public DepartmentMember
(String firstName, String lastName, Position position) {
this.firstName = firstName;
this.lastName = lastName;
this.position = position;
}
public enum Position {
Engineer, Assistant, Lecturer, SeniorLecturer, Professor;
}
}
public static void main(String[] args) {
DepartmentMember member1 = new DepartmentMember(
"Иван",
"Иванов",
DepartmentMember.Position.Assistant
);
DepartmentMember member2 = new DepartmentMember(
"Петр",
"Петров",
DepartmentMember.Position.Professor
);
}
Преимущества такого подхода очевидны: поле position может принять только один из заранее определенных значений, которые указаны при создании перечислений. При вызове конструктора или сеттера поля position, пользователю класса сразу будет понятно, что необходимо передать в качестве аргумента метода.
Обратите внимание на синтаксис перечисления. Перечисление является классом, но очень своеобразным. Вместо ключевого слова class используется ключевое слово enum (от слова enumeration – перечисление). Вместо полей и методов у перечисления идет просто набор констант через запятую.
Как уже было сказано, перечисление – это класс, а это значит, что он может иметь методы, и мы можем инкапсулировать перечисление и операции работы с ним в одной оболочке.
В данном примере, экземпляр перечисления хранит строку с цветом.
2. Интеграция приложения с СУБД
В Spring имеется набор модулей для интеграции с различными технологиями хранения данных. Spring позволяет избавить разработчика от рутины при разработке программного кода, реализующего доступ к данным. Вместо возни с низкоуровневым доступом к данным можно положиться на Spring, который выполнит эту работу за вас, и сконцентрироваться на управлении данными в самом приложении.
Что такоеJDBC, драйвер,JPA,ORMи как это все между собой соотносится?
Как правило, каждая система управления базами данных (MySQL, PostgreSQL и так далее) имеет свой протокол взаимодействия с клиентами. Чтобы работать с базой данных, клиент должен соблюдать протокол взаимодействия с базой данных.
Чтобы программист не тратил время на самостоятельную реализацию протокола при разработке очередного приложения, разработчик сервера баз данных сам предоставляет всем желающим программный код, который общается с базой данных на понятном этой базе протоколе. Такой программный код и называется драйвером базы данных. Драйвер реализует протокол общения с БД и предоставляет API, которое позволяет нам общаться с базой данных, не вдаваясь в детали реализации протокола.
Как раз для этого разработчики Java предоставили стандарт JDBC (Java DataBase Connectivity) – специальное API, которое используется приложениями Java для взаимодействия с базой данных. Стандарт JDBC позволяет отправлять запросы к базе данных для выполнения операций выбора, вставки, обновления и удаления.
Если разработчики СУБД хотят, чтобы их база данных использовалась Java-разработчиками, они предоставляют JDBC-драйвер для их базы данных. Разработчики Java подключат драйвер и используют его для общения с той или иной базой данных. Если, в какой-то момент, разработчики захотят сменить СУБД, они просто меняют драйвер старой базы на драйвер новой. Благодаря стандарту JDBC, ничего менять в коде работы с базой данных не требуется.
Что такое и зачем нужна технологияORM?
При написании объектно-ориентированного кода, который взаимодействует с базой данных, у разработчика возникает несколько проблем:
данные в программе и в базе данных используют разные парадигмы (объектно-ориентированная и реляционная соответственно). Работу по преобразованию данных из одной парадигмы в другую ложатся на плечи программиста, что влечет за собой лишнюю работу и может приводить к ошибкам в процессе преобразования;
программисту желательно абстрагироваться от конкретной схемы хранения данных. То есть, программисту желательно работать не с реляционной базой данных, а просто с некоторым «хранилищем», а конкретная реализация этого «хранилища» может быстро и безболезненно меняться.
Для устранения этих проблем используется технология ORM(Object-Relational Mapping, «объектно-реляционное отображение») — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных».
Проще говоря, ORM– это прослойка, посредник между базой данных и объектным кодом. Используя ORM, программист не занимается формированием SQL-запросов и не думает в терминах «таблица», «записи» и «реляционные отношения», а просто работает с «хранилищем объектов» – он может туда записывать и получать объекты, не заботясь о подробностях их хранения.
В Java предусмотрен специальный стандарт JPA (Java Persistence API), который использует концепцию ORM. Существует несколько реализаций этого интерфейса, например, Hibernate, OpenJPA, EclipseLink и другие.
SpringDataJPA – обертка над JPA в Spring, которая предоставляет много полезных «фишек» разработчику. Она позволяет легче создавать Spring-управляемые приложения, которые используют новые способы доступа к данным, например нереляционные базы данных, map-reduce фреймворки, cloud сервисы, а так же уже хорошо улучшенную поддержку реляционных баз данных.
ТерминологияJPA
Основное понятие JPA – сущность (Entity). Сущность – это Java-класс, который представляет бизнес-логику приложения и определяет данные, которые будут храниться в базе данных и извлекаться из нее.
Как правило, класс сущности представляет таблицу в базе данных, поля или свойства класса представляют собой колонки в таблице, а объект сущности представляет собой одну запись в таблице.
Важным моментом при работе с JPA являются аннотации, коих здесь будет очень много. Разберемся с некоторыми из них:
@Entity – позволяет серверу узнать, что это не просто какой-то класс, а сущность;
@Id – помечает первичный ключ в таблице. Вопрос составных ключей в данном занятии не рассматривается;
@Table – позволяет настраивать отображение класса в таблицу. В данном случае, мы можем указать, какое имя будет иметь соответствующая таблица в базе данных;
@GeneratedValue – указывает, что данное поле является генерируемым значением. Очень часто этой аннотацией помечают первичные ключи, чтобы они генерировались автоматически при добавлении новых записей в таблицу;
@Column – позволяет настраивать отображение колонки в таблице. В данном случае, мы можем указать, какое имя будет иметь соответствующая колонка в таблицу.
Репозитории. Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.
В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.
Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.
Практическая часть
Установим СУБД Postgres и запустим pgAdmin 4.
Создадим пользователя ejournal_user, после чего создадим базу данных для нашего приложения.
Добавляем в pom.xml зависимости для работы с Spring Data JPA и JDBC драйвер для Postgres.
Далее необходимо настроить подключение к СУБД и нужной базе данных.
Для настройки приложения Spring воспользуемся языком YAML. Для этого удалим файл resources/application.properties и создадим вместо него файл application.yml.
Создадим класс сущности Student
Для уменьшения количества кода, мы будем использовать плагин Lombok.
Проект Lombok — это плагин компилятора, который добавляет в Java новые «ключевые слова» и превращает аннотации в Java-код, уменьшая усилия на разработку и обеспечивая некоторую дополнительную функциональность.
Lombok преобразует аннотации в исходном коде в Java-операторы до того, как компилятор их обработает: зависимость lombok отсутствует в рантайме, поэтому использование плагина не увеличит размер сборки.
При использовании Lombok наш исходный код не будет валидным кодом Java. Поэтому потребуется установить плагин для IDE, иначе среда разработки не поймёт, с чем имеет дело. Lombok поддерживает все основные Java IDE. Интеграция бесшовная. Все функции вроде «показать использования» и «перейти к реализации» продолжают работать как и раньше, перемещая вас к соответствующему полю/классу.
Далее подключим библиотеку в pom.xml.
Вернемся в класс Student, добавим аннотацию для геттеров, сеттеров, а также конструктор со всеми параметрами.
Программирование слоя сервисов
Service – это Java класс, который содержит в себе основную бизнес-логику. В основном сервис использует готовые DAO/Repositories или же другие сервисы, для того чтобы предоставить конечные данные для пользовательского интерфейса. Сервисы, как правило, вызываются контроллерами или другими сервисами.
Объект службы создается контейнером Spring, каждая служба является «одиночкой» (синглтоном), который создается в момент запуска приложения и уничтожается в момент закрытия приложения. Обратите внимание на аннотацию @Service. Этой аннотацией мы сообщаем контейнеру Spring, что это не просто класс, а класс сервиса.
Итак, мы создали службу, у которой есть два публичных метода. Первый метод добавляет нового студента, второй метод возвращает список всех студентов. В дальнейшем служба будет обращаться к объекту репозитория за данными, а пока что оставим код таким, какой он есть.
Вернемся к созданию веб-слоя. Создадим класс контроллера, создадим две конечные точки: для добавления студента и для получения списка всех студентов.
Обратите внимание, что мы не создаем объект службы, а получаем его «извне» с помощью аннотации @Autowired. Контейнер Spring «внедрит» ссылку на объект службы в поле service. Подробнее про внедрение зависимостей будет изложено в следующем занятии.
Работа с репозиторием
Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.
В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.
Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.
Теперь перейдем в класс службы и создадим ссылку на объект репозитория.
Обратите внимание, что мы не создавали класс, который реализует интерфейс StudentRepository, тогда откуда мы его получим объект интерфейсного типа? Дело в том, что Springсгенерирует класс за нас. Этот сгенерированный класс будет иметь набор стандартных операций для работы с сущностями. В нашем случае, это операция findAll(), которая возвращает все сущности в таблице student.
Запустим сервер и выполним два клиентских запроса - один на создание студента, второй - на получение списка всех студентов.
Добавляем нового студента
Теперь получим список всех студентов.
Реализация отношения "один-ко-многим"
Как мы знаем, важной составляющей реляционных баз данных является отношения между таблицами "один-к-одному", "один-ко-многим", "многие-ко-многим".
Реализуем отношение "один-ко-многим". Создадим сущность Group - студенческая группа. В студенческой группе может быть от 0 до N студентов.
Прежде всего перейдем в сущность Student. Добавим поле group, который будет ссылаться на студенческую группу, в которой будет состоять студент. Так как в группе может быть много студентов, указываем аннотацию @ManyToOne. Также указываем аннотацию @JoinColumn, которая указывает на имя колонки, которая будет содержать Foreign Key.
Технология ORM позволяет создавать двусторонние связи между таблицами. В этом случае, при выдаче JSON, может возникнуть бесконечный цикл. Чтобы его избежать, укажем аннотацию @JsonIgnore. В этом случае, колонка group будет проигнорирована в процессе сериализации\десериализации.
Далее создадим сущность Group.
Обратите внимание, что отношение один-ко-многим мы моделируем с помощью обычной коллекции. Указываем аннотацию @OneToMany, также в свойстве mappedBy указываем, какое поле "держит" отношение со стороны студента.
Далее модифицируем класс контроллера. Создадим конечные точки для добавления новой группы, а также для получения списка всех групп. Также модифицируем конечную точку для добавления студента, чтобы указать id группы, в которую необходимо добавить студента.
Теперь создадим репозиторий для сущности Group.
Далее модифицируем класс сервиса. Добавим методы для добавления новой группы, а также для получения списка всех групп. Также модифицируем метод добавления новой группы. Метод работает следующим образом: получаем объект группы по id, после чего добавляем ссылку на группу в поле group объекта Student.
Запустим приложение и проверим его работу. Сначала добавим группу, после чего получим список групп.
Добавим новую группу
Получим список групп
Теперь добавим нового студента
Получим список всех групп
Вложенные и внутренние классы
В Java определены вложенные классы. Вложенным называется такой класс, который объявляется в другом классе.
Вложенный класс не может существовать независимо от класса, в который он вложен. Следовательно, область действия вложенного класса ограничена его внешним классом. Если вложенный класс объявлен в пределах области действия внешнего класса, то он становится членом последнего. Имеется также возможность объявить вложенный класс, который станет локальным в пределах блока
Существуют два типа вложенных классов. Одни вложенные классы объявляются с помощью модификатора доступа static, а другие - без него. Нестатический вариант вложенных классов называются внутренними.
Внутренние классы позволяют вам группировать классы, логически принадлежащие друг другу, и управлять доступом к ним. Однако важно понимать, что внутренние классы заметно отличаются от композиции.
Внутренний класс может взаимодействовать со своим внешним классом, а код, написанный с использованием внутренних классов, получается более элегантным и понятным.
Создание внутренних классов
Создать внутренний класс несложно - достаточно разместить определение класса внутри окружающего класса.
Внутренний класс имеет доступ ко всем переменным и методам внешнего класса, в который он вложен, и может обращаться к ним непосредственно, как и все остальные нестатические члены внешнего класса.
Иногда внутренний класс используется для предоставления ряда услуг внешнему классу, в котором он содержится.
Класс можно вложить в область действия блока. В итоге получается локальный класс, недоступный за пределами блока.
Также внутренний класс можно быть анонимным классом (анонимные классы уже были рассмотрены ранее).
Заходим на сайт , скачиваем последнюю версию установщик и устанавливаем Git.
Как видим, мы успешно развернули проект на Heroku. Проверим работу веб-сервиса. Перейдем в браузер и укажем адрес
Лекция Bob Martin "The Future of Programming"-
Книги (папка с книгами - )
Зарегистрироваться на сайте и пройти следующие уроки:
@Entity
@Table(name = "students")
@Data
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private int age;
}
Service.java
@org.springframework.stereotype.Service
public class Service {
public void addStudent(Student student, int id) {
// Добавление нового студента
}
public List<Student> getAllStudents() {
// Получение списка студентов
}
}
Controller.java
@RestController
public class Controller {
@Autowired
private Service service;
@PostMapping("/student")
public void addStudent(@RequestBody Student student) {
service.addStudent(student);
}
@GetMapping("/student")
public List<Student> getAllStudents() {
return service.getAllStudents();
}
}
@Entity
@Table(name = "groups")
@Data
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
@OneToMany(mappedBy = "group")
private List<Student> studentList;
}
StudentRepository.java
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
Service.java
@org.springframework.stereotype.Service
public class Service {
@Autowired
private StudentRepository studentRepo;
public void addStudent(Student student) {
studentRepo.save(student);
}
public List<Student> getAllStudents() {
return studentRepo.findAll();
}
}
Student.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstname;
private String lastname;
private int age;
@ManyToOne (fetch = FetchType.LAZY,optional = false)
@JoinColumn(name = "group_id",nullable = false)
@JsonIgnore
private Group group;
}
Group.java
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "groups")
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(mappedBy = "group", cascade = CascadeType.ALL)
private List<Student> students;
public void addStudent(Student student) {
students.add(student);
}
}
Controller.java
@RestController
public class Controller {
@Autowired
private Service service;
@PostMapping("/student/{group_id}")
public void addStudent(@RequestBody Student student, @PathVariable(name = "group_id") int group_id) {
service.addStudent(student, group_id);
}
@GetMapping("/student")
public List<Student> getAllStudents() {
return service.getAllStudents();
}
@PostMapping("/group")
public void addGroup(@RequestBody Group group) {
service.addGroup(group);
}
@GetMapping("/group")
public List<Group> getAllGroups() {
return service.getAllGroups();
}
}
GroupRepository.java
public interface GroupRepository extends JpaRepository<Group, Integer> {}
Service.java
@org.springframework.stereotype.Service
public class Service {
@Autowired
private StudentRepository studentRepo;
@Autowired
private GroupRepository groupRepo;
public void addStudent(Student student, int id) {
Group g = groupRepo.getOne(id);
student.setGroup(g);
studentRepo.save(student);
}
public List<Student> getAllStudents() {
return studentRepo.findAll();
}
public void addGroup(Group group) {
groupRepo.saveAndFlush(group);
}
public List<Group> getAllGroups() {
return groupRepo.findAll();
}
}
public class Main {
public static void main(String[] args) {
Parcel parcel = new Parcel();
parcel.ship("Лимпопо");
}
}
class Parcel {
class Contents {
private int i = 11;
public int value() {return i;}
}
class Destination {
private String label;
public Destination(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
public void ship(String destination) {
Contents c = new Contents();
Destination d = new Destination(destination);
System.out.println(d.getLabel());
}
}
public class Outer {
private List<Integer> numbers = new ArrayList<>();
public void printAnalyticalData() {
Inner inner = new Inner();
System.out.println("Min: " + inner.min());
System.out.println("Max: " + inner.max());
System.out.println("Sum: " + inner.sum());
}
private class Inner {
private int min() {
return numbers.stream().min(Integer::compareTo).orElse(0);
}
private int max() {
return numbers.stream().max(Integer::compareTo).orElse(0);
}
private int sum() {
return numbers.stream().reduce(Integer::sum).orElse(0);
}
}
}
public class Main {
public static void main(String[] args) {
class Local {
private int value;
private Local(int value) {
this.value = value;
}
}
Local local = new Local(10);
}
}
2. Перегрузка методов
1. Что такое перегрузка методов
В Java разрешается в одном и том же классе определять два или более метода с одинаковым именем, если только объявления их параметров отличаются. В этом случае методы называются перегружаемыми, а сам процесс – перегрузкой метода (method overloading).
class MyClass {
public void foo() {
// ... код
}
public void foo(String s) {
// ... код
}
}
Если у методов одинаковые имена, как Java узнает, какой именно из них вызывается? Ответ прост: перегружаемые методы должны отличаться по типу и/или количеству входных параметров. Даже разного порядка аргументов достаточно для того, чтобы методы считались разными (хотя это не рекомендуется).
Перегрузка по возвращаемым значениям
Логично спросить, почему при перегрузке используются только имена классов и списки аргументов? Почему не идентифицировать методы по их возвращаемым значениям?
// ДАННЫЙ КОД ВЫЗОВЕТ ОШИБКУ КОМПИЛЯЦИИ!
class MyClass {
public int foo() {
return 0;
}
public double foo() {
return 0;
}
}
Идентифицировать их нельзя, потому что Java в этом случае не может определить, какая версия метода должна выполняться.
При вызове перегружаемого метода для определения нужного варианта в Java используется тип и\или количество аргументов метода. Следовательно, перегружаемые методы должны отличаться по типу и\или количеству их параметров. Возвращаемые типы перегружаемых методов могут отличаться, но самого возвращаемого метода недостаточно, чтобы отличить два разных варианта метода. Когда в исполняющей среде Java встречается вызов перегружаемого метода, в ней просто выполняется тот вариант, параметры которого соответствуют аргументам, указанным в вызове.
Перегрузка методов позволяет поддерживать принцип «один интерфейс, несколько методов».
В языках программирования без перегрузки методов, каждому методу должно быть присвоено однозначное имя. Но зачастую требуется реализовать, по существу, один и тот же метод для разных типов данных.
В таком случае, в языках программирования без перегрузки реализуют несколько методов, которые немного отличаются названиями.
Перегрузка методов ценна тем, что позволяет обращаться к похожим методам по общему имени. Следовательно, имя представляет общее действие, которое должно выполняться. Выбор подходящего варианта метода для конкретной ситуации входит в обязанности компилятора.
Ничто не запрещает вам реализовать несколько перегруженных методов, каждый из которых будет работать совершенно по-разному. Но на практике крайне рекомендуется, чтобы перегруженные методы реализовывали одну и ту же общую операцию.
Перегрузка конструкторов
Наряду с перегрузкой обычных методов можно также выполнять перегрузку конструкторов. Перегружаемые конструкторы – это норма и часто используемый прием.
Соответствующий перегружаемый конструктор вызывается в зависимости от параметров, указываемых при выполнении оператора new.
class MyClass {
public MyClass() {
// какой-то код
}
public MyClass(int arg0) {
// какой-то код
}
public MyClass (int arg0, String arg1) {
// какой-то код
}
}
Если вы пишете для класса несколько конструкторов, иногда бывает удобно вызвать один конструктор из другого, чтобы избежать дублирования кода. Такая операция проводится с использованием ключевого слова this.
class MyClass {
public MyClass() {
// Вызываем конструктор MyClass(int arg0)
this(0);
}
public MyClass(int arg0) {
// Вызываем конструктор MyClass (int arg0, String arg1)
this(arg0, " ");
}
public MyClass(int arg0, String arg1) {
// какой-то код
}
}
Задание на лабораторную работу
Задания для выполнения:
1. Программное обеспечение для университета
При выполнении задания продумайте следующие аспекты:
имена полей и типы данных;
как реализовать "уникальность" номера зачетной книжки и номера группы в рамках приложения;
модификаторы доступа для полей и методов;
тип методов, которые требуется реализовать;
если надо, реализуйте приватные методы и добавьте приватные поля, которые должны обеспечивать работу публичных методов.
1.1 Класс Student
Создайте публичный класс Student – студента некоторой специальности некоторого университета. Класс не хранит явным образом информацию о специальности, номере группы\потока, предметах, университете.
В классе должны быть следующие поля:
имя;
фамилия;
год поступления;
уникальный шестизначный номер зачетной книжки.
В классе должны быть следующие конструкторы:
принимает на вход имя и фамилию. При использовании данного конструктора, в номер зачетной книжки записывается 0;
принимает на вход имя, фамилию, номер зачетной книжки.
В классе должны быть следующие методы:
метод возвращает имя студента;
метод изменяет имя студента;
метод возвращает фамилию студента;
метод изменяет фамилию студента;
метод возвращает номер зачетной книжки;
метод изменяет номер зачетной книжки;
метод возвращает год поступления;
метод изменяет год поступления.
1.2 Класс Group
Создайте публичный класс Group – студенческой группы. Класс не хранит явным образом специальность и название университета.
В классе должны быть следующие поля:
уникальный номер (в пределах специальности);
массив студентов.
В классе должны быть следующие конструкторы:
принимает на вход номер группы (в этом случае, количество студентов записывается как 0);
принимает на вход номер группы, количество студентов (инициализация массива, но его элементы остаются пустыми);
принимает на вход массив студентов.
В классе должны быть следующие методы:
метод возвращает номер группы;
метод изменяет номер группы;
метод возвращает общее число студентов группы;
метод возвращает ссылку на студента по номеру зачетной книжки;
метод удаляет студента из группы по номеру зачетной книжки (помните про корректное удаление элемента из массива);
метод добавляет нового студента в группу (принимает на вход ссылку на объект Student, если массив уже полностью заполнен - реализуйте расширение массива);
метод возвращает массив студентов;
метод возвращает массив студентов, отсортированный по фамилиям (подумайте, как это сделать; если фамилии одинаковы - сортируйте по имени; если имена одинаковы - по номеру зачетной книжки).
1.3 Класс Main
В классе Main напишите код, чтобы протестировать функциональность созданных классов и реализованных методов
1.4 Модификация класса Group
Класс Group содержит внутри себя массив студентов. Методы класса Group требуют увеличения или уменьшения размера массива, вставки элемента внутри массива, вывод данных массива в отсортированном порядке.
Итого, есть данные и операции над данными (вставка, удаление, изменение размера, вывод в отсортированном порядке).
Используя механизм инкапсуляции и принципы ООП, модифицируйте класс Group так, чтобы программа больше соответствовала принципам ООП.
Статические поля и методы
Обычно при создании класса вы описываете, как объекты этого класса ведут себя и как они выглядят. Объект появляется только после того, как он будет создан ключевым словом new, и только начиная с этого момента для него выделяется память и появляется возможность вызова методов.
Но есть ситуации, в которых такой подход недостаточен. Первая ситуация - это когда некоторые данные должны храниться "в единственном числе" независимо от того, сколько было создано объектов класса. Вторая - когда вам потребуется метод, не привязанный ни к какому конкретному объекту класса (то есть метод, который можно вызвать даже при полном отсутствии объектов класса).
Такой эффект достигается путем использования ключевого слова static, делающего элемент класса статическим. Поля или методы, которые объявлены как static, не привязаны к определенному экземпляру этого класса. Поэтому даже если в программе еще не создавался ни один объект класса, вы можете вызывать статический метод или получить доступ к статическим полям. С обычным объектом вам необходимо сначала создать его и использовать для вызова метода или доступа к полям, так как нестатические данные должны точно знать объект, с которым работают.
Некоторые ОО языки используют термин данные уровня класса и методы уровня класса, подразумевая, что данные и методы существуют только на уровне класса в целом, а не для отдельных объектов этого класса.
Понятие статического поля
Чтобы сделать данные или метод статическими, просто поместите ключевое слово static перед их определением. Например, следующий код создает статическое поле класса и инициализирует его:
StaticTest.java
class StaticTest {
static int val = 924;
}
Теперь, даже при создании двух объектов StaticTest, для элемента StaticTest.val выделяется единственный блок памяти. Оба объекта совместно используют одно значение val. Пример:
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
В данном примере как st1.val, так и st2.val имеют одинаковые значения, равные 924, потому что они ссылаются на один блок памяти.
Существует два способа обратиться к статической переменной. Как было видно выше, на нее можно ссылаться по имени объекта, например st2.val. Также возможно обратиться к ней прямо через имя класса; для нестатических членов класса такая возможность отсутствует.
StaticTest.val++;
После выполнения операции, значения st1.val и st2.val будут равны 48.
Синтаксис с именем класса является предпочтительным, потому что он не только подчеркивает, что переменная является статической, но и в некоторых случаях предоставляет компилятору больше возможностей для оптимизации.
Если для инициализации статических переменных требуется произвести вычисления, то для этой цели достаточно объявить статический блок, который будет выполняться только один раз при первой загрузке класса. Пример:
class NetworkConnector {
public static final int baud;
private static final int bits_per_interval = 4;
static {
baud = 9600 / bits_per_interval;
}
// ...
}
Как только загружается класс NetworkConnector, выполняются все действия в статическом блоке.
Понятие статического метода
Та же логика верна и для статических методов. Вы можете обратиться к такому методу или через объект, как это делается для всех методов, или в специальном синтаксисе ИмяКласса.метод(). Статические методы определяются по аналогии со статическими данными:
class Incrementable {
static void increment() {
StaticTest.val++;
}
}
Нетрудно заметить, что метод increment() класса Incrementable увеличивает значение статического поля val. Метод можно вызвать стандартно, через объект:
Incrementable sf = new Incrementable();
sf.increment();
Или, поскольку метод increment() является статическим, можно вызвать его с прямым указанием класса:
Incrementable.increment();
Если применительно к полям ключевое слово static радикально меняет способ определения данных (статические данные существуют на уровне класса, в то время как нестатические данные существуют на уровне объектов), то в отношении методов изменения не столь принципиальны. Одним из важных применений static является определение методов, которые могут вызываться без объектов.
На статические методы налагаются следующие ограничения:
они могут непосредственно вызывать только другие статические методы;
им непосредственно доступны только статические переменные;
им недоступны ключевые слова this или super.
Статический импорт
В языке Java имеется языковое средство, расширяющее возможности ключевого слова import и называемое статическим импортом. Оператор import, предваряемый ключевым словом static, можно применять для импорта статических членов класса или интерфейса. Благодаря статическому импорту появляется возможность ссылаться на статические члены непосредственно по именам, не используя имя класса. Это упрощает и сокращает синтаксис, требующийся для работы со статическими членами.
Рассмотрим пример без использования статического импорта. Представим себе, что мы вычисляем гипотенузу прямоугольного треугольника. Мы будем часто использовать методы Math.pow() и Math.sqrt()
получается слишком громоздкой. Подобных неудобств можно избежать, если воспользоваться статическим импортом. Рассмотрим пример ниже
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;
public class Main {
public static void main(String[] args) {
double side1 = 3.0;
double side2 = 4.0;
double hypot;
hypot = sqrt(pow(side1, 2) + pow(side2, 2));
}
}
После использования статического импорта нет нужды использовать имя класс Math для вызова статических методов pow() и sqrt().
Если предполагается применять много статических методов или полей, определенных в классе, то можно импортировать класс Math полностью
import static java.lang.Math.*;
Каким бы удобным ни был статический импорт, очень важно не злоупотреблять им. Не следует забывать, что библиотечные классы Java объединяются в пакеты для того, чтобы избежать конфликтов пространств имен и непреднамеренного сокрытия прочих имен. Если статический член используется в программе один или два раза, то его лучше не импортировать. Статический импорт следует оставить на тот случай, если статический члены применяются многократно, в частности при выполнении математических вычислений.
1. Инкапсуляция
Инкапсуляция в Java реализована с помощью использования модификаторов доступа.
Язык Java предоставляет несколько уровней защиты, которые позволяет настраивать область видимости данных и методов. В Java имеется четыре категории видимости элементов класса:
private– члены класса доступны только членам данного класса. Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию;
по умолчанию (package-private) – члены класса доступны классам, которые находятся в этом же пакете;
protected– члены класса доступны классам, находящимся в том же пакете, и подклассам – в других пакетах;
public– члены класса доступны для всех классов в этом и других пакетах.
Модификатор класса указывается перед остальной частью описания типа отдельного члена класса. Это означает, что именно с него должен начинаться оператор объявления класса.
public String errMessage;
private AccountBalance balance;
private boolean isError(byte status) {}
public class Account {}
Когда член класса обозначается модификатором доступа public, он становится доступным для любого другого кода в программе, включая и методы, определенные в других классах.
Когда член класса обозначается модификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену класса.
При отсутствии модификатора доступа, члены класса доступны другим членам класса, который находится в этом же пакете.
Модификатор доступа protected связан с использованием механизма наследования и будет рассмотрен позже.
Модификатор доступа указывается перед остальной частью описания типа отдельного члена класса (то есть, именно с модификатора доступа начинается объявление члена класса).
Член класса (переменная, конструктор, методы), объявленный public, доступен из любого метода вне класса.
Всё что объявлено private, доступно только конструкторам и методам внутри класса и нигде больше. Они выполняют служебную или вспомогательную роль в пределах класса и их функциональность не предназначена для внешнего пользования. Закрытие (private) полей обеспечивает инкапсуляцию.
Сокрытие полей класса
В подавляющем большинстве случаев, поля класса объявляются как private (это не касается статических переменных и констант, там ситуация может быть другая). Должны быть веские основания объявить поле класса общедоступным. Манипулирование данными должно осуществляться только с помощью методов.
Для того чтобы дать возможность получить доступ к переменной или дать возможность изменить ее значение, объявляют специальные методы, которые называются "геттерами" и "сеттерами".
Геттер возвращает значение приватного поля, тогда как сеттер меняет значение приватного поля (новое значение передается в качестве аргумента метода).
Хотя сигнатура и имена геттеров и сеттеров могут быть любыми, приучите себя соблюдать строгий шаблон для объявления геттеров и сеттеров.
Геттер должен иметь префикс get, после которого идет название поля с большой буквы. Геттер, как правило, не имеет входных аргументов.
Сеттер должен иметь префикс set, после которого идет название поля с большой буквы. Сеттер принимает на вход новое значение поля. Возвращаемый тип, как правило, void.
Account.java
public class Account {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
Большинство IDE для Java имеют механизм для генерации геттеров и сеттеров. В IntelliJ IDEA нажмите комбинацию Alt+Insert находясь в окне редактирования java-файла. Откроется контекстное меню Generate, где вы можете выбрать генерацию геттера и сеттера, после чего указать поля, для которых необходимо сгенерировать методы.
5. Пример использования инкапсуляции
Представим, что нам необходимо создать класс «Корзина» (Cart), который хранит в себе набор объектов класса «Товар» (Item).
Какие методы «Корзина» должна предоставлять для внешнего использования? Это могут быть, например, методы «Добавить товар», «Убрать последний добавленный товар», «Подсчет суммы цен товаров в корзине», «Повышение цен в корзине на N процентов» и «Снижение цен в корзине на N процентов».
Название метода
Описание
public Cart(int capacity)
Конструктор с 1 параметром – максимальным количеством товаров в корзине.
public boolean addItem(Item item)
Добавление товара в корзину. Возвращает успешность операции.
public Item deleteLastAddedItem()
Удаление последнего добавленного товара в корзину. Возвращает удаленный товар.
public double calculateItemPrices()
Подсчет суммы цен всех товаров в корзине.
public void raiseItemPrices(double percent)
Поднять цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).
public void cutItemPrices(double percent)
Снизить цены товаров в корзине на определенный процент (значение процента передается как аргумент метода).
Как вы можете заметить, это публичные методы, а значит, их можно вызвать через оператор-точку имея ссылку ну объект.
Cart cart = new Cart();
cart.addItem(new Item("Клавиатура", 2000));
Перечень этих публичных методов и составляет интерфейс класса – то есть, с помощью этих методов объект класса будет взаимодействовать с внешним миром.
Эти методы имеют вполне четко определенные входные аргументы и могут возвращать значения четко определенных типов, и никак иначе. По аналогии с этим, поворот колес автомобиля осуществляется четко определенным образом – поворотом руля, и бензин надо заливать в четко определенное отверстие крышки бензобака, а не как-то еще.
То – как будет реализовано хранение товаров в корзине – это внутренняя логика класса и она не должна быть доступна внешнему миру, она должна быть скрыта от внешнего вмешательства. Другие классы, которые будут использовать объекты класса Cartне должны знать и не должны иметь доступ к тому – как там «внутри» реализовано хранение товаров, подсчет цен и изменение цены на определенный процент и так далее, они могут только лишь использовать предоставленные им публичные методы. Давайте реализуем «Корзину» с помощью структуры «стек», которая, в свою очередь, реализована обычным массивом.
Cart.java
public class Cart {
private Item[] stack; // массив для реализации стека
private int topIndex; // указатель на вершину стека
// При создании корзины мы должны
// указать максимальное количество элементов
// в корзине
public Cart(int capacity) {
stack = new Item[capacity];
topIndex = -1;
}
// Добавление нового товара в корзину
public boolean addItem(Item item) {
return push(item);
}
// Приватный метод, который реализует добавление в стек
private boolean push (Item item) {
// Добавляем товар в стек
return true; // или false если не стек переполнен
}
// Удаление последнего добавленного товара в корзину
public Item deleteLastAddedItem() {
return pop();
}
// Приватный метод, который реализует извлечение из стека
private Item pop() {
return new Item(); // Извлеченный из стека товар
}
}
Как мы видим, массив с товарами, указать на вершину стек объявлены как privateчлены класса. Это значит, что мы не можем получить к ним доступ извне – они доступны только внутри данного класса.
Программиста, который будет использовать класс Cart, не должна волновать ситуация с переполнением стека, с попыткой извлечь элемент из пустого стека, он не должен следить за указателем на вершину стека, он даже не должен знать что это стек.
Для него объект класса Cart это некоторый объект, который предоставляет «услугу» в виде корзины товаров и с этой корзиной можно работать с помощью определенных публичных методов.
В дальнейшем мы можем переделать класс Cart и поменять внутреннюю реализацию. Мы можем использовать структуру "очередь", мы можем использовать коллекции, мы можем иначе реализовать операции добавления и удаления элемента в стеке, но если мы сохраним интерфейс класса неизменным, то для внешнего мира эти изменения внутренней логики не будут важны и если мы поменяем внутреннюю логику одного небольшого участка программы, то вся остальная программа будет работать так же.
Python
Ссылки на материалы:
Комментарии - стр. 35
Метод format - стр. 36 - 37
Отступы - стр. 42 - 43
Оператор if - стр. 51 - 54
Цикл for - стр. 55 - 57
Функции - стр. 59 - 60
Списки - стр. 79 - 81
Последовательности - стр. 85 - 87
Материал по "срезам" (slices, используйте при работе со списками, для работы с частями списка):