\documentclass[j-spec.tex]{subfiles} \begin{document} \sloppy \setcounter{section}{6} \setlength{\columnsep}{22pt} \pagestyle{plain} \tableofcontents \section{Инструментарий: программные интерфейсы} \subsection*{В предыдущем разделе} В предыдущем разделе были рассмотрены графические интерфейсы пользователя. \begin{itemize} \item Создание окон, \item размещение компонентов, \item рисование элементов, \item обработка событий. \end{itemize} \subsection*{В этом разделе} Будет рассмотрено понятие и принцип работы программных интерфейсов, ключевое слово \code{implements}. Реализация интерфейса, реализация по умолчанию, частичная реализация интерфейса, наследование и множественное наследование интерфейсов. Отдельно будет рассмотрен принцип создания так называемых адаптеров и анонимных классов. Знания в области применения графических фреймворков будут дополнены информацией о поведении исключений на интерфейсе пользователя. \begin{itemize} \item \nom{Интерфейс}{(API, англ. application programming interface) — описание способов взаимодействия одной компьютерной программы с другими (API приложения) или компонентов одной программы между собой (API объектов).}; \item \nom{implements}{ключевое слово языка, указывающее на то, какой интерфейс (или интерфейсы) реализует класс.}; \item \nom{Анон. класс}{это класс, не имеющий имени и его создание происходит в момент инициализации объекта.}; \item \nom{Реализация}{это класс, содержащий переопределённые методы интерфейса, выполняющий конкретные действия при вызове методов интерфейса.}; \item \nom{Адаптер}{структурный шаблон проектирования, предназначенный для организации использования функций объекта, недоступного для модификации, через специально созданный интерфейс. Другими словами -- это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.}; \item \nom{Р\'{е}ндеринг}{термин в компьютерной графике, обозначающий процесс получения изображения по модели с помощью компьютерной программы.}; \item \nom{SOLID}{Мнемонический акроним для первых пяти принципов, названных Робертом Мартином, которые означали пять основных принципов объектно-ориентированных проектирования и программирования.}; \end{itemize} \begin{frm} \excl Обычно -- теория и примеры. В этом разделе повествование будет построено от практики и плохого кода к хорошему коду и теоретическому обоснованию сделанного. \end{frm} \subsection{Введение и результат} В этом разделе важно не особенно обращать внимание на то, какие именно классы и методы используются, а внимательно следить за взаимодействием и отношениями объектов, потому что интерфейсы, о которых далее планируется говорить -- это механизм упрощающий и универсализирующий взаимодействия объектов. Поначалу, код может показаться непростым, но задача будет поставлена таким образом, что без программных интерфейсов не обойтись. Почему будет сложно? Несмотря на то что принципы ООП уже известны, нужно уметь их применять. \begin{figure}[H] \centering \includegraphics[width=12cm]{jd-02-result-circles.png} \caption{Результат выполнения написанного кода} \label{pic:result-circles} \end{figure} В результате работы с этим разделом будет создан некий небольшой демонстрационный набросок игрового двухмерного движка, без физики, с объектами и анимацией. На рисунке \hrf{pic:result-circles} изображено окно с кружками\footnote{\href{https://vimeo.com/852133775}{Видео с демонстрацией движения}}. На окне ничего не обрабатывается и практически ничего не происходит. Для реализации задуманного понадобится: окно, которое будет взаимодействовать с операционной системой, канва, на которой будет происходить рисование, и объекты, которые будут нарисованы. \subsection{Подготовка проекта} \subsubsection{Основное окно} Сложное окно не нужно: константы с размерами, координатами, и конструктор здесь же в основном методе. Самое важное сейчас это то, что окно -- это объект с какими-то свойствами и каким-то поведением. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Основное окно},label={lst:init-window}] package ru.gb.jdk.two.online; import javax.swing.*; public class MainWindow extends JFrame { private static final int POS_X = 400; private static final int POS_Y = 200; private static final int WINDOW_WIDTH = 800; private static final int WINDOW_HEIGHT = 600; private MainWindow() { setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setBounds(POS_X, POS_Y, WINDOW_WIDTH, WINDOW_HEIGHT); setTitle("Circles"); setVisible(true); } public static void main(String[] args) { new MainWindow(); } } \end{lstlisting} \subsubsection{Канва для рисования} \label{subsubsec:circles-canvas} Для рисования будет использоваться компонент \code{JPanel}, наследнику панели будет дано название \code{MainCanvas}. Любой компонент фреймворка Swing может перерисовываться, вызывая метод \code{paintComponent()}. Для начала, в конструкторе панели для отработки связи компонентов следует делать что-то незначительное, например, менять цвет фона на синий. \begin{frm} \info Переопределив метод перерисовки панели не следует удалять вызов родительского метода, поскольку предполагается, что перерисовка панели происходит хорошо, но туда будет добавлена логика. \end{frm} Также для универсализации дальнейших вызовов (удобства взаимодействия с канвой) следует добавить методы, возвращающие границы панели. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Панель для канвы},label={lst:init-canvas}] package ru.gb.jdk.two.online; import javax.swing.*; import java.awt.*; public class MainCanvas extends JPanel { MainCanvas() { setBackground(Color.BLUE); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); } public int getLeft() { return 0; } public int getRight() { return getWidth() - 1; } public int getTop() { return 0; } public int getBottom() { return getHeight() - 1; } } \end{lstlisting} Далее необходимо расположить канву на основном окне и осуществить привязку всех действий канвы ко времени физического мира. В конструкторе основного окна создаётся переменная класса \code{MainCanvas} и располагается в центре. \begin{frm} \excl Нет прямого запрета на написание логики будущего движка или игры в классе канвы, но это архитектурно неверное решение, ведь на канве, на которой происходит рисование, должно происходить только рисование. Подробнее в разделе \hrf{subsubsec:solid} \end{frm} Исходя из принципа единой ответственности, принимается решение о том, что логика взаимодействия объектов будет описана в основном классе, а \code{MainCanvas} останется универсальным, чтобы иметь возможность в дальнейшем рисовать что угодно. Для этого следует описать в основном окне метод, который будет периодически вызываться канвой, например, \code{onDrawFrame()}. В нём будет описываться бизнес-логика. На начальном этапе, это два метода -- \code{update()} который будет изменять состояние приложения, и \code{render()}, который будет отдавать команды рисующимся компонентам. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отделение логики}] private MainWindow() { setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setBounds(POS_X, POS_Y, WINDOW_WIDTH, WINDOW_HEIGHT); setTitle("Circles"); MainCanvas canvas = new MainCanvas(); add(canvas); setVisible(true); } public void onDrawFrame() { update(); render(); } private void update() { } private void render() { } \end{lstlisting} \subsubsection{Цикл отрисовки} Перерисовка канвы -- это циклический процесс, и на каждой итерации \code{MainCanvas} должен вызывать метод \code{onDrawFrame()} основного класса. Для этого канве необходимо иметь ссылку на основное окно и внутри метода \code{paintComponent()} вызывается метод \code{controller.onDrawFrame()}. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Событие отрисовки}] public class MainCanvas extends JPanel { private final MainWindow controller; MainCanvas(MainWindow controller) { setBackground(Color.BLUE); this.controller = controller; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); controller.onDrawFrame(); } \end{lstlisting} Далее, чтобы зациклить это действие, возможно два пути: самый простой -- создать постоянно обновляющуюся канву, то есть в методе \code{paintComponent()} вызывать \code{repaint()} но это полностью нагрузит одно из ядер процессора только отрисовкой окна. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Цикл отрисовки}] public class MainCanvas extends JPanel { private final MainWindow controller; MainCanvas(MainWindow controller) { setBackground(Color.BLUE); this.controller = controller; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); controller.onDrawFrame(); repaint(); } \end{lstlisting} Второй путь -- любой поток возможно заставить какое-то время поспать, для этого вызывается статический метод класса \code{Thread}, принимающий в качестве аргумента количество миллисекунд, которое поток должен обязательно поспать. Это даст FPS\footnote{FPS, Frames per second -- (англ. кадров в секунду) количество сменяемых кадров за единицу времени в кинематографе, телевидении, компьютерной графике и т. д.} близкий к 60, приемлемый для применения в цифровой технике. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Снятие нагрузки с процессора}] @Override protected void paintComponent(Graphics g) { super.paintComponent(g); controller.onDrawFrame(); try { Thread.sleep(16); } catch (InterruptedException e) { throw new RuntimeException(e); } repaint(); } \end{lstlisting} В результате создан бесконечный цикл отрисовки, аналогичный циклу \code{do-while}, который сам себя заставляет крутиться с некоторой периодичностью и на каждой итерации сообщает контроллеру, что прошло около одной шестидесятой секунды. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Изменения в основном окне}] private MainWindow() { setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setBounds(POS_X, POS_Y, WINDOW_WIDTH, WINDOW_HEIGHT); setTitle("Circles"); MainCanvas canvas = new MainCanvas(this); add(canvas); setVisible(true); } \end{lstlisting} В код, вызывающий конструктор канвы, также требуется внести незначительные изменения. В канву необходимо передать ссылку на основное окно, на котором она находится, для этого используется ключевое слово \code{this}. \begin{frm} \info Такой способ применения ключевого слова \code{this} ещё пригодится в этом разделе. \end{frm} \subsubsection{Параметры отрисовки} Метод \code{onDrawFrame()} будет обновлять сцену и заставлять объекты на ней рисовать самих себя (р\'{е}ндерить сцену). Для обновления сцены, привязанного ко времени физического мира необходимо знать дельту времени, то есть период времени, прошедший с появления предыдущего кадра. \begin{frm} \excl Писать логику обновления, исходя из частоты кадра, или из того, что канва «спит» 16 миллисекунд -- очень сомнительная опора, потому что поток \textbf{гарантированно} ждёт 16 миллисекунд. При этом, сколько будут выполняться остальные действия -- неизвестно, так как отрисовка происходит не через фиксированные промежутки времени, а по очереди сообщений окна и под влиянием множества других факторов. \end{frm} Метод \code{onDrawFrame()} должен принимать от канвы ряд параметров и распределять их по методам обновления и рендеринга. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Параметры метода обновления}] public void onDrawFrame(MainCanvas canvas, Graphics g, float deltaTime) { update(canvas, deltaTime); render(canvas, g); } private void update(MainCanvas canvas, float deltaTime) { } private void render(MainCanvas canvas, Graphics g) { } \end{lstlisting} При вычислении дельты времени важно привести все единицы измерения к единому и привычному времени, например, к секундам. Скоростью в этом случае будет «пиксель в секунду» и из метода будет отдаваться время в секундах. При обращении к контроллеру передаётся также ссылка на текущий объект канвы и объект графики. Пока самое главное, что нужно понять об этих двух объектах -- канва считает время в физическом мире и постоянно перерисовывает себя, сообщая об этом факте основному окну, а основное окно на этот факт как-то реагирует\footnote{ООП вокруг этих событий тоже важно хорошо понимать -- объекты передают ссылки друг на друга и вызывают друг у друга методы.}. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вычисление дельты времени между кадрами канвы}] private long lastFrameTime; MainCanvas(MainWindow controller) { this.controller = controller; lastFrameTime = System.nanoTime(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); try { Thread.sleep(16); } catch (InterruptedException e) { throw new RuntimeException(e); } float deltaTime = (System.nanoTime() - lastFrameTime) * 0.000000001f; controller.onDrawFrame(this, g, deltaTime); lastFrameTime = System.nanoTime(); repaint(); } \end{lstlisting} Приложение будет рисовать какие-то объекты, и не важно, будут ли это кружки, квадратики, картинки, человечки или какие-то другие объекты. Важно, чтобы у программы было описано поведение этих объектов \footnote{\href{https://www.plantuml.com/plantuml/png/ZLBDJiCm3BxdASmD9FO2qLHDMXdj0SfXKYz8ABGU2qgTAlNMmxGzEsOTMmf2a4jY-_lvP6NMiGcyQPowuZKs2AqpbfIgdEawU91tQQdsFKodYrlervOnIM2dGCu4Ti9cM-rXPSauiCHGvezfKJw_P7VvVRRy6rVcwJ9JHzmXyhk-ERKyfS7qYs0QFFSsRMtuKWQa6eEK-pZttD3MT9BxwNlkGpG_YxJIuhblMaz8F1BpMqjlnUixLYnonK-v6F6Va1TxhSU-C76uuacq2AsZ3O0MdIC34XLZ1hCJEna5-XgEy4EQN_33KCta-lmO7m00 }{Исходный код PlantUML}}. \begin{figure}[H] \centering \fontsize{12}{1}\selectfont \includesvg[scale=1.01]{pics/jd-02-mains-01.svg} \caption{} \end{figure} \subsection{Рисуемые объекты} \subsubsection{Двумерный рисуемый объект (спрайт)} Класс \code{Sprite} описывает общие для всех рисуемых объектов в программе поведение и свойства. В графических фреймворках часто начало координат находится в верхнем левом или нижнем левом углу. Однако, очень часто, когда пишутся какие-то игры или другие приложения с использованием графики в качестве координат используется центр объекта. То есть необходимо условиться, что \code{X} и \code{Y} -- это центр любого визуального объекта на канве. И, следовательно, удобно хранить не длину с шириной, а половину длины и половину ширины. А границы объекта, соответственно, будут отдаваться через геттеры и сеттеры. Дополнительно следует указать спрайту, что он умеет обновляться и рендериться, а его наследники уже смогут самостоятельно решать, как именно они хотят это делать. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Абстрактный рисуемый объект -- спрайт}] package ru.gb.jdk.two.online; import java.awt.*; public abstract class Sprite { protected float x; protected float y; protected float halfWidth; protected float halfHeight; protected float getLeft() { return x - halfWidth; } protected void setLeft(float left) { x = left + halfWidth; } protected float getRight() { return x + halfWidth; } protected void setRight(float right) { x = right - halfWidth; } protected float getTop() { return y - halfHeight; } protected void setTop(float top) { y = top + halfHeight; } protected float getBottom() { return y + halfHeight; } protected void setBottom(float bottom) { y = bottom - halfHeight; } protected float getWidth() { return 2f * halfWidth; } protected float getHeight() { return 2f * halfHeight; } void update(MainCanvas canvas, float deltaTime) { } void render(MainCanvas canvas, Graphics g) { } } \end{lstlisting} \subsubsection{Конкретный рисуемый объект} Инстанцировать абстрактный класс нельзя, поэтому, нужно создать класс шарика, который будет перемещаться по экрану. В конструкторе шарику задаются случайные размеры с определённым разбросом. Чтобы не усложнять пример отдельным объектами, описывающими физику мира, непосредственно объекту шарика будут заданы скорости по осям \code{X} и \code{Y}, и цвет. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Рисуемый объект}] package ru.gb.jdk.two.online; import java.awt.*; import java.util.Random; public class Ball extends Sprite { private static Random rnd = new Random(); private final Color color; private float vX; private float vY; Ball() { halfHeight = 20 + (float) (Math.random() * 50f); halfWidth = halfHeight; color = new Color(rnd.nextInt()); vX = 100f + (float) (Math.random() * 200f); vY = 100f + (float) (Math.random() * 200f); } @Override void update(MainCanvas canvas, float deltaTime) { x += vX * deltaTime; y += vY * deltaTime; if (getLeft() < canvas.getLeft()) { setLeft(canvas.getLeft()); vX = -vX; } if (getRight() > canvas.getRight()) { setRight(canvas.getRight()); vX = -vX; } if (getTop() < canvas.getTop()) { setTop(canvas.getTop()); vY = -vY; } if (getBottom() > canvas.getBottom()) { setBottom(canvas.getBottom()); vY = -vY; } } @Override void render(MainCanvas canvas, Graphics g) { g.setColor(color); g.fillOval((int) getLeft(), (int) getTop(), (int) getWidth(), (int) getHeight()); } } \end{lstlisting} В классе шарика переопределяются методы обновления и рендеринга. Самый простой рендер -- объекту графики задаётся цвет текущего шарика и вызывается метод \code{fillOval()}, которому передаются левая и верхняя координаты, ширина и высота. Несмотря на то, что объекты содержат поля типа \code{float}, работа происходит с пиксельной системой координат, а значит необходимо переводить в целые числа\footnote{Такой способ, конечно же, не подходит для реальных проектов, там необходимо всё сразу переводить в «мировые координаты» (например принять центр экрана за 0, верхний-левый угол за -1 нижний-правый за 1, как это делается в OpenGL) чтобы рендерить экраны.}. В методе обновления к текущим координатам шарика прибавляется расстояние, которое должен был преодолеть шарик за то время пока канва спала и рендерилась. $$ball(x_{new}, y_{new}) = ball(x + vx * \delta t, y + vy * \delta t).$$ Дополнительно, обрабатываются отскоки от границ панели, то есть описаны четыре условия, что при достижении границы меняется направление вектора. В основном классе делается очень прямолинейно -- создаётся массив из спрайтов, способный удержать десять шариков. В методе обновления каждый шарик из массива необходимо попросить обновиться, а в методе рендеринга -- дать команду на отрисовку. \begin{frm} \info Реализация обновления и отрисовки остаётся самим объектам, то есть инкапсулируется в них. Только каждый объект сам по себе знает, как именно ему обновляться с течением времени, и как рисоваться, а основной экран управляет на более высоком уровне -- на какой канве, когда и что рисовать. \end{frm} В конструкторе добавляется простой цикл инициализирующий приложение десятью шариками. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Управление объектами приложения}] private final Sprite[] sprites = new Sprite[10]; private MainWindow() { // ... for (int i = 0; i < sprites.length; i++) { sprites[i] = new Ball(); } // ... } private void update(MainCanvas canvas, double deltaTime) { for (int i = 0; i < sprites.length; i++) { sprites[i].update(canvas, deltaTime); } } private void render(MainCanvas canvas, Graphics g) { for (int i = 0; i < sprites.length; i++) { sprites[i].render(canvas, g); } } \end{lstlisting} Напомню, что самое главное, что необходимо понять из этого приложения -- это взаимодействия и взаимовлияния объектов. Наследование, полиморфизм, инкапсуляция поведений и свойств. \subsection{Интерфейсы} \subsubsection{Пример без интерфейсов} \begin{frm} \quot Начать разговор об интерфейсах я решил с создания отдельного класса фона, но сразу столкнулся с необходимостью думать головой... \end{frm} На первый взгляд, логично было бы предположить, что фон -- это спрайт, имеющий прямоугольную форму и всегда рисующийся первым. Но, есть затруднения, связанные с таким подходом: при изменении размеров окна фон тоже желательно изменить в размерах, а это лишние слушатели и десятки строк кода, поэтому при отрисовке объекта фона гораздо проще будет дать команду канве на изменение фона. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Фон, как наследник спрайта и изменения в основном классе}] package ru.gb.jdk.two.online; import java.awt.*; public class Background extends Sprite { private float time; private static final float AMPLITUDE = 255f / 2f; private Color color; @Override public void update(MainCanvas canvas, float deltaTime) { time += deltaTime; int red = Math.round(AMPLITUDE + AMPLITUDE * (float) Math.sin(time * 2f)); int green = Math.round(AMPLITUDE + AMPLITUDE * (float) Math.sin(time * 3f)); int blue = Math.round(AMPLITUDE + AMPLITUDE * (float) Math.sin(time)); color = new Color(red, green, blue); } @Override public void render(MainCanvas canvas, Graphics g) { canvas.setBackground(color); } } // MainCanvas sprites[0] = new Background(); for (int i = 1; i < sprites.length; i++) { sprites[i] = new Ball(); } \end{lstlisting} Цвет фона меняется синусоидально по каждому из трёх компонентов цвета, поэтому изменение происходит плавно (рис. \hrf{pic:bkg-sine}). Для реализации фона от спрайта, фактически, нужно только поведение, а свойства не нужны. Но и отказываться от наследования не очень правильно, потому что тогда не получится фон единообразно в составе массива спрайтов обновлять. Эти факты напрямую намекают на унификацию поведения -- на интерфейс. \begin{figure}[H] \centering \scalebox{.49}{\input{pics/jd-02-bkg-sine.pgf}} \caption{График изменения значений компонентов цвета} \label{pic:bkg-sine} \end{figure} \subsubsection{Понятие интерфейса} \begin{frm} \info Механизм наследования очень удобен, но он имеет свои ограничения. В частности, в языке Java допустимо наследование только от одного класса, в отличие, например, от языка С++, где имеется множественное наследование. \end{frm} В языке Java проблему отсутствия множественного наследования частично позволяют решить интерфейсы. Интерфейсы определяют некоторый функционал, не имеющий конкретной реализации, который затем реализуют классы, применяющие эти интерфейсы. Один класс может применить к себе множество интерфейсов. Правильно говорить «реализовать интерфейс». Интерфейс можно очень примерно представить как очень абстрактный класс. Интерфейс -- это описание методов. \begin{frm} \info Интерфейс -- это описание способов взаимодействия с объектом. Интерфейсы определяют функционал, не имеющий конкретной реализации. \end{frm} Примером интерфейса в реальной жизни может быть интерфейс управления автомобилем, интерфейс взаимодействия с компьютером или интерфейс USB, так, компьютеру не важно, что именно находится по ту сторону провода -- флеш накопитель, веб-камера или мобильный телефон, а важно, что компьютер умеет работать с интерфейсом USB, отправлять туда байты или получать. Потоки ввода-вывода, которые были изучены -- это тоже своего рода интерфейс, соединяющий не важно какого отправителя, например, программный код и не важно какого получателя, например, файл. Интерфейсы объявляются также, как классы, и могут иметь очень похожую на класс структуру, то есть быть вложенным или внутренним. Чаще всего каждый отдельный интерфейс описывают в отдельном файле, также как класс, но используя ключевое слово \code{interface}. Ниже показаны примеры интерфейсов, человек и бык, в которых описаны методы «ходить» и «издавать звуки». Все методы во всех интерфейсах всегда публичные, и в классическом варианте (до Java 1.8) не имеют никакой реализации. Поскольку все методы всегда публичные, то этот модификатор принято не писать. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Примеры интерфейсов}] package ru.gb.jdk.two.online.samples; public interface Human { public void walk(); public void talk(); } package ru.gb.jdk.two.online.samples; public interface Bull { void walk(); void talk(); } \end{lstlisting} \subsubsection{Реализация интерфейса} Продолжая учебный пример: созданы классы мужчина и бык. Класс мужчины реализовывает интерфейс человека, а класс быка -- быка. \begin{frm} \info Для реализации интерфейса необходимо переопределить все его методы, либо сделать класс абстрактным. \end{frm} Множественного наследования нет, но существует возможность реализовать любое количество интерфейсов. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Классы, реализующие интерфейс}] package ru.gb.jdk.two.online.samples; public class Man implements Human { @Override public void walk() { System.out.println("Walks on two feet"); } @Override public void talk() { System.out.println("Talks meaningful words"); } } package ru.gb.jdk.two.online.samples; public class Ox implements Bull { @Override public void walk() { System.out.println("Walks on hooves"); } @Override public void talk() { System.out.println("MooOooOoooOOoo"); } } \end{lstlisting} Одним из самых удобных следствий применения интерфейсов является возможность объявлять не только классы и создавать объекты, но и создать идентификторы, которые ссылаются на объект, реализующий интерфейс. То есть, по идентификатору типа интерфейса могут лежать абсолютно не связанные между собой объекты, главное, чтобы они реализовывали интерфейс. При этом сохраняется возможность работать с методами интерфейса которые могут быть для разных классов по-разному реализованы. Это иная форма изученного ранее \textbf{полиморфизма}. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Интерфейсные переменные}] package ru.gb.jdk.two.online.samples; public class Main { public static void main(String[] args) { Man man0 = new Man(); //class Man Ox ox0 = new Ox(); // class Ox Human man1 = new Man(); // interface Human Bull ox2 = new Ox(); // interface Bull } } \end{lstlisting} Для демонстрации ещё одного способа применения интерфейсов, будет описан класс минотавра\footnote{мифический персонаж с телом человека и головой быка.}, реализовывающий интерфейсы человека и быка своим собственным способом, а именно, ходил на ногах человека, но не мычал, как бык, а загадывал загадки. \begin{frm} \info При использовании интерфейсов важно то, что классы не связаны между собой наследованием, а обращение к ним единообразно. \end{frm} Интересно то, что в программе таким образом появляется возможность обратиться к минотавру не только как к человеку, но и как к быку, то есть гипотетически, можно создать некоторого Тесея, управляющего большим количеством минотавров. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Реализация множества интерфейсов}] package ru.gb.jdk.two.online.samples; public class Main { private static class Minotaurus implements Human, Bull { @Override public void walk() { System.out.println("Walks on two legs"); } @Override public void talk() { System.out.println("Asks you a riddle"); } } public static void main(String[] args) { Bull minos0 = new Minotaurus(); Human minos1 = new Minotaurus(); Minotaurus minos = new Minotaurus(); Human man1 = new Man(); Bull ox2 = new Ox(); Bull[] allBulls = {ox2, minos0, minos}; Human[] allHumans = {man1, minos, minos1}; } } \end{lstlisting} Также важно, что в интерфейсах разрешено наследование. То есть, один интерфейс может наследоваться от другого интерфейса, соответственно, при реализации такого, наследующего интерфейса, необходимо переопределять не только методы интерфейса, но и методы всех его родителей, также, как если бы происходило переопределение методов абстрактного класса. \begin{frm} \excl Следует обратить особенное внимание на то, что в интерфейсах разрешено множественное наследование. \end{frm} \begin{figure}[H] \centering \fontsize{12}{1}\selectfont \includesvg[scale=1.01]{pics/jd-02-derived-interfaces.svg} \caption{Наследование интерфейсов} \end{figure} \subsubsection{Вопросы для самопроверки} \begin{enumerate} \item Программный интерфейс -- это: \begin{enumerate} \item окно приложения в ОС; \item реализация методов объекта; \item объявление методов, реализуемых в классах. \end{enumerate} \item Интерфейсы нужны для: \begin{enumerate} \item компенсации отсутствия множественного наследования; \item отделения API и реализации; \item оба варианта верны. \end{enumerate} \item Интерфейсы позволяют: \begin{enumerate} \item удобно создавать новые объекты, не связанные наследованием; \item единообразно обращаться к методам объектов, не связанных наследованием; \item полностью заменить наследование. \end{enumerate} \end{enumerate} \subsubsection{Применение интерфейса} В описанном ранее примере интерфейс помогает решить проблему единообразия поведения спрайтов и фона, при их различии в свойствах. То есть, сложилась ситуация в которой существует необходимость хранить в одном массиве объекты со схожим поведением, но наследовать их друг от друга не совсем логично. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Интерфейс обновляемого и рисуемого объекта}] package ru.gb.jdk.two.online; import java.awt.*; public interface Interactable { void update(MainCanvas canvas, float deltaTime); void render(MainCanvas canvas, Graphics g); } \end{lstlisting} В качестве решения описан интерфейс \code{Interactable}, содержащий методы обновления и рендеринга без реализации. \begin{frm} \excl Если описывать ещё более гибкое приложение, нужно создавать два интерфейса, \code{Updatable} и \code{Renderable}, чтобы иметь возможность отделить рисуемые объекты от обновляемых. \end{frm} В данном случае интерфейс описывает объекты, которые должны уметь рисоваться и обновляться. В спрайте и фоне интерфейс реализуется. При этом получается, то фон никак не связан со спрайтом, но при этом, оба умеют рисоваться и обновляться, благодаря интерфейсу. Далее, при смене массива спрайтов на массив интерактивностей приложение не сломается. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Реализация интерфейса объектами приложения}] public abstract class Sprite implements Interactable public abstract class Background implements Interactable \end{lstlisting} \subsubsection{Создание библиотечных классов} Если обратить внимание на развитие повествования по курсу, можно заметить, что сначала произошёл выход за пределы одного метода (методы, помимо \code{main}), потом за пределы одного класса (классы котиков, собачек, и т.д.), затем за пределы одного пакета (логическое разделение классов), за пределы программного кода (потоки ввода-вывода), а теперь код, который возможно использовать несколькими программами. \begin{figure}[H] \centering \fontsize{12}{1}\selectfont \includesvg[scale=1.01]{pics/jd-02-interfaces-use.svg} \caption{Применение интерфейсов для отделения единиц компиляции} \end{figure} У классов канвы и спрайта, а также у интерфейса, нет никакой специфики, они ничего не «знают» о том, какие объекты существуют в программе и как эти объекты взаимодействуют между собой. Эти классы и интерфейс применимы, по сути, где угодно, не только в этой конкретной программе с этими конкретными классами. С использованием таких общих фрагментов кода становится возможным достаточно быстро написать вторую игру: новый пакет, новый класс, скопированный код от основного окна шариков. А полностью копировать спрайты и интерфейсы не целесообразно. Сделав правильное дробление по пакетам становится очевидно, что существует общий библиотечный пакет, и какие-то приложения с конкретными реализациями. \begin{figure}[H] \begin{forest} for tree={ font=\ttfamily, grow'=0, child anchor=west, parent anchor=south, anchor=west, calign=first, edge path={ \noexpand\path [draw, \forestoption{edge}] (!u.south west) +(7.5pt,0) |- node[fill,inner sep=1.5pt] {} (.child anchor)\forestoption{edge label}; }, before typesetting nodes={ if n=1 {insert before={[,phantom]}} {} }, fit=band, before computing xy={l=20pt}, } [JDKit [src/ru.gb.jdk.two.online [bricks [Brick] [MainWindow] ] [circles [Background] [Ball] [MainWindow] ] [common [Interactable] [MainCanvas] [Sprite] ] ] [README.md] ] \end{forest} \end{figure} В общий пакет классы скопировались без проблем, шарики перенеслись с минимальными изменениями -- только публичные модификаторы понадобились. В главном окне с будущими летающими квадратиками создан аналогичный конструктор, размеры, положение. Но общей канвой воспользоваться невозможно. Это происходит, потому что канва может принимать в качестве параметра в конструкторе только основное окно из пакета кружочков. И далее канва уже у класса с кружочками вызывает метод \code{onDrawFrame()}. \begin{frm} \excl Привязка к классу ограничивает возможности. \end{frm} Решение на поверхности -- использование интерфейсов. Необходимо написать интерфейс, который может по смыслу называться, например, \code{CanvasRepaintListener}, который будет уметь ожидать от канвы вызов метода и реализовывать его. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Интерфейс слушателя событий канвы}] package ru.gb.jdk.two.online.common; import java.awt.*; public interface CanvasRepaintListener { void onDrawFrame(MainCanvas canvas, Graphics g, float deltaTime); } \end{lstlisting} Такой интерфейс логично создавать в общем пакете и переписать канву так, чтобы она принимала на вход не класс, а объект, реализующий интерфейс. Далее, оба слушателя реализуются через интерфейс. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование интерфейса на канве}] public class MainCanvas extends JPanel { private final CanvasRepaintListener controller; private long lastFrameTime; public MainCanvas(CanvasRepaintListener controller) { this.controller = controller; lastFrameTime = System.nanoTime(); } \end{lstlisting} Интерфейс может быть реализован классом, а окна приложений -- это тоже классы. Это позволяет не только наследоваться от классов фреймворка Swing, но и реализовывать интерфейсы, описанные внутри приложения. Следовательно, окно без изменений продолжает передавать себя в конструктор, а метод интерфейса уже реализован. Чтобы подчеркнуть, что это реализация интерфейса -- дописана аннотация \code{@Override}. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Интерфейс обновляемого и рисуемого объекта}] public class MainWindow extends JFrame implements CanvasRepaintListener { private MainWindow() { ... } @Override public void onDrawFrame(MainCanvas canvas, Graphics g, float deltaTime) { ... } \end{lstlisting} \subsubsection{Особенности интерфейсов} Интерфейсы были значительно переработаны в Java 1.8, было добавлено довольно много механизмов, об одном из которых нельзя не сказать. Реализация интерфейсов по умолчанию. Пример будет построен на основе тех интерфейсов, которые уже написаны -- человек и бык. Очевидно, что именно у этих интерфейсов возможны реализации по умолчанию, например, для действия «ходить»: человек ходит на двух ногах, а бык на четырёх копытах. Для описания реализации по умолчанию используется ключевое слово \code{default}. Если написать реализацию, но не использовать данное ключевое слово, произойдёт ошибка компиляции (или среда разработки укажет на ошибочность такой конструкции). \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Реализация по умолчанию}] package ru.gb.jdk.two.online.samples; public interface Human { default void walk() { System.out.println("Walks on two feet"); } public void talk(); } package ru.gb.jdk.two.online.samples; public interface Bull { void walk() { // compile time error System.out.println("Walks on four hooves"); } void talk(); } \end{lstlisting} Первое, и самое очевидное следствие использования реализации по умолчанию -- отсутствие необходимости переопределять все методы в классах, реализующих эти интерфейсы, что делает интерфейс, в свою очередь, чуть более похожим на класс. Реализованные по умолчанию интерфейсы могут задействовать созданные в этом интерфейсе поля, а наличие в интерфейсе полей делает его ещё более похожим на класс. Все поля в интерфейсах статические и неизменяемые, а если заменить публичный модификатор доступа на другой -- будет ошибка компиляции. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование полей в интерфейсах}] public interface Bull { public static final int amount = 2; default void walk() { System.out.println("Walks on " + amount + " hooves"); } void talk(); } \end{lstlisting} \subsection{Анонимные классы} \subsubsection{Понятие и применение} Программные интерфейсы открывают перед разработчиком широчайшие возможности по написанию более выразительного кода. Одна из наиболее часто используемых возможностей -- анонимные классы. Класс -- это новый тип данных для программы. Классы бывают вложенными и внутренними. Внутренние классы -- это классы, которые пишутся внутри других классов, которые в свою очередь описаны в файле. А также вложенные или локальные классы, которые возможно объявлять непосредственно в методах, и работать с ними, как с обычными классами. Анонимный класс, что довольно очевидно - это класс без названия. Далее приводится пример создания интерфейса \code{MouseListener} и описания в нём методов \code{mouseUp()}, \code{mouseDown()}. В основной части программы описан класс, реализующий этот интерфейс, то есть переопределяющий все его методы. Далее в методе \code{main()} создаётся экземпляр этого класса и появляется возможность использовать его методы. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Способ использования API через именованный класс}] public interface MouseListener { void mouseUp(); void mouseDown(); } private static class MouseAdapter implements MouseListener { @Override public void mouseUp() { } @Override public void mouseDown() { } } public static void main(String[] args) { MouseAdapter m = new MouseAdapter(); m.mouseDown(); m.mouseUp(); } \end{lstlisting} Очень часто, элементы управления (кнопки, события входящих датчиков (клавиатура, мышка), сеть требуют на вход каких-то обработчиков собственных данных, которые будут слушать конкретный источник данных, отлавливать события и знать что делать. Это делается через интерфейсы. С точки зрения программы, создаётся некий метод, например, метод добавления к кнопке слушателя. Далее, если в элемент управления в качестве слушателя передаётся какой-то объект, реализующий нужный интерфейс, то этот объект начнёт ловить события и как-то их обрабатывать. \begin{figure}[H] \centering \fontsize{12}{1}\selectfont \includesvg[scale=1.01]{pics/jd-02-action-listener.svg} \caption{Применение слушателей событий} \label{pic:action-listeners} \end{figure} Для полноценного примера (листинг \hrf{lst:anonymous-classes}) следует описать метод, принимающий на вход \code{MouseListener} (строка \hrf{line:method-with-listener}). И передать в метод объект (строка \hrf{line:listener-is-object}), предполагая, что внутри объекта для данного конкретного случая описано, как именно должна вести себя программа, когда кто-то нажал или отпустил кнопку мышки. Допустим, есть параметр метода \code{MouseListener m} и необходимость создать туда некоторый экземпляр, который реализует этот интерфейс. Но есть класс, который это делает: \code{MouseAdapter}. Проще будет создать экземпляр адаптера, но без идентификатора (строка \hrf{line:lstnr-anon-obj}). Часто такие классы создаются не просто без названия экземпляра, но и вовсе без имени, прямо в аргументе методов. Действительно, зачем классу имя, если он будет использован только один раз и только в этом методе для создания одного единственного объекта? Класс \code{MouseAdapter} идеально выполняет критерий \textbf{S} из принципов \textbf{SOLID} -- Single Responsibility, делает только одно полезное дело -- реализует интерфейс \code{MouseListener}. Но раз дело только одно -- можно его выполнять и без размышлений о названии класса. Для создания интерфейсной переменной есть немного необычный синтаксис, на \hrf{line:lstnr-as-identifier} строке. Получается что создаётся один экземпляр анонимного класса, который реализует интерфейс \code{MouseListener}. И созданный здесь же экземпляр данного класса кладётся в идентификатор. Возможно также не создавать интерфейсный идентификатор, а сразу передать реализующий экземпляр в аргумент метода. Получится, что в метод передаётся новый экземпляр анонимного класса который реализует интерфейс слушателя, и тут же даётся описание этого класса, в котором переопределяются соответствующие методы (строка \hrf{line:lstnr-made-inline}). Ещё раз: \textbf{анонимные классы} -- это классы, не имеющие названия и реализующие какой-то интерфейс. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Возможные способы создания обработчиков событий},label={lst:anonymous-classes}] private static void addMouseListener(MouseListener l) { <@\label{line:method-with-listener}@> l.mouseDown(); l.mouseUp(); } private static class MouseAdapter implements MouseListener { @Override public void mouseUp() { } @Override public void mouseDown() { } } public static void main(String[] args) { MouseAdapter m = new MouseAdapter(); addMouseListener(m); <@\label{line:listener-is-object}@> addMouseListener(new MouseAdapter()); <@\label{line:lstnr-anon-obj}@> MouseListener l = new MouseListener() { <@\label{line:lstnr-as-identifier}@> @Override public void mouseUp() { } @Override public void mouseDown() { } }; addMouseListener(l); addMouseListener(new MouseListener() { <@\label{line:lstnr-made-inline}@> @Override public void mouseUp() { } @Override public void mouseDown() { } }); \end{lstlisting} \subsubsection{Альтернативы} Существует возможность избежать использования анонимных классов и реализации сложных интерфейсов. Например, использовать адаптеры. Для панели на которой рисовались летающие шарики (\hrf{subsubsec:circles-canvas}) существует слушатель и метод добавления реализации слушателя мышки, очень похожий на созданный в учебных целях. \begin{figure}[H] \centering \includegraphics[width=12cm]{jd-02-mouse-listener.png} \caption{Метод добавления слушателя мышки} \end{figure} Если в аргументе этого метода начать писать \code{new}, среда разработки предложит реализовать интерфейс \code{MouseListener}, но также предложит ещё один вариант -- \code{MouseAdapter}. В исходниках класса \code{MouseAdapter} видно, что это класс, реализующий несколько интерфейсов, но только формально, то есть все реализации пустые. \begin{figure}[H] \centering \includegraphics[width=12cm]{jd-02-mouse-adapter.png} \caption{Содержимое класса \code{MouseAdapter}} \end{figure} Это позволяет переопределять не все, а только некоторые методы интерфейса, значительно экономя место в коде приложения, если, например, нужна реакция только на одно какое-то действие. \begin{figure}[H] \centering \includegraphics[width=12cm]{jd-02-adapter-impl.png} \caption{Частичная реализация интерфейса} \end{figure} \begin{frm} \quot Если попытаться это корректно перевести на русский язык, должно получиться: создай новый экземпляр анонимного класса, который наследуется от класса \code{MouseAdapter}, реализующего нужный интерфейс и переопредели этот конкретный метод. Остальные оставь пустыми, потому что остальные действия можно игнорировать. \end{frm} Как избежать таких «многоэтажных» и многострочных конструкций и при этом получить понятный код? Реализовать интерфейс в том классе, в котором в данный момент пишется код, и переопределить все методы интерфейса. В требуемые методы -- написать реализацию, а туда, где требуется объект, реализующий интерфейс -- передать ссылку \code{this}. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример реализации интерфейса «собой»}] public class MainWindow extends JFrame implements CanvasRepaintListener, MouseListener { MainWindow() { // ... canvas.addMouseListener(this); } @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { System.out.println("Clicked!"); } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } } \end{lstlisting} \subsubsection{SOLID} \label{subsubsec:solid} \begin{table}[H] \centering \begin{tabular}{|l|c|p{120mm}|} \hline Буква & Аббревиатура & Пояснение \\ [0.5ex] \hline S & SRP & Принцип единственной ответственности (single responsibility principle). Для каждого класса должно быть определено единственное назначение. Все ресурсы, необходимые для его осуществления, должны быть инкапсулированы в этот класс и подчинены только этой задаче. \\ \hline O & OCP & Принцип открытости/закрытости (open-closed principle). «Программные сущности ... должны быть открыты для расширения, но закрыты для модификации». \\ \hline L & LSP & Принцип подстановки Лисков (Liskov substitution principle). «Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа не зная об этом». \\ \hline I & ISP & Принцип разделения интерфейса (interface segregation principle). «Много интерфейсов, специально предназначенных для клиентов, лучше, чем один интерфейс общего назначения» \\ \hline D & DIP & Принцип инверсии зависимостей (dependency inversion principle). «Зависимость на Абстракциях. Нет зависимости на что-то конкретное» \\ \hline \end{tabular} \end{table} \subsubsection{Вопросы для самопроверки} \begin{enumerate} \item Программный интерфейс — это способ \begin{enumerate} \item рисования объектов; \item взаимодействия объектов; \item взаимодействия программы с пользователем. \end{enumerate} \item Анонимный класс — это класс без \begin{enumerate} \item интерфейса; \item объекта; \item имени. \end{enumerate} \item Поле в интерфейсе \begin{enumerate} \item невозможно; \item public static final; \item private final. \end{enumerate} \item Метод по-умолчанию \begin{enumerate} \item можно переопределять; \item можно не переопределять; \item можно использовать с полем интерфейса; \item все варианты верны. \end{enumerate} \end{enumerate} \subsection{Исключения в графических интерфейсах пользователя} Поскольку графический интерфейс пользователя - это всегда многопоточность, и привычного терминала под рукой чаще всего нет, то возникают особенности обработки исключений. Как ловить? Как показывать? \begin{figure}[H] \centering \includegraphics[width=12cm]{jd-02-except-not-seen.png} \caption{Исключение, которое никогда не увидят} \label{pic:unseen-exception} \end{figure} Например, есть окно, на котором есть кнопка. У кнопки есть обработчик, в котором что-то идёт не по плану, например, выход за пределы массива при подсчёте. Достаточно типичная ситуация. Возникает законный вопрос - как такое исключение поймать? \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример окна с исключением}] package ru.gb.jdk.two.online.samples; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class Exceptional extends JFrame implements ActionListener { private Exceptional() { setDefaultCloseOperation(EXIT_ON_CLOSE); setBounds(1100, 200, 500, 300); JButton btn = new JButton("Push me!"); btn.addActionListener(this); add(btn); setVisible(true); } public static void main(String[] args) { new Exceptional(); } @Override public void actionPerformed(ActionEvent e) { throw new ArrayIndexOutOfBoundsException("Bad thing happened!"); } } \end{lstlisting} Если бы такое же исключение было выброшено в консольном приложении, программа бы завершилась, здесь же видно, что, несмотря на исключение в консоли, окно всё ещё открыто и приложение работает. Ясно видно, что исключения не завершают приложение. Во-первых, следует применять правильный способ создания главных окон в Swing, эта конструкция уже не должна быть магической: у класса \code{SwingUtilities} есть статический метод, в который передаётся экземпляр анонимного класса, реализующего интерфейс, и в переопределяемом методе создаётся окно. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Верный способ создавать окна в Swing},label={lst:swing-utilities}] public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new Exceptional(); } }); } \end{lstlisting} Исключение происходит в специальном потоке EDT -- Event Dispathing Thread. Этот поток совершает диспетчеризацию всех событий, происходящих во фреймворке Swing и является генератором других потоков. Метод из листинга \hrf{lst:swing-utilities} явно создаёт \code{JFrame} именно под управлением EDT. Если внимательно изучить текст исключения внизу экрана на рисунке \hrf{pic:unseen-exception}, очевидно, что исключение возникло в потоке с названием \code{AWT-EventQueue-0}. Наличие у потока номера говорит о том, что таких очередей событий у приложения может быть много, и в каких-то из них могут возникать исключения. Исключение происходит в потоке. Обработчик исключений тоже содержится в потоке и называется \code{Thread.UncaughtExceptionHandler} и является интерфейсом. Интерфейс содержит один метод -- непойманное исключение, который принимает на вход поток, в котором произошло исключение и объект исключения, которое произошло. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример окна с исключением}] public class Exceptional extends JFrame implements ActionListener, Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { } } \end{lstlisting} Такие обработчики уже написаны и встроены в среду исполнения Java, но они только пишут в консоль стектрейс. Переопределив метод интерфейса в приложении -- появляется возможность реагировать на исключения более сложно. \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример окна с исключением},label={lst:gui-handle-except}] private Exceptional() { Thread.setDefaultUncaughtExceptionHandler(this); <@\label{line:set-thread-except-hanler}@> //... } @Override <@\label{line:thread-except-hanler}@> public void uncaughtException(Thread t, Throwable e) { JOptionPane.showMessageDialog( null, e.getMessage(), "Exception!", JOptionPane.ERROR_MESSAGE); } \end{lstlisting} В конструкторе (листинг \hrf{lst:gui-handle-except}), на строке \hrf{line:set-thread-except-hanler} для потока устанавливается обработчик исключений по-умолчанию, передаётся собственный объект окна. В самой функции обработки (строка \hrf{line:thread-except-hanler}), например, выводится на экран модальное окно с текстом исключения. Запустив приложение видно, что в консоли среды разработки нет сообщений об исключениях. Благодаря программным интерфейсам. \begin{figure}[H] \centering \includegraphics[width=12cm]{jd-02-except-handle.png} \caption{Результат обработки исключения} \end{figure} \subsection*{Практическое задание} \begin{enumerate} \item Полностью разобраться с кодом. \item Для приложения с шариками описать появление и убирание шариков по клику мышки левой и правой кнопкой соответственно. \item Написать, выбросить и обработать такое исключение, которое не позволит создавать более, чем 15 шариков. \item ** Написать ещё одно приложение, в котором на белом фоне будут перемещаться изображения формата png, лежащие в папке проекта. \end{enumerate} \newpage \printnomenclature[40mm] \end{document}