diff --git a/build/j-spec.pdf b/build/j-spec.pdf index 1d068d6..8a02f5a 100644 Binary files a/build/j-spec.pdf and b/build/j-spec.pdf differ diff --git a/j-spec.tex b/j-spec.tex index 84aebb4..24cf4e9 100644 --- a/j-spec.tex +++ b/j-spec.tex @@ -4,13 +4,18 @@ %\documentclass[a4paper,12pt]{report} % article is for single section compilation -\documentclass[a4paper,12pt]{article} +\documentclass[a4paper]{article} \usepackage[english,russian]{babel} \usepackage{ifthen} \input{settings/main-style-preamble} \input{settings/fancy-listings-preamble} +\titleformat{\chapter}[block]{\larger[4]\bfseries}{\thechapter.~}{.5em}{} +\titleformat{\section}[block]{\larger[3]\bfseries}{\thesection.~}{.5em}{} +\titleformat{\subsection}[block]{\larger[2]\bfseries}{\thesubsection.~}{.5em}{} +\titleformat{\subsubsection}[block]{\larger\bfseries}{\thesubsubsection.~}{.5em}{} + \graphicspath{{./pics/}} \title{Техническая специализация Java} \author{Иван Игоревич Овчинников} @@ -22,6 +27,7 @@ \usepackage{subfiles} \begin{document} +\fontsize{12}{12}\selectfont \maketitle \ifx\isSingle\undefined @@ -51,13 +57,17 @@ \ifx\isSingle\undefined \chapter{Java Development Kit} \fi -%\subfile{jtd6-11a} +\subfile{jtd1-06-abstract} +\newpage +\subfile{jtd2-07-abstract} +\newpage +\subfile{jtd3-08-abstract} +\newpage -\appendix +% \subfile{jtd6-11a} -\setcounter{secnumdepth}{5} \ifx\isSingle\undefined -\printnomenclature[40mm] + \appendix \chapter*{Семинары} \addcontentsline{toc}{chapter}{Семинары} @@ -72,5 +82,9 @@ \subfile{jtc4-04-workshop} \newpage \subfile{jtc5-05-workshop} +\newpage +\subfile{jtd1-06-workshop} + +\printnomenclature[40mm] \end{document} diff --git a/jtd1-06-abstract.tex b/jtd1-06-abstract.tex new file mode 100644 index 0000000..770e928 --- /dev/null +++ b/jtd1-06-abstract.tex @@ -0,0 +1,879 @@ +\documentclass[j-spec.tex]{subfiles} + +\begin{document} \sloppy +\setcounter{section}{5} +\setlength{\columnsep}{22pt} +\pagestyle{plain} +\tableofcontents + +\section{Инструментарий: GUI -- графический интерфейс пользователя} +\subsection*{В предыдущей главе} +\begin{itemize} +\item ООП; +\item процедурное программирование; +\item исключения; +\item устройство языка Java; +\item устройство платформы для создания и запуска приложений на JVM-языках; +\item базовые средства ввода-вывода; +\item базовые терминальные приложения; +\item алгоритмические задачи, не требующие сложных программных решений. +\end{itemize} + +\subsection*{В этом разделе} +Интерфейс пользователя -- это важно, поскольку это именно то, что видят пользователи в первую очередь. + +\begin{frm} \excl + Это самое сложное занятие начального этапа, но не потому что оно содержит очень сложную информацию, а потому что заставит под необычным углом взглянуть на те принципы ООП, которые уже известны. +\end{frm} + +Будут рассмотрены вопросы создания окон, менеджеров размещений, элементов графического интерфейса, и обработчиков событий. + +\begin{itemize} +\item \nom{Swing}{библиотека для создания графического интерфейса для программ на языке Java. Swing разработан компанией Sun Microsystems и содержит ряд графических компонентов (Swing widgets), таких как кнопки, поля ввода, таблицы и тому подобные.}; +\item \nom{Асинхронность}{вид программирования, позволяющий вынести выполняемые задачи отдельными блоками кода. Применяется в сервисах, где предыдущее действие тормозит следующее. Синхронный процесс выполняется поэтапно. Пользователь совершает действие, ждёт, пока программа обработает часть кода, переходит к другому блоку. При использовании асинхронности, код убирает операцию, блокирующую следующие действия.}; +\item \nom{Параллельность}{способ организации компьютерных вычислений, при котором программы разрабатываются, как набор взаимодействующих вычислительных процессов, работающих асинхронно и при этом одновременно. Параллельное программирование -- это техника программирования, которая использует преимущества многоядерных или многопроцессорных компьютеров и является подмножеством более широкого понятия многопоточности (multithreading).}; +\item \nom{Окно}{это прямоугольная область экрана, в котором приложение отображает информацию и получает реакцию от пользователя. Одновременно на экране может отображаться несколько окон, в том числе, окон других приложений, однако лишь одно из них может получать реакцию от пользователя -- активное окно. Пользователь использует клавиатуру, мышь и прочие устройства ввода для взаимодействия с приложением, которому принадлежит активное окно.}; +\item \nom{Компонент}{независимый модуль программы, предназначенный для повторного использования. Часто компоненты объединяют по общим признакам и организовывают в соответствии с определёнными правилами и ограничениями. Например, компоненты графического интерфейса.}; +\item \nom{Компоновщик}{Компоновщик (layout manager) в библиотеке Java Swing отвечает за расположение и организацию компонентов (например, кнопок, текстовых полей, панелей). Он определяет, как компоненты будут выравниваться, размещаться и изменяться при изменении размеров окна.}; +\item \nom{Панель}{Панель в библиотеке Java Swing представляет собой контейнер, который используется для группировки и организации других компонентов в пользовательском интерфейсе. Она представляет собой область с фиксированным размером на которую можно добавить другие компоненты, такие как кнопки, текстовые поля или изображения.}; +\item \nom{События}{это действия или случаи, возникающие в программируемой системе, о которых система сообщает для того, чтобы было возможно с ними взаимодействовать. Например, если пользователь нажимает кнопку на графическом интерфейсе, возможно ответить на это действие, отобразив информационное окно.}; +\item \nom{Обработчик}{это механизм, с помощью которого приложение может перехватить события, такие как сообщения, действия мыши и нажатия клавиш. Функция, которая перехватывает события определенного типа, называется процедурой-обработчиком. Процедура-обработчик может действовать для каждого получаемого события, а затем изменить или отменить событие.}. +\end{itemize} + +\subsection{Почему именно Swing?} +Вместо вступления следует кратко ответить на самый популярный вопрос. + +\begin{itemize} + \item[\faRemove] это популярный и современный фреймворк; + \item[\faRemove] пригодится любому программисту на Java; + \item[\faCheck] поможет лучше понять ООП; + \item[\faCheck] работа композиции из объектов; + \item[\faCheck] обмен информацией между объектами; + \item[\faCheck] явно использует ссылочную природу данных; + \item[\faCheck] улучшает запоминание базовых взаимосвязей объектов; + \item[\faCheck] без искусственных примеров (в результате будет разработана простая игра, крестики-нолики с графическим интерфейсом). +\end{itemize} + +\begin{frm} + \faQuestion~ Почему не JavaFX? Фреймворк был выведен из стандартной библиотеки языка, начиная с Java 9, и достаточно сложен для базового знакомства с графическими библиотеками. + + \faQuestion~ Почему не LibGDX? Фреймворк является надстройкой над Swing, объясняя его всё равно необходимо будет объяснять Swing/AWT. + + \faExclamation~ Intellij IDEA — написана на Swing. +\end{frm} + +\subsection{JFrame: Главный класс окна} +\subsubsection{Создание окна} +В этом разделе происходит изучение программирования и ООП, на примере и с использованием фреймворка Swing, поэтому необходимо сосредоточиться на объектах, их свойствах и взаимосвязях. + +\begin{frm} \info + Изучение фреймворка Swing -- не основная задача раздела, поэтому важно помнить, что не нужно зазубривать все названия всех компонентов. +\end{frm} + +Окно графического интерфейса, как и любая другая программа -- это класс и объекты. В листинге \hrf{lst:init-game-window} создаётся новый класс с названием \code{GameWindow}. Для начала, необходимо получить доступ к методам, содержащимся в библиотеке, для этого применяется наследование от класса \code{JFrame}, и создаётся конструктор. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Класс окна},label={lst:init-game-window}] +package ru.gb.jdk.one.online; +import javax.swing.*; +public class GameWindow extends JFrame { + GameWindow() { + //... + } +} +\end{lstlisting} + +В основном классе программы просто создаётся новый объект вновь описанного класса с окном. С точки зрения программирования и ООП не происходит ничего значительно отличающегося от котиков и пёсиков. Если запустить получившееся приложение, оно должно запуститься, и сразу завершиться, это признак верно написанного кода. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание нового окна}] +package ru.gb.jdk.one.online; +public class Main { + public static void main(String[] args) { + new GameWindow(); + } +} +\end{lstlisting} + +\subsubsection{Закрытие окна, завершение приложения} +Б\'{о}льшая часть свойств окна не меняется за всё время существования окна и задаётся в конструкторе, то есть окно при создании будет наделено какими-то свойствами, которые в некоторых случаях можно будет поменять во время работы приложения. Самое не очевидное на первый взгляд то, что в Swing при нажатии на крестик в углу окна программа не завершается. По умолчанию, все создаваемые окна -- невидимые. Это сделано потому что есть возможность создавать сколько угодно окон для приложения и такое поведение значительно снижает риск неожиданного поведения. + +\begin{frm} \excl + Все окна по умолчанию невидимые. Нажатие на крестик по умолчанию делает окно невидимым, а не завершает программу. +\end{frm} + +Для того, чтобы программа закрылась, необходимо принять решение, какое окно в ней будет главным. Чтобы программа завершалась при закрытии главного окна (обычно, это первое, что делается при создании одно оконных приложений) экземпляру JFrame устанавливается свойство \code{DefaultCloseOperation}. То есть устанавливается, что нужно сделать, когда это (главное) окно закроется. В это свойство записывается константа \code{EXIT_ON_CLOSE}. Если этого не сделать, то будет использовано поведение по умолчанию, окно сделается невидимым, и приложение не завершится. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Завершение приложения по закрытию окна}] + setDefaultCloseOperation(EXIT_ON_CLOSE); +\end{lstlisting} + +\subsubsection{Свойства окна} +Добавив констант\footnote{Считается хорошим тоном не держать в коде никаких «магических цифр», чтобы не думать, что они значат и почему они именно такие, и что будет если их поменять.} с шириной, высотой и положением окна по осям относительно рабочего стола, становится возможным настроить размеры и положение окна в конструкторе. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Базовые свойства окна}] +private static final int WINDOW_HEIGHT = 555; +private static final int WINDOW_WIDTH = 507; +private static final int WINDOW_POSX = 800; +private static final int WINDOW_POSY = 300; + +GameWindow() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + setLocation(WINDOW_POSX, WINDOW_POSY); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + + setVisible(true); +} +\end{lstlisting} + +Установка всех начальных свойств окна осуществляется вызовом соответствующих сеттеров в конструкторе. По умолчанию окно -- невидимое, поэтому для его демонстрации в конструкторе вызывается метод \code{setVisible} с передаваемым аргументом \code{true}. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-01-jframe-01.png} + \caption{Пустое окно указанного размера} +\end{figure} + +Окно -- это всегда \textit{отдельный поток программы}, внутри которого работает бесконечный цикл. В объекте окна существует очередь сообщений, которую цикл опрашивает и выполняет. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример сообщения, показывающего многопоточность}] +public static void main(String[] args) { + new GameWindow(); + System.out.println("Method main() is over"); +} +\end{lstlisting} + +Запустив программу и внимательно изучив результат, очевидно, что окно создалось, в консоли видно, что работа метода \code{main} закончилась, а окно всё равно выполняется, его, при желании, можно подвигать, изменить размер и так далее. Это и есть наглядная демонстрация \textit{многопоточности}. Таким образом, получается, что когда создаётся новое окно -- нет необходимости его ни в какой контейнер помещать, ни думать, как оно будет взаимодействовать с пользователем, оно создастся и будет жить своей жизнью. \textbf{Инкапсуляция}. Если появится необходимость что-то ещё выполнить в методе \code{main}, запрета на написание действий не существует и действия будут выполняться \textit{параллельно}, \textit{асинхронно}. + +\subsubsection{Вопросы для самопроверки} +\begin{enumerate} +\item Почему в классе \code{GameWindow} доступны методы фреймворка? + \begin{enumerate} + \item Из-за устройства фреймворка Swing; + \item из-за наследования от JFrame; + \item из-за импорта классов Swing. + \end{enumerate} +\item Чтобы создать пустое окно в программе нужно + \begin{enumerate} + \item импортировать библиотеку Swing; + \item создать класс MainWindow; + \item создать класс-наследник JFrame. + \end{enumerate} +\item Свойства окна, такие как размер и заголовок возможно задать + \begin{enumerate} + \item написанием методов в классе-наследнике; + \item вызовом методов в конструкторе; + \item созданием констант в первых строках класса. + \end{enumerate} +\end{enumerate} + +\subsection{Компоненты окна} +\subsubsection{Кнопка} +Для того, чтобы точно ничего не перепутать в процессе разработки, возможно придать окну больше индивидуальности, задав заголовок и запретив пользователю изменять его размеры, для игры в крестики-нолики это будет важно, чтобы красиво отображалось поле. Для этого вызовем методы \code{setTitle()} и \code{setResizeable()}, соответственно. + +\begin{frm} \info + Элементы графического интерфейса -- это хорошо знакомые кнопки, текстовые поля, надписи (лейблы), и тому подобные. +\end{frm} + +За кнопки отвечает класс \code{JButton}, при создании экземпляра есть возможность сразу в конструкторе задать надпись, которая будет отображаться на кнопке. Сразу создадим несколько кнопок, например, «выход». Кнопки недостаточно просто создать, поскольку неизвестно, где они должны находиться. Одну из созданных кнопок добавим на окно -- для этого внутри конструктора необходимо воспользоваться методом \code{add()}, который требует в качестве аргумента передать ему какой-то \code{Component}. Все кнопки, лейблы и прочие элементы интерфейса -- это наследники класса \code{Component}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Компонент «Кнопка»}] + JButton btnStart = new JButton("New Game"); + JButton btnExit = new JButton("Exit"); + + GameWindow() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + setLocation(WINDOW_POSX, WINDOW_POSY); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + setTitle("TicTacToe"); + setResizable(false); + add(btnStart); + + setVisible(true); + } +\end{lstlisting} + +После добавления в конструкторе кнопки, при запуске приложения видно, что она заняла всё окно приложения. Если убрать вызов метода \code{setResizeable()}, то также возможно удостовериться, что при изменении размеров окна, размер кнопки также будет меняться. + +\begin{figure}[H] + \centering + \begin{subfigure}[b]{0.47\textwidth} + \centering + \includegraphics[width=\textwidth]{jd-01-btn-01.png} + \caption{ } + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.47\textwidth} + \centering + \includegraphics[width=\textwidth]{jd-01-btn-02.png} + \caption{ } + \end{subfigure} + \caption{Изменение размеров кнопки в следствие изменения размеров окна} +\end{figure} + +При попытке добавить вторую кнопку на это же окно очевидно (рис. \hrf{pic:btn-overhead}), что вторая кнопка полностью перекрыла первую. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вторая кнопка}] + add(btnExit); +\end{lstlisting} + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-01-btn-03.png} + \caption{Перекрытие компонентов} + \label{pic:btn-overhead} +\end{figure} + +\subsubsection{Компоновщики (менеджеры размещений)} +Перекрытие происходит из-за использования компоновщика, или, как их ещё называют, менеджера размещений. + +\begin{frm} \info + Менеджеры размещений нужны для того, чтобы не думать каждый раз о том, как изменится размер и координаты конкретного элемента, допустим, при изменении окна и не писать сложное поведение вложенных компонентов чтобы просто отобразить то, что привычно пользователю. +\end{frm} + +Компоновщики активно используются в любом программировании графических интерфейсов в любых языках программирования, от C++ до JavaScript, потому что это достаточно удобный механизм, берущий на себя значительный пласт работы. Использование компоновщиков позволяет эффективно управлять и размещать компоненты в окне или панели пользовательского интерфейса, обеспечивая гибкость и адаптивность приложения к изменениям размеров и расположения компонентов на экране. + +Компоновщик -- это специальный объект, который помещается на некоторые (\code{RootPaneContainer}\footnote{\href{https://docs.oracle.com/javase/tutorial/uiswing/layout/index.html}{docs.oracle.com: Компоновщики}}) компоненты и осуществляет автоматическую расстановку добавляемых в него компонентов, согласно правилам. Компоновщиков существует несколько типов, каждый из которых предоставляет свои специфические возможности и алгоритмы расположения. Компоновщик выбирается в зависимости от требуемых задач и желаемого внешнего вида интерфейса. +\begin{itemize} + \item \code{BorderLayout} (по умолчанию); + \item \code{BoxLayout}; + \item \code{CardLayout}; + \item \code{FlowLayout}; + \item \code{GridBagLayout}; + \item \code{GridLayout}; + \item \code{GroupLayout}; + \item \code{SpringLayout}. +\end{itemize} + +По умолчанию в Swing используется компоновщик \code{BorderLayout} (рис. \hrf{pic:layout-border}). Он располагает всё, что ему передаётся в центре, но также у него есть ещё четыре положения, маленькие области по краям. + +\begin{figure}[H] + \centering + \fontsize{12}{1}\selectfont + \begin{subfigure}[b]{0.32\textwidth} + \centering + \includesvg[scale=1.01]{pics/jd-01-border-layout.svg} + \caption{\code{BorderLayout}} + \label{pic:layout-border} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.32\textwidth} + \centering + \includesvg[scale=1.01]{pics/jd-01-flow-layout.svg} + \caption{\code{FlowLayout}} + \label{pic:layout-flow} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.32\textwidth} + \centering + \includesvg[scale=1.01]{pics/jd-01-grid-layout.svg} + \caption{\code{GridLayout}} + \label{pic:layout-grid} + \end{subfigure} + \caption{Популярные менеджеры размещений} +\end{figure} + + +Если какая-то область не занята компонентом, она автоматически уменьшается до нулевых размеров, оставляя место другим компонентам. Поэтому, если необходимо какой-то компонент расположить не в центре, это нужно явно указать при добавлении. На первый взгляд, это немного не очевидно, поэтому лучше запомнить, что при добавлении надо указать ещё один параметр, константу, например, \code{BorderLayout.SOUTH}. \code{FlowLayout} будет располагать элементы друг за другом слева направо, сверху вниз. Компоновщик-сетка \code{GridLayout} при создании принимает на вход число строк и столбцов и располагает компоненты в получившейся сетке. + +\begin{frm} \info + Основная идея, которую надо понять, это не названия компоновщиков, а то, что в Swing вся работа происходит через компоновщики -- Layout, которые каждый по-своему располагают элементы в окне. +\end{frm} + +\subsubsection{Панель для размещения JPanel} +Разнообразие требований к разработке графических интерфейсов может привести к необходимости создания бесконечного числа компоновщиков. Поэтому разработчики библиотеки Swing придумали использовать не только компоненты сами по себе, но и группы элементов, которые располагаются на так называемых панелях (\code{JPanel}). Главная особенность панелей в том, что внутри каждой панели возможно использовать свой собственный компоновщик. \code{JPanel} -- это по умолчанию невидимый прямоугольник, на котором может находиться собственный компоновщик. Например, становится доступным создание для окна панели с кнопками, а остальное пространство оставить под другие важные вещи. В листинге \hrf{lst:jpanel-init} описан код создания панели, добавление её в нижнюю часть основного экрана, расположение внутри панели компоновщика и двух кнопок. Важно, что на экран добавляются не кнопки по отдельности, а компонент, на который предварительно добавили кнопки. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание и применение панели},label={lst:jpanel-init}] +GameWindow() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + setLocation(WINDOW_POSX,WINDOW_POSY); + setSize(WINDOW_WIDTH,WINDOW_HEIGHT); + setTitle("TicTacToe"); + setResizable(false); + + JPanel panBottom = new JPanel(new GridLayout(1, 2)); + panBottom.add(btnStart); + panBottom.add(btnExit); + add(panBottom, BorderLayout.SOUTH); + setVisible(true); +} +\end{lstlisting} + +\code{JPanel} позволяет также осуществлять рисование и взаимодействие с пользователем. Основные графические интерактивности в демонстрационном приложении будут сделаны именно на панели. Такую панель с достаточно большой функциональностью логично выделить в отдельный класс. В случае игры в крестики-нолики это будет карта поля сражения (листинг \hrf{lst:jpanel-map}). В описании конструктора для простоты панель перекрашивается в чёрный цвет (строка \hrf{line:map-background}), чтобы увидеть, что панель создаётся без ошибок. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание панели с полем боя},label={lst:jpanel-map}] + package ru.gb.jdk.one.online; + + import javax.swing.*; + import java.awt.*; + + public class Map extends JPanel { + Map() { + setBackground(Color.BLACK); <@\label{line:map-background}@> + } + } +\end{lstlisting} + +Естественно, панель также недостаточно просто создать (листинг \hrf{lst:jpanel-map-add}, строка \hrf{line:map-init}), но нужно её куда-то разместить. Например, на основной экран (строка \hrf{line:map-add}). Поскольку не была указана сторона экрана, панель заняла всё свободное место на окне, кроме юга, где расположилась панель с кнопками. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Добавление панели с полем боя},label={lst:jpanel-map-add}] +GameWindow() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + setLocation(WINDOW_POSX, WINDOW_POSY); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + setTitle("TicTacToe"); + setResizable(false); + + Map map = new Map(); <@\label{line:map-init}@> + + JPanel panBottom = new JPanel(new GridLayout(1, 2)); + panBottom.add(btnStart); + panBottom.add(btnExit); + add(panBottom, BorderLayout.SOUTH); + add(map); <@\label{line:map-add}@> + setVisible(true); +} +\end{lstlisting} + +\subsection{Многооконное приложение, взаимосвязи} +\subsubsection{Структура} +Полученных знаний достаточно, чтобы начать описывать так называемую бизнес-логику. Созданная панель \code{Map} будет выполнять функции поля боя, поэтому логично расположить в ней метод startNewGame(), начинающий новую игру. В качестве параметров метод должен принимать какие-то начальные настройки самой игры. Например, будут два режима игры, компьютер против игрока и игрок против игрока, размер поля, и сразу не будем привязываться к квадратному полю 3х3, для полей больше, чем 3х3 понадобится выигрышная длина, то есть число крестиков или ноликов, расположенных подряд на одной прямой для победы той или иной стороны. В теле метода сразу будет установлена так называемая заглушка, чтобы знать, что метод вызывается и все параметры передаются верно. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Метод начала новой игры},label={lst:map-start-new-game}] +void startNewGame(int mode, int fSzX, int fSzY, int wLen) { + System.out.printf("Mode: %d;\nSize: x=%d, y=%d;\nWin Length: %d", + mode, fSzX, fSzY, wLen); +} +\end{lstlisting} + +Если сразу описать архитектуру проекта, его будет проще наполнять логикой и расширять, чем если писать последовательно, удерживая общую картину в голове. Итоговое приложение приложение будет работать в двух окнах: первое -- стартовое, где будут задаваться настройки поля и производиться выбор режима игры; второе -- основное, где будет происходить собственно игра. Основное окно уже написано, и при его закрытии происходит выход из программы. Для создания второго окна необходимо написать ещё один класс, названный, например, \code{SettingsWindow}, наследник \code{JFrame}. Конструктор второго окна будет принимать экземпляр игрового окна. В первую очередь это сделано для передачи параметров игры, а во-вторых, чтобы красиво отцентрировать его относительно основного. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Заготовка окна настроек новой игры игры},label={lst:frame-settings}] +package ru.gb.jdk.one.online; + +import javax.swing.*; + +public class SettingsWindow extends JFrame { + SettingsWindow(GameWindow gameWindow) { + + } +} +\end{lstlisting} + +В основном окне \code{GameWindow} понадобится два поля, одно класса \code{SettingsWindow} чтобы иметь возможность экземпляр этого окна показывать когда появится необходимость и второе -- это панель \code{Map}. В основном окне при создании экземпляра окна настроек в него передаётся \code{this}. + +\begin{frm} \info + Обратите внимание, на этот способ применять this, когда неоюходимо передать в метод ссылку на объект, который вызывает этот метод, фактически, основное окно передаёт себя. +\end{frm} + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание окна настроек в основном окне},label={lst:frame-create-settings}] +Map map; +SettingsWindow settings; + +GameWindow() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + setLocation(WINDOW_POSX, WINDOW_POSY); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + setTitle("TicTacToe"); + setResizable(false); + + map = new Map(); + settings = new SettingsWindow(this); + // ... +} +\end{lstlisting} + +На рисунке \hrf{pic:ttt-class-diagram} первый черновик диаграммы классов разрабатываемого приложения\footnote{\href{https://www.plantuml.com/plantuml/uml/ZPBVQl904CNlzodckujV3GX5MbOhKhHggNyWGfHIojQiSMco2TbHIyK-UySqQJIbs5mCpEGt9pc7QHiK2Qx3WFt3bGmbn85Gch5588o1dWYbgxGNRQ7PlAl2TgLGjbgmOq2F3JlQHhNOmr9f4O3I2EvWr1cxp_tkeDUVmWtKw_Mpi3leJFi7jdPrbfsCdHcXrxNQNz0vePSP-W7tjsl4ICCBQkTW--Uu-wRowL3448guaRMEH5JQDraS9ciRB7jVH6LLs3uFCD_wFSIoikL_2ntf38NIj3qfRryKzZUHyY0apd8m8Rt79n29RogDOvMu959ujIgvqrGeFOkHt1viMM7aoIeidVTPMkSay23rbtBwPtQY_1NQn_V2GUbDz2eDj5WnvfmYXVyvJz_bdCe9aKSBaMsNmk7yj6TjgJqwtay0 }{Исходный код PlantUML}}. Создание идеальной диаграммы классов или модели данных не входит в цели курса, более важно понятное объяснение того, что в данный момент программируется. Буквами F обозначены экземпляры \code{JFrame}, буквой P \code{Jpanel}, а A это Application, то есть основной класс приложения. На диаграмме видно, что основное приложение создаёт основное окно, на которое добавлена панель \code{Map} и которое время от времени будет обращаться к \code{SettingsWindow} за настройками новой игры. + +\begin{figure}[H] + \centering + \fontsize{7}{1}\selectfont + \includesvg[scale=.6]{pics/jd-01-ttt-01.svg} + \caption{Диаграмма классов приложения} + \label{pic:ttt-class-diagram} +\end{figure} + +\subsubsection{Окно с настройками игры и обработчики кнопок} +Окно настроек игры на данный момент будет представлено одной кнопкой старта игры, вызывающей метод старта игры с одним зафиксированным набором настроек -- игра против компьютера, поле 3х3, чтобы выиграть необходимо собрать 3 крестика (или нолика) подряд. + +В данный момент окно создаётся в координатах (0,0) и имеет размер (0,0), то есть в левом верхнем углу экрана видно только кнопки свернуть, развернуть, закрыть. В конструкторе окна задаются его размеры и то, что его местоположение должно быть относительным главному окну. Аналогично основному окну добавлена кнопка подтверждения правильности настроек и старта игры. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Окно настроек новой игры},label={lst:frame-settings-basic}] +public class SettingsWindow extends JFrame { + private static final int WINDOW_HEIGHT = 230; + private static final int WINDOW_WIDTH = 350; + + JButton btnStart = new JButton("Start new game"); + SettingsWindow(GameWindow gameWindow) { + setLocationRelativeTo(gameWindow); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + + add(btnStart); + } +} +\end{lstlisting} + +Далее необходимо «оживить» кнопки на окнах, это делается специальной конструкцией, синтаксис которой пока что придётся запомнить. Синтаксически всё написанное уже понятно и оговорено -- у объекта кнопки \code{btnExit} вызывается метод добавления к этому объекту некоторого слушателя действия. Какое может у кнопки быть самое очевидное действие? Нажатие. В аргумент метода добавления передаётся некий новый объект класса «слушатель действия», у которого переопределяется метод «действие произошло». Кнопка старта игры будет делать видимым окно с будущими настройками. В листинге \hrf{lst:main-btn-lsnr} показаны обработчики кнопок старта новой игры и завершения приложения, находящихся на основном окне программы. Эти обработчики необходимо поместить внутрь конструктора основного окна. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчики нажатий на кнопки основного окна},label={lst:main-btn-lsnr}] +btnExit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } +}); + +btnStart.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + settings.setVisible(true); + } +}); +\end{lstlisting} + + + +\subsubsection{Последовательность выполнения программы} +В основном окне понадобится метод, инициализирующий новое игровое поле, поскольку прямой вызов по кнопке старта новой игры на окне настроек метода в панели основного окна противоречит инкапсуляции. + +Зачем мы так делаем, казалось бы усложняем? Но нет: панель находится на основном окне, а кнопка начала игры будет находиться на окне настроек, которое не может «знать», какие на основном окне есть панели. Или может оказаться, что дальше нет никакого интерфейса, а игра происходит по сети. + +\begin{frm} \excl + В этом и есть суть ООП, когда один объект максимально отделён от другого, и каждому из них вообще не важно, как реализован другой. +\end{frm} + +Соответственно, когда в окне настроек нажата кнопка «начать игру», обработчик вызывает метод главного окна, а главное окно в свою очередь уже знает, что оно разделено на панели, и вынуждает панель \code{Map} начинать (рисунок \hrf{pic:starting-flow})\footnote{\href{www.plantuml.com/plantuml/uml/ZL1TQy9047o_Nx5zKp1WAzAYLaIqQcbLY125GhcQqnmab-1jQgdOtzwzSjQFizWy16vsPdPdMXhv2lCaPbSOYKH05dEf69l7N6leyKG4KeNf6XgDXnAi8ucYsOGD0_eys90QvNmB2wbu358X18DXPnIylFQxWrv_0lTGhLOliuD1Pz8tvFBjPV9uv4-9UrSk_uix8sx5Sh_WiPqZfWhUKFackWjtF-GEVUOP93ohswSl4ALQQbk9jiywi_DzNOMYXVIHn8yEHsRzKAoDYi2jBVrqYrkySqbX-Rlud7aRrNWbj1RXuHhAHjvZrvi6XU8kydigm-DBapGK9LZudzEV_umCdeGY0Jdl2wZLtEJWULxpD5uDhjaHHDBpnvCypyZ0eEUOP7N3_XnwAxcCq3Ff75c5jOGAyoJ-1W00 }{Исходный код диаграммы в PlantUML}}. Для чего нужен промежуточный метод? Чтобы не делать лишних связей между классами. Это логично с точки зрения инкапсуляции. Одно окно не должно никак управлять панелью на другом окне. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчик кнопки старта новой игры}] +SettingsWindow(GameWindow gameWindow) { + setLocationRelativeTo(gameWindow); + setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + btnStart.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gameWindow.startNewGame(0, 3, 3, 3); + setVisible(false); + } + }); + add(btnStart); +} +\end{lstlisting} + +В окне настроек описан обработчик нажатия на единственную кнопку, из этого обработчика вызывается единственный доступный метод -- «старт новой игры» на основном окне. По факту нажатия, также, целесообразно спрятать окно настроек. Из метода основного класса \code{startNewGame()} вызывается \code{map.startNewGame()} класса мэп. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Цепочка вызовов методов старта новой игры}] +void startNewGame(int mode, int fSzX, int fSzY, int wLen) { + map.startNewGame(mode, fSzX, fSzY, wLen); +} +\end{lstlisting} + +Ещё раз цепочка вызовов (рис. \hrf{pic:starting-flow}): +\begin{enumerate} +\item основное окно делает окно настроек видимым; +\item окно настроек говорит основному, что пора начинать игру; +\item основное окно в свою очередь знает, как именно надо игру начинать и просит панель стартовать. +\end{enumerate} +Если всё сделано верно, в терминале появится вывод из заглушки на панели \code{Map}. + +\begin{verbatim} +Mode: 0; +Size: x=3, y=3; +Win Length: 3 +\end{verbatim} + +\begin{figure}[H] + \centering + \fontsize{8}{1}\selectfont + \includesvg[scale=.7]{pics/jd-01-ttt-invoke.svg} + \caption{Цепочка вызовов при старте новой игры} + \label{pic:starting-flow} +\end{figure} + +\subsubsection{Вопросы для самопроверки} +\begin{enumerate} +\item Менеджер размещений - это + \begin{enumerate} + \item сотрудник, занимающийся разработкой интерфейса; + \item объект, выполняющий расстановку компонентов на окне приложения; + \item механизм, проверяющий возможность отображения окна в ОС. + \end{enumerate} +\item Экземпляр JPanel позволяет + \begin{enumerate} + \item применять комбинации из компоновщиков + \item добавить к интерфейсу больше компонентов + \item создавать группы компонентов + \item всё вышеперечисленное + \end{enumerate} +\item Для выполнения кода по нажатию кнопки на интерфейсе нужно + \begin{enumerate} + \item создать обработчик кнопки и вписать код в него + \item переопределить метод нажатия у компонента кнопки + \item использовать специальный класс “слушателя” кнопок + \end{enumerate} +\end{enumerate} + +\subsection{Основная панель с игрой} +\subsubsection{Рисование} +Далее всё будет происходить на панели с полем для игры. Для рисования самой панели фреймворком определён метод \code{paintComponent()}. Этот метод вызывается фреймворком когда что-то происходит, например, когда основное окно перекрывается другим, перемещается на другой экран, или если его развернуть из свёрнутого состояния, вызывается он гораздо реже, чем это необходимо для логики игры. Для описания игрового процесса необходимо перерисовывать компонент по каждому клику мышкой и по каждому действию оппонента. + +\begin{frm} \excl + Важно помнить, что метод \code{paintComponent()} не следует напрямую вызывать из кода. Этот метод должен вызываться только фреймворком. Для того чтобы запросить у фреймворка вызов этого метода тоже есть специальный метод. +\end{frm} + +Для дальнейшей разработки важно отделить стандартный метод рисования компонента от пользовательского рисования на этом компоненте, так называемую бизнес-логику. Для этого будет создан ещё один метод \code{void render(Graphics g)}, который будет вызываться из переопределённого \code{paintComponent()}. из самого \code{paintComponent()} вызов метода родительского класса удалять не следует, поскольку там, скорее всего, происходит что-то важное. Для вызова же метода фреймворка, необходимо в нужный момент сказать фреймворку что требуется перерисовать панель, фреймворк поставит метод \code{paintComponent()} в очередь сообщений окна, и когда очередь дойдёт до выполнения этого метода -- окно выполнит перерисовку. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отделение бизнес-логики рисования}] +@Override +protected void paintComponent(Graphics g) { + super.paintComponent(g); + render(g); +} + +private void render(Graphics g) { } +\end{lstlisting} + + +\begin{frm} \excl + Это действие полностью асинхронно и \textit{косвенно} зависит от наших вызовов +\end{frm} + +Чтобы рисовать нужен объект класса \code{Graphics}, который умеет рисовать геометрические фигуры, линии, текст и тому подобное. Чтобы нарисовать поле для игры понадобится ширина и высота поля в пикселях. Их возможно узнать из свойств панели -- ширины и высоты. Всё, что связано с размерами лучше вынести в переменные объекта, поскольку они понадобятся в других методах. Помимо ширины и высоты понадобятся переменные, в которых будет храниться высота и ширина каждой ячейки. Размеры каждой ячейки пригодятся для создания отступа одной линии от другой. Далее циклически делаются отступы и рисуются горизонтальные и вертикальные линии. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Разлиновка поля для игры}] +private int panelWidth; +private int panelHeight; +private int cellHeight; +private int cellWidth; + +private void render(Graphics g) { + panelWidth = getWidth(); + panelHeight = getHeight(); + cellHeight = panelHeight / 3; + cellWidth = panelWidth / 3; + + g.setColor(Color.BLACK); + for (int h = 0; h < 3; h++) { + int y = h * cellHeight; + g.drawLine(0, y, panelWidth, y); + } + for (int w = 0; w < 3; w++) { + int x = w * cellHeight; + g.drawLine(x, 0, x, panelHeight); + } + repaint(); +} +\end{lstlisting} + +У многих разработчиков, в зависимости от используемой операционной системы после запуска этого кода не полностью или вовсе не рисуется разлиновка, это происходит из-за асинхронности рисования, скорее всего метод с линиями отработал позже того, как Swing нарисовал панель. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-01-ttt-lined-result.png} + \caption{Результат разлиновки поля для игры} +\end{figure} + +Чтобы всё увидеть, необходимо заставить компонент панели полностью перерисоваться. Это делается вызовом метода \code{repaint()} из метода старта новой игры. + +\subsubsection{Обработчик мышки} +Обработчик действий мышки очень похож на те обработчики, которые уже написаны. В конструкторе панели описывается метод добавления слушателя, в котором переопределяется метод \code{mouseReleased()}, то есть для приложения важно когда пользователь отпустит кнопку и аналогично методу отрисовки следует сразу отделить обработчик от основной исполняемой логики. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчик отпускания кнопки мышки}] +Map() { + addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + update(e); + } + }); +} +\end{lstlisting} + +Внутри метода обновления также принудительно вызывается метод перерисовки компонента, чтобы получился игровой цикл: старт -- отрисовка -- клик мыши -- отрисовка -- клик -- отрисовка... + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчик отпускания кнопки мышки}] +private void update(MouseEvent e) { + int cellX = e.getX() / cellWidth; + int cellY = e.getY() / cellHeight; + System.out.printf("x=%d, y=%d\n", cellX, cellY); + repaint(); +} +\end{lstlisting} + +В методе обновления из объекта \code{MouseEvent} получаются координаты клика, делятся на размер ячейки и тем самым получается номер ячейки, в которую произошёл клик. + +\subsubsection{Логика игры} +Поскольку лекция про графические интерфейсы и ООП, а все используемые конструкции примитивны, код логики игры будет приведён без подробных пояснений. + +Для работы понадобится генератор псевдослучайных чисел, символы, которыми будет обозначаться на поле игрок, компьютер и пустая ячейка, собственно поле и его размеры. Размеры -- на будущее. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Субъекты игры}] +private static final Random RANDOM = new Random(); +private final int HUMAN_DOT = 1; +private final int AI_DOT = 2; +private final int EMPTY_DOT = 0; +private int fieldSizeY = 3; +private int fieldSizeX = 3; +private char[][] field; +\end{lstlisting} + +Метод инициализации поля -- создаётся новый массив и заполняется пустыми символами. Его вызов логично сразу разместить в метод старта новой игры. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Инициализация карты}] +private void initMap() { + fieldSizeY = 3; + fieldSizeX = 3; + field = new char[fieldSizeY][fieldSizeX]; + for (int i = 0; i < fieldSizeY; i++) { + for (int j = 0; j < fieldSizeX; j++) { + field[i][j] = EMPTY_DOT; + } + } +} +\end{lstlisting} + +Когда кто-то (игрок или компьютер) будет совершать ход, будет важно, попал ли игрок в какую-то ячейку поля и пустота ли она, потому что нельзя ставить крестик поверх нолика и наоборот. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Попал ли игрок в пустую ячейку и в поле}] +private boolean isValidCell(int x, int y) { + return x >= 0 && x < fieldSizeX && y >= 0 && y < fieldSizeY; +} + +private boolean isEmptyCell(int x, int y) { + return field[y][x] == EMPTY_DOT; +} +\end{lstlisting} + +Компьютер будет очень примитивный -- он будет делать ход в случайные места на карте. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ход компьютера}] +private void aiTurn() { + int x, y; + do { + x = RANDOM.nextInt(fieldSizeX); + y = RANDOM.nextInt(fieldSizeY); + } while (!isEmptyCell(x, y)); + field[y][x] = AI_DOT; +} +\end{lstlisting} + +Очевидно, что учебные цели предполагают не только демонстрацию того, как надо делать, но также и демонстрацию того как делать не надо. Далее приведён код, который не следует допускать при работе над приложениями. Метод принимает на вход символ, который нужно проверить и проверяет - не победил ли он. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проверка победы одного из игроков}] +private boolean checkWin(char c) { + if (field[0][0]==c && field[0][1]==c && field[0][2]==c) return true; + if (field[1][0]==c && field[1][1]==c && field[1][2]==c) return true; + if (field[2][0]==c && field[2][1]==c && field[2][2]==c) return true; + + if (field[0][0]==c && field[1][0]==c && field[2][0]==c) return true; + if (field[0][1]==c && field[1][1]==c && field[2][1]==c) return true; + if (field[0][2]==c && field[1][2]==c && field[2][2]==c) return true; + + if (field[0][0]==c && field[1][1]==c && field[2][2]==c) return true; + if (field[0][2]==c && field[1][1]==c && field[2][0]==c) return true; + return false; +} +\end{lstlisting} + +\begin{frm} \excl + Всегда пишите с помощью циклов, потому что стоит захотеть изменить размер поля на 4х4 или 5х5 -- размер и сложность этого метода будет расти в геометрической прогрессии. +\end{frm} + +И метод проверки поля на состояние ничьей. Ничья в крестиках-ноликах наступает, когда не победил ни игрок ни оппонент, и не осталось пустых клеток. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проверка на ничью}] +private boolean isMapFull() { + for (int i = 0; i < fieldSizeY; i++) { + for (int j = 0; j < fieldSizeX; j++) { + if (field[i][j] == EMPTY_DOT) return false; + } + } + return true; +} +\end{lstlisting} + +Вся дальнейшая работа будет сконцентрирована на методах обновления игрового состояния и отрисовки игрового поля. В результате клика в ячейку необходимо проверить, валидная-ли ячейка, и можно-ли туда ходить. Если какое-то из условий не прошло, клик игнорируется, а если всё хорошо -- делается ход. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ход игрока}] +int cellX = e.getX()/cellWidth; +int cellY = e.getY()/cellHeight; +if (!isValidCell(cellX, cellY) || !isEmptyCell(cellX, cellY)) return; +field[cellY][cellX] = HUMAN_DOT; + +repaint(); +\end{lstlisting} + +В метод отрисовки также необходимо добавить логику. Если в ячейке поля ничего нет -- ничего делать не нужно, далее условие, если в ячейке крестик -- что-то сделаем, если нолик -- сделаем что-то другое и в противном случае выбросим исключение. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отрисовка поля}] +for (int y = 0; y < fieldSizeY; y++) { + for (int x = 0; x < fieldSizeX; x++) { + if (field[y][x] == EMPTY_DOT) continue; + + if (field[y][x] == HUMAN_DOT) { + g.setColor(Color.BLUE); + g.fillOval(x * cellWidth + DOT_PADDING, + y * cellHeight + DOT_PADDING, + cellWidth - DOT_PADDING * 2, + cellHeight - DOT_PADDING * 2); + } else if (field[y][x] == AI_DOT) { + g.setColor(new Color(0xff0000)); + g.fillOval(x * cellWidth + DOT_PADDING, + y * cellHeight + DOT_PADDING, + cellWidth - DOT_PADDING * 2, + cellHeight - DOT_PADDING * 2); + } else { + throw new RuntimeException("Unexpected value " + field[y][x] + + " in cell: x=" + x + " y=" + y); + } + } +} +\end{lstlisting} + +Далее -- непосредственно отрисовка. Сюда можно картинку вставлять, закрашивать квадраты, рисовать крестики и нолики. Для простоты будут рисоваться кружки. Методу объекта графики \code{g.fillOval()} в сигнатуре передаётся левая верхняя координата прямоугольника, в который затем будет вписан овал, его ширина и высота соответственно. Чтобы задать цвет -- перед тем как рисовать необходимо изменить цвет объекта графики \code{g.setColor(Color.BLUE)}. Для человека далее будут рисоваться синие кружки, а для компьютера красные. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-01-ttt-hum-turn.png} +\end{figure} + +\subsubsection{Последние приготовления} +По сути, осталось сделать две вещи -- описать так называемую бизнес-логику, то есть в правильном порядке вызвать методы с логикой игры, избавиться от исключений и вывести сообщение об окончании игры. Для того, чтобы вывести результат, поверх игрового поля будет выводиться сообщение. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Состояния игрового поля}] +private int gameOverType; +private static final int STATE_DRAW = 0; +private static final int STATE_WIN_HUMAN = 1; +private static final int STATE_WIN_AI = 2; + +private static final String MSG_WIN_HUMAN = "Победил игрок!"; +private static final String MSG_WIN_AI = "Победил компьютер!"; +private static final String MSG_DRAW = "Ничья!"; +\end{lstlisting} + +В методе обновления уточняется, что когда пользователь поставил точку, необходимо проверить состояние поля на наличие победы или ничьей, дать возможность компьютеру поставить точку и сделать тоже самое. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Логика обновления}] +// update +if (checkEndGame(HUMAN_DOT, STATE_WIN_HUMAN)) return; +aiTurn(); +repaint(); +if (checkEndGame(AI_DOT, STATE_WIN_AI)) return; +// end update + +private boolean checkEndGame(int dot, int gameOverType) { + if (checkWin(dot)) { + this.gameOverType = gameOverType; + repaint(); + return true; + } + if (isMapFull()) { + this.gameOverType = STATE_DRAW; + repaint(); + return true; + } + return false; +} +\end{lstlisting} + +В методе рендеринга, как только поле выведено и если игра закончилась необходимо вывести сообщение с одним из вариантов исхода игры. Для упрощения также следует завести классовую переменную с признаком окончания игры. Метод окончания игры рисует тёмно серый прямоугольник с жёлтой надписью о победе одного из игроков или ничьей в зависимости от состояния. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отрисовка сообщения об окончании игры}] +//render +if (isGameOver) showMessageGameOver(g); +// end render + +private void showMessageGameOver(Graphics g) { + g.setColor(Color.DARK_GRAY); + g.fillRect(0, 200, getWidth(), 70); + g.setColor(Color.YELLOW); + g.setFont(new Font("Times new roman", Font.BOLD, 48)); + switch (gameOverType) { + case STATE_DRAW: + g.drawString(MSG_DRAW, 180, getHeight() / 2); break; + case STATE_WIN_AI: + g.drawString(MSG_WIN_AI, 20, getHeight() / 2); break; + case STATE_WIN_HUMAN: + g.drawString(MSG_WIN_HUMAN, 70, getHeight() / 2); break; + default: + throw new RuntimeException("Unexpected gameOver state: " + gameOverType); + } +} +\end{lstlisting} + +Далее в листинге \hrf{lst:minor-additions} приведены несколько мелких правок в методах. Прочитав текст исключения при старте приложения становится ясно, что оно возникает, когда программа не может что-то поделить на ноль. Размеры поля до их инициализации равны нулю, поэтому понадобится ещё одна булева переменная -- инициализирована ли игра. + +\begin{itemize} +\item В конструкторе панели поле не инициализировано; +\item в методе обновления нет смысла обрабатывать клики по неинициализированному полю или полю на котором закончилась игра; +\item при старте новой игры игра перестаёт быть законченой, а поле становится инициализированным; +\item рендеринг на неинициализированном поле не имеет смысла; +\item в методе проверки на победу нужно добавить присвоение истинности булевой переменной с фактом окончания игры. +\end{itemize} + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Незначительные добавления в методах панели},label={lst:minor-additions}] +private boolean isGameOver; +private boolean isInitialized; + +Map() { + isInitialized = false; +} + +private void update(MouseEvent e) { + if (isGameOver || !isInitialized) return; +} + +void startNewGame(int mode, int fSzX, int fSzY, int wLen) { + isGameOver = false; + isInitialized = true; +} + +private void render(Graphics g) { + if (!isInitialized) return; +} + +private boolean checkEndGame(int dot, int gameOverType) { + if (checkWin(dot)) { + isGameOver = true; + } + if (isMapFull()) { + isGameOver = true; + } + return false; +} +\end{lstlisting} + +Результат запуска получившегося приложения представлен на рисунке \hrf{pic:ttt-results}. + +\begin{figure}[H] + \centering + \begin{subfigure}[b]{0.3\textwidth} + \centering + \includegraphics[width=\textwidth]{jd-01-ttt-rslt-cmp.png} + \caption{Победа компьютера} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.3\textwidth} + \centering + \includegraphics[width=\textwidth]{jd-01-ttt-rslt-hum.png} + \caption{Победа игрока} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.3\textwidth} + \centering + \includegraphics[width=\textwidth]{jd-01-ttt-rslt-drw.png} + \caption{Ничья} + \end{subfigure} + \caption{Результаты игры} + \label{pic:ttt-results} +\end{figure} + +\subsection*{Практическое задание} +\begin{enumerate} +\item Полностью разобраться с кодом. +\item Переделать проверку победы, чтобы она не была реализована просто набором условий. +\item Попробовать переписать логику проверки победы, чтобы она работала для поля 5х5 и количества фигур 4. +\item ** Доработать искусственный интеллект, чтобы он мог примитивно блокировать ходы игрока, и примитивно пытаться выиграть сам. +\end{enumerate} + +\newpage +\printnomenclature[40mm] + +\end{document} diff --git a/jtd1-06-workshop.tex b/jtd1-06-workshop.tex index c4f6b95..d5e52da 100644 --- a/jtd1-06-workshop.tex +++ b/jtd1-06-workshop.tex @@ -2,11 +2,10 @@ \usepackage{spreadtab} \begin{document} -%\setcounter{section}{4} \section{Семинар: Простейшие интерфейсы пользователя} \subsection{Инструментарий} \begin{itemize} -\item \href{http://}{Презентация} для преподавателя, ведущего семинар; +\item \href{https://docs.google.com/presentation/d/1ji8J8XfgjWyAmoIkQyHOyoWGsnAbD3Sk7-D7YBlaQIM}{Презентация} для преподавателя, ведущего семинар; \item \href{https://drive.google.com/file/d/1LWyE8aEy4-1gsognqhXIXwDcoLviVge4/view}{Фон} GeekBrains для проведения семинара в Zoom; \item JDK любая 11 версии и выше; \item \href{https://www.jetbrains.com/idea/download}{IntelliJ IDEA Community Edition} для практики и примеров используется IDEA. @@ -31,25 +30,25 @@ \hline @ Quiz & 5 & @ 6-18 & @ Преподаватель задаёт вопросы викторины, через 30 секунд демонстрирует слайд-подсказку и ожидает ответов (по минуте на ответ) \\ \hline - @ Рассмотрение ДЗ лекции & 10 & @ 19-27 & @ Преподаватель демонстрирует свой вариант решения домашнего задания с лекции, возможно, по предварительному опросу, демонстрирует и разбирает вариант решения одного из студентов \\ + @ Рассмотрение ДЗ лекции & 15-20 & @ 19-23 & @ Преподаватель демонстрирует свой вариант решения домашнего задания с лекции, возможно, по предварительному опросу, демонстрирует и разбирает вариант решения одного из студентов \\ \hline - @ Вопросы и ответы & 10 & @ 28 & @ Преподаватель ожидает вопросов по теме прошедшей лекции, викторины и продемонстрированной работы \\ + @ Вопросы и ответы & 10 & @ 24 & @ Преподаватель ожидает вопросов по теме прошедшей лекции, викторины и продемонстрированной работы \\ \hline - @ Задание 1 & 10 & @ 29-33 & @ \\ + @ Задание 1 & 15 & @ 25-30 & @ Создание и компоновка элементов управления \\ \hline - @ Задание 2 & 15 & @ 34-37 & @ \\ + @ Задание 2 & 15 & @ 31-35 & @ Связь компонентов и сбор сведений о состоянии компонента \\ \hline - @ Перерыв (если нужен) & 5 & @ 38 & @ Преподаватель предлагает студентам перерыв на 5 минут (студенты голосуют) \\ + @ Задание 3 & 10 & @ 36-38 & @ Передача данных с компонентов \\ \hline - @ Задание 3 & 10 & @ 39-42 & @ \\ + @ Перерыв (если нужен) & 5 & @ 39 & @ Преподаватель предлагает студентам перерыв на 5 минут (студенты голосуют) \\ \hline - @ Задание 4 & 15 & @ 43-46 & @ \\ + @ Задание 4 & 15 & @ 40-44 & @ Создание окна управления сервером \\ \hline - @ Задание 5 & 20 & @ 47-49 & @ \\ + @ Задание 5 & 30 & @ 45-48 & @ Создание окна клиентской части текстового чата \\ \hline - @ Домашнее задание & 5 & @ 50-51 & @ Объясните домашнее задание, подведите итоги урока \\ + @ Домашнее задание & 5 & @ 49-50 & @ Объясните домашнее задание, подведите итоги урока \\ \hline - @ Рефлексия & 10 tag(end) & @ 52-53 & @ Преподаватель запрашивает обратную связь \\ + @ Рефлексия & 10 tag(end) & @ 51-52 & @ Преподаватель запрашивает обратную связь \\ \hline @ Длительность & sum(cell(beg):cell(end)) & & \\ \hline @@ -488,7 +487,7 @@ Settings(GameWindow mainWindow) { \begin{lstlisting}[language=Java,style=JCodeStyle] public class Map extends JPanel { public static final int MODE_HVA = 0; - public static final int MODE_HVH = 0; + public static final int MODE_HVH = 1; //... } @@ -515,7 +514,7 @@ private void btnStartDelegate() { \subsubsection*{Задание 4} \begin{itemize} \item \textbf{Ценность этапа} Создание окна управления сервером -\item \textbf{Тайминг} 15-20 мин +\item \textbf{Тайминг} 10-15 мин \item \textbf{Действия преподавателя} \begin{itemize} \item Выдать задание студентам; @@ -670,7 +669,7 @@ private ServerWindow() { \subsubsection*{Задание 5} \begin{itemize} \item \textbf{Ценность этапа} Создание окна клиентской части текстового чата -\item \textbf{Тайминг} 25-30 мин +\item \textbf{Тайминг} 20-25 мин \item \textbf{Действия преподавателя} \begin{itemize} \item Выдать задание студентам; diff --git a/jtd2-07-abstract.tex b/jtd2-07-abstract.tex new file mode 100644 index 0000000..2d7dac5 --- /dev/null +++ b/jtd2-07-abstract.tex @@ -0,0 +1,1028 @@ +\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} diff --git a/jtd2-07-workshop.tex b/jtd2-07-workshop.tex new file mode 100644 index 0000000..580f97d --- /dev/null +++ b/jtd2-07-workshop.tex @@ -0,0 +1,600 @@ +\documentclass[j-spec.tex]{subfiles} +\usepackage{spreadtab} + +\begin{document} +\section{Семинар: Интерфейсы и API} +\subsection{Инструментарий} +\begin{itemize} +\item \href{https://docs.google.com/presentation/d/1OBDGtC5dJz7Tu9zEJDayETOKZL0aPXne3Y8VUOGZUUU/edit?usp=share_link}{Презентация} для преподавателя, ведущего семинар; +\item \href{https://drive.google.com/file/d/1LWyE8aEy4-1gsognqhXIXwDcoLviVge4/view}{Фон} GeekBrains для проведения семинара в Zoom; +\item JDK любая 11 версии и выше; +\item \href{https://www.jetbrains.com/idea/download}{IntelliJ IDEA Community Edition} для практики и примеров используется IDEA. +\end{itemize} + +\subsection{Цели семинара} +\begin{itemize} +\item Практика создания и проектирования интерфейсов; +\item передача и обработка сообщений с использованием интерфейсов; +\item Применение существующих интерфейсов для обработки исключений; +\item Отделение графического, сетевого и логического слоёв для приложения «Сетевой чат». +\end{itemize} + +\subsection{План-содержание} +\noindent +\begin{spreadtab}{{longtable}{|p{37mm}|l|l|p{90mm}|}} +\hline + @ Что происходит & @ Время & @ Слайды & @ Описание \\ +\hline +\endhead + @ Организационный момент & 5 tag(beg) & @ 1-5 & @ Преподаватель ожидает студентов, поддерживает активность и коммуникацию в чате, озвучивает цели и планы на семинар. Важно упомянуть, что выполнение домашних заданий с лекции является, фактически, подготовкой к семинару \\ +\hline + @ Quiz & 5 & @ 6-18 & @ Преподаватель задаёт вопросы викторины, через 30 секунд демонстрирует слайд-подсказку и ожидает ответов (по минуте на ответ) \\ +\hline + @ Рассмотрение ДЗ лекции & 10 & @ 19-24 & @ Преподаватель демонстрирует свой вариант решения домашнего задания с лекции, возможно, по предварительному опросу, демонстрирует и разбирает вариант решения одного из студентов \\ +\hline + @ Вопросы и ответы & 10 & @ 25 & @ Преподаватель ожидает вопросов по теме прошедшей лекции, викторины и продемонстрированной работы \\ +\hline + @ Задание 1 & 30 & @ 26-30 & @ Отделение бизнес-логики и графического интерфейса \\ +\hline + @ Перерыв (если нужен) & 5 & @ 31 & @ Преподаватель предлагает студентам перерыв на 5 минут (студенты голосуют) \\ +\hline + @ Задание 2 & 30 & @ 32-36 & @ Создание и частичная реализация интерфейсов для улучшения понимания механизмов взаимодействия объектов \\ +\hline + @ Задание 3 & 10 & @ 37-40 & @ Написать абстрактный пример применения интерфейсов с реализацией по умолчанию и наследование \\ +\hline + @ Домашнее задание & 5 & @ 41-42 & @ Объясните домашнее задание, подведите итоги урока \\ +\hline + @ Рефлексия & 10 tag(end) & @ 43-44 & @ Преподаватель запрашивает обратную связь \\ +\hline + @ Длительность & sum(cell(beg):cell(end)) & & \\ +\hline +\end{spreadtab} + +\subsection{Подробности} +\subsubsection*{Организационный момент} +\begin{itemize} +\item \textbf{Цель этапа:} Позитивно начать урок, создать комфортную среду для обучения. +\item \textbf{Тайминг:} 3-5 минут. +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Запрашивает активность от аудитории в чате; + \item Презентует цели курса и семинара; + \item Презентует краткий план семинара и что студент научится делать. + \end{itemize} +\end{itemize} + +\subsubsection*{Quiz} +\begin{itemize} +\item \textbf{Цель этапа:} Вовлечение аудитории в обратную связь. +\item \textbf{Тайминг:} 5--7 минут (4 вопроса, по минуте на ответ). +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Преподаватель задаёт вопросы викторины, представленные на слайдах презентации; + \item через 30 секунд демонстрирует слайд-подсказку и ожидает ответов. + \end{itemize} +\item \textbf{Вопросы и ответы:} + \begin{enumerate} + \item Множественное наследование в Java (3) + \begin{enumerate} + \item запрещено; + \item разрешено; + \item разрешено для интерфейсов. + \end{enumerate} + \item Интерфейсы позволяют (2) + \begin{enumerate} + \item удобно создавать новые объекты, не связанные наследованием; + \item единообразно обращаться к методам объектов, не связанных наследованием; + \item полностью заменить наследование. + \end{enumerate} + \item Поле в интерфейсе (2) + \begin{enumerate} + \item невозможно; + \item public static final; + \item private final. + \end{enumerate} + \item Обработчик исключений для графического потока (2) + \begin{enumerate} + \item это статический метод; + \item это интерфейс; + \item реализован JVM. + \end{enumerate} + \end{enumerate} +\end{itemize} + +\subsubsection*{Рассмотрение ДЗ} +\begin{itemize} +\item \textbf{Цель этапа:} Пояснить не очевидные моменты в формулировке ДЗ с лекции, синхронизировать прочитанный на лекции материал к началу семинара. +\item \textbf{Тайминг:} 15-20 минут. +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Преподаватель демонстрирует свой вариант решения домашнего задания из лекции; + \item возможно, по предварительному опросу, демонстрирует и разбирает вариант решения одного из студентов. + \end{itemize} +\item \textbf{Домашнее задание из лекции:} + \begin{itemize} + \item Полностью разобраться с кодом -- это задание не нужно обсуждать, возможно просто спросить, кто действительно сел, взял в руки карандаш и блокнот, и попытался разобраться с кодом с лекции. Похвалить тех, кто это действительно сделал. + \item Для приложения с шариками описать появление и убирание шариков по клику мышки левой и правой кнопкой соответственно + + \textbf{Вариант решения} + + На лекции был описан инициализатор приложения, заполняющий массив игровых объектов шариками. необходимо было описать слушатель мышки, добавляющий и убирающий шарики из массива + + \begin{lstlisting}[language=Java,style=JCodeStyle] +if (e.getButton() == MouseEvent.BUTTON1) { + interactables[objectsCount++] = new Ball(e.getX(), e.getY()); +} else if (e.getButton() == MouseEvent.BUTTON3) { + if (objectsCount == 1) return; + objectsCount--; +} + \end{lstlisting} + + Для обеспечения работоспособности данного кода необходимо добавить в мячик конструктор, принимающий начальные координаты + + \begin{lstlisting}[language=Java,style=JCodeStyle] +Ball(int x, int y) { + this(); + this.x = x; + this.y = y; +} + \end{lstlisting} + + \item Написать, выбросить и обработать такое исключение, которое не позволит создавать более, чем 15 шариков. + + \textbf{Вариант решения} + + Очевидно, что обработка исключений должна вызвать ряд проблем, поскольку тема обработки исключений при использовании графических интерфейсов рассматривается только на следующей лекции, но за попытку решить проблему следует похвалить студентов. + + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Собственно, исключение}] +public class BallsOverflowException extends RuntimeException { + BallsOverflowException() { + super("Balls overflow, 15 allowed!"); + } +} + \end{lstlisting} + + По очевидным причинам выбрасывать исключение там, где добавляются шарики -- не логично, поэтому необходимо выбрасывать или на апдейте или на рендере. + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Место выброса исключения}] +private void update(MainCanvas canvas, float deltaTime) { + for (int i = 0; i < objectsCount; i++) { + interactables[i].update(canvas, deltaTime); + } + if (objectsCount >= 15) + throw new BallsOverflowException(); +} + \end{lstlisting} + +Для обработки исключений в Swing необходимо установить обработчик исключений для потока. сделаем этим обработчиком «себя». + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработка исключения}] +private MainWindow() { //constructor + Thread.setDefaultUncaughtExceptionHandler(this); +} + +@Override +public void uncaughtException(Thread t, Throwable e) { + if (e.getClass().equals(BallsOverflowException.class)) { + System.out.println(e.getMessage()); + } +} + \end{lstlisting} + + \item ** Написать ещё одно приложение, в котором на белом фоне будут перемещаться изображения формата png, лежащие в папке проекта. + + \textbf{Вариант решения} + + Главным отличием приложения будет работа с изображением. Такова была идея примера -- тиражируемость приложения. То есть вообще весь код может быть взят из шариков или квадратиков, но в главном рисуемом объекте должен происходить не рендеринг примитива, а рендеринг картинки. + + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={}] + +public class Ball extends Sprite { + private Image img = null; + private float vX; + private float vY; + + Ball() { + halfHeight = 64; // logo size magic numbers + halfWidth = 62; + try { + img = ImageIO.read(new File("logo.png")); + } catch (IOException e) { + throw new RuntimeException(e); + } + vX = 100f + (float) (Math.random() * 200f); + vY = 100f + (float) (Math.random() * 200f); + } + + @Override + public void render(MainCanvas canvas, Graphics g) { + g.drawImage(img, (int) getLeft(), (int) getTop(), null); + } +} + + \end{lstlisting} + \end{itemize} +\end{itemize} + +\subsubsection*{Вопросы и ответы} +\begin{itemize} +\item \textbf{Ценность этапа} Вовлечение аудитории в обратную связь, пояснение неочевидных моментов в материале лекции и другой проделанной работе. +\item \textbf{Тайминг} 5-15 минут +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Преподаватель ожидает вопросов по теме прошедшей лекции, викторины и продемонстрированной работы; + \item Если преподаватель затрудняется с ответом, необходимо мягко предложить студенту ответить на его вопрос на следующем семинаре (и не забыть найти ответ на вопрос студента!); + \item Предложить и показать пути самостоятельного поиска студентом ответа на заданный вопрос; + \item Посоветовать литературу на тему заданного вопроса; + \item Дополнительно указать на то, что все сведения для выполнения домашнего задания, прохождения викторины и работы на семинаре были рассмотрены в методическом материале к этому или предыдущим урокам. + \end{itemize} +\end{itemize} + +\subsubsection*{Задание 1} +\begin{itemize} +\item \textbf{Ценность этапа} Отделение бизнес-логики и графического интерфейса. +\item \textbf{Тайминг} 25-30 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \item Пояснить студентам пользу и необходимость следования паттерну MVC, проговорить важность разделения логики и открывающиеся возможности при правильном проектировании. + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item На предыдущем семинаре было описано окно сервера приложения, содержащее две кнопки (старт и стоп) и текстовое поле журнала. Необходимо вынести логику работы сервера в класс \code{ChatServer}, а в обработчиках кнопок оставить только логику нажатия кнопки и журналирования сообщений от сервера. + +Для достижения цели необходимо описать интерфейс «слушатель сервера», с методом «получить сообщение», вызывать его с одной стороны, и реализовать с другой. + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] +//ChatServerListener.java +package ru.gb.jdk.two.sem; + +public interface ChatServerListener { + void onMessageReceived(String msg); +} + +//ChatServer.java + +package ru.gb.jdk.two.sem; + +public class ChatServer { + private boolean isServerWorking; + private final ChatServerListener listener; + + ChatServer(ChatServerListener listener) { + isServerWorking = false; + this.listener = listener; + } + + public void start() { + if (isServerWorking) { + listener.onMessageReceived("Server is already working"); + return; + } + listener.onMessageReceived("Server started"); + isServerWorking = true; + } + + public void stop() { + if (!isServerWorking) { + listener.onMessageReceived("Server is stopped"); + return; + } + listener.onMessageReceived("Server stopped"); + isServerWorking = false; + } +} + +//ServerWindow.java +//constructor +server = new ChatServer(this); + +btnStop.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + server.stop(); + } +}); + +btnStart.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + server.start(); + } + }); + +// method implementation +@Override +public void onMessageReceived(String msg) { + log.append(msg + "\n"); +} + +\end{lstlisting} + + \item [$*_1$] Отделить функционал логгирования сообщений сервера для простоты изменения способа оповещения пользователя. + + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] +@Override +public void onMessageReceived(String msg) { + putMessageToLog(msg); +} + +private void putMessageToLog(String msg) { + log.append(msg + "\n"); +} + \end{lstlisting} + + \end{itemize} +\end{itemize} + +\subsubsection*{Задание 2} +\begin{itemize} +\item \textbf{Ценность этапа} Создание и частичная реализация интерфейсов для улучшения понимания механизмов взаимодействия объектов. +\item \textbf{Тайминг} 25-30 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \item Если группа студентов справилась с заданием, а времени осталось более 5 минут, выдавать группе задания «со звёздочкой». + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item Создать интерфейсы \code{ServerSocketThreadListener} и \code{SocketThreadListener}, содержащие методы, соответствующие событиям сервера и клиента чата. Реализовать созданные интерфейсы простым логированием. Со стороны клиента -- только \code{SocketThreadListener}, со стороны сервера -- оба интерфейса. + + \textbf{Вариант решения} + +\begin{lstlisting}[language=Java,style=JCodeStyle] +package ru.gb.jdk.two.sem.network; + +import java.net.Socket; + +public interface SocketThreadListener { + void onSocketStart(Socket s); + void onSocketStop(); + + void onSocketReady(Socket socket); + void onReceiveString(Socket s, String msg); + + void onSocketException(Throwable e); +} + +package ru.gb.jdk.two.sem.network; + +import java.net.ServerSocket; +import java.net.Socket; + +public interface ServerSocketThreadListener { + void onServerStart(); + void onServerStop(); + void onServerSocketCreated(ServerSocket s); + void onServerSoTimeout(ServerSocket s); + void onSocketAccepted(ServerSocket s, Socket client); + void onServerException(Throwable e); +} +\end{lstlisting} + +\begin{lstlisting}[language=Java,style=JCodeStyle] + // Server + +/** + * Server socket thread methods + * */ +@Override +public void onServerStart() { listener.onMessageReceived("Server thread started"); } + +@Override +public void onServerStop() { listener.onMessageReceived("Server thread stopped"); } + +@Override +public void onServerSocketCreated(ServerSocket s) { listener.onMessageReceived("Server socket created"); } + +@Override +public void onServerSoTimeout(ServerSocket s) { + // listener.onMessageReceived("Accept timeout"); +} + +@Override +public void onSocketAccepted(ServerSocket s, Socket client) { listener.onMessageReceived("client connected"); } + +@Override +public void onServerException(Throwable e) { e.printStackTrace(); } + +/** + * Socket Thread listening + * */ +@Override +public synchronized void onSocketStart(Socket s) { listener.onMessageReceived("Client connected"); } + +@Override +public synchronized void onSocketStop() { listener.onMessageReceived("Client dropped"); } + +@Override +public synchronized void onSocketReady(Socket socket) { listener.onMessageReceived("Client is ready"); } + +@Override +public synchronized void onReceiveString(Socket s, String msg) { listener.onMessageReceived(msg); } + +@Override +public void onSocketException(Throwable e) { e.printStackTrace(); } + +// Client +@Override +public void onSocketStart(Socket s) { log.append("Started" + "\n"); } + +@Override +public void onSocketStop() { log.append("Stopped" + "\n"); } + +@Override +public void onSocketReady(Socket socket) { log.append("Ready" + "\n"); } + +@Override +public void onReceiveString(Socket s, String msg) { log.append(msg + "\n"); } + +@Override +public void onSocketException(Throwable e) { e.printStackTrace(); } +\end{lstlisting} + + \item [$*_1$] Создать классы -- \code{ServerSocketThread} и \code{SocketThread}, соответственно слушателям, то есть реализовать в классе конструкторы с возможностью сохранения ссылки на слушателей. Создать экземпляр \code{ServerSocketThread} из объекта \code{ChatServer}, а \code{SocketThread} из \code{ClientGUI}. На данном этапе, не важно, где именно будут создаваться эти объекты. + + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] +package ru.gb.jdk.two.sem.network; + +public class ServerSocketThread { + private ServerSocketThreadListener listener; + + public ServerSocketThread(ServerSocketThreadListener listener) { + this.listener = listener; + } +} + +package ru.gb.jdk.two.sem.network; + +public class SocketThread { + private final SocketThreadListener listener; + + public SocketThread(SocketThreadListener listener) { + this.listener = listener; + } +} + +// Server +private ServerSocketThread server; + + +public void start() { + if (isServerWorking) { + listener.onMessageReceived("Server is already working"); + return; + } + server = new ServerSocketThread(this); + listener.onMessageReceived("Server started"); + isServerWorking = true; +} + +//Client +private void connect() { + SocketThread socketThread = new SocketThread(this); +} +\end{lstlisting} + + \end{itemize} +\end{itemize} + +\subsubsection*{Задание 3} +\begin{itemize} +\item \textbf{Ценность этапа} Написать абстрактный пример применения интерфейсов с реализацией по умолчанию и наследованием. +\item \textbf{Тайминг} 10-15 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item Описать команду разработчиков. В команде разработчиков могут находиться бэкендеры, которые в состоянии писать серверный код, фронтендеры, которые могут программировать экранные формы, и фуллстэк разработчики, совмещающие в себе обе компетенции. Реализовать класс фулстэк разработчика, создать экземпляр и последовательно вызвать все его методы. + + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] +package ru.gb.jdk.two.sem.devs; + +public interface Backender { void developServer(); } + +public interface Frontender { void developGUI(); } + +public interface Fullstack extends Backender, Frontender { +} + +public class FullstackDeveloper implements Fullstack { + @Override + public void developServer() { System.out.println("Server done"); } + + @Override + public void developGUI() { System.out.println("GUI done"); } +} + +public class Main { + public static void main(String[] args) { + FullstackDeveloper dev = new FullstackDeveloper(); + dev.developGUI(); + dev.developServer(); + } +} +\end{lstlisting} + + \item [$*_1$] Добавить возможность при создании экземпляров классов любых интерфейсов -- добавлять их в один массив, например, \code{Developer[] team = {...};} + + \textbf{Вариант решения} + + К первому решению добавляется маркерный интерфейс «Разработчик». Очевидно, что нельзя для этого интерфейса вызвать более точные методы фуллстэк разработчика. + \begin{lstlisting}[language=Java,style=JCodeStyle] +package ru.gb.jdk.two.sem.devs; + +public interface Developer { +} + +public interface Backender extends Developer { void developServer(); } + +public interface Frontender extends Developer { void developGUI(); } + +public class Main { + public static void main(String[] args) { + Developer dev = new FullstackDeveloper(); + } +} + +\end{lstlisting} + +\end{itemize} +\end{itemize} + +\subsubsection*{Домашнее задание} +\begin{itemize} +\item \textbf{Ценность этапа} Задать задание для самостоятельного выполнения между занятиями. В данном семинаре домашнее задание не предусмотрено в связи со сложностью заданий семинара. Необходимо досконально разобраться в написанном (и запланированном коде) и взаимосвязях объектов. Также возможно уточнить, что задания семинара направлены на написание многопоточного сетевого чата, но, поскольку это будет достаточно сложный проект, его написание приводится постепенно +\item \textbf{Тайминг} 5-10 минут. +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Пояснить студентам в каком виде выполнять и сдавать задания + \item Уточнить кто будет проверять работы (преподаватель или ревьювер) + \item Объяснить к кому обращаться за помощью и где искать подсказки + \item Объяснить где взять проект заготовки для дз + \end{itemize} +\item \textbf{Задания} + \begin{enumerate} + \item [5-25 мин] Выполнить все задания семинара, если они не были решены, без ограничений по времени; + + \textbf{Все варианты решения приведены в тексте семинара выше} + \item [5 мин] 1. Дописать третье задание таким образом, чтобы в идентификатор типа \code{Developer} записывался объект \code{Frontender}, и далее вызывался метод \code{developGUI()}, не изменяя существующие интерфейсы, только код основного класса. + + \begin{lstlisting}[language=Java,style=JCodeStyle] +public class Main { + public static void main(String[] args) { + Developer dev = new FrontendDeveloper(); + if (dev instanceof Frontender) { + ((Frontender) dev).developGUI(); + } + } +} + \end{lstlisting} + \end{enumerate} +\end{itemize} + +\subsubsection*{Рефлексия и завершение семинара} +\begin{itemize} +\item \textbf{Цель этапа:} Привести урок к логическому завершению, посмотреть что студентам удалось, что было сложно и над чем нужно еще поработать +\item \textbf{Тайминг:} 5-10 минут +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Запросить обратную связь от студентов. + \item Подчеркните то, чему студенты научились на занятии. + \item Дайте рекомендации по решению заданий, если в этом есть необходимость + \item Дайте краткую обратную связь студентам. + \item Поделитесь ощущением от семинара. + \item Поблагодарите за проделанную работу. + \end{itemize} +\end{itemize} + +\newpage +\end{document} diff --git a/jtd3-08-abstract.tex b/jtd3-08-abstract.tex new file mode 100644 index 0000000..7d9fd58 --- /dev/null +++ b/jtd3-08-abstract.tex @@ -0,0 +1,911 @@ +\documentclass[j-spec.tex]{subfiles} + +\begin{document} \sloppy +\setcounter{section}{7} +\setlength{\columnsep}{22pt} +\pagestyle{plain} +\tableofcontents + +\section{Инструментарий: Обобщения} +\subsection*{В предыдущем разделе} +\begin{itemize} +\item программные интерфейсы — понятие и принцип работы; +\item ключевое слово implements; +\item Наследование и множественное наследование интерфейсов; +\item реализация, реализации по-умолчанию; +\item частичная реализация интерфейсов, адаптеры; +\item анонимные классы. +\end{itemize} + +\subsection*{В этом разделе} +Смысл обобщений в программировании примерно такой же, как и в обычной разговорной речи -- отсутствие деталей реализации. С точки зрения проектирования обобщения -- это более сложный полиморфизм. Будет рассмотрены diamond operator, обобщённые методы и подстановочные символы (Wildcards). Применение обобщений для ограничений типов сверху и снизу. Выведение и стирание типов, целевые типы и загрязнение кучи. + +\begin{itemize} +\item \nom{Обобщения}{особый подход к описанию данных и алгоритмов, позволяющий работать с различными типами данных без изменения внешнего описания.}; +\item \nom{Wildcard}{в обобщённом коде знак вопроса, называемый подстановочным символом, означает неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа.}; +\item \nom{Diamond operation}{автоматизация подстановки типа, пустые угловые скобки, вынуждающие компилятор вывести тип из контекста.}; +\item \nom{Стирание типа}{подготовка написанного обобщённого кода к компиляции таким образом, чтобы в байт-коде не было обобщённых конструкций.}; +\item \nom{Выведение типа}{это возможность компилятора автоматически определять аргументы типа на основе контекста}; +\item \nom{Сырой (raw) тип}{это имя обобщённого класса или интерфейса без аргументов типа, то есть это, фактически, написание идентификатора и вызов конструктора обобщённого класса как обычного, без треугольных скобок.}; +\item \nom{Целевой тип}{это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение.}. +\end{itemize} + +\subsection{Понятие обобщения} +\subsubsection{Работа без обобщений} +Обобщения -- это механизм создания общего подхода к реализации одинаковых алгоритмов, но с разными данными. Обобщения -- это некоторые конструкции, позволяющие работать с данными не задумываясь о том, какие именно данные лежат внутри структуры или метода (строки, числа или коты). Обобщения позволяют единообразно работать с разными типами данных. + +\begin{frm} \info +Обобщённое программирование в Java позволяет создавать классы, интерфейсы и методы, которые могут работать с различными типами данных. В результате, код становится более универсальным и повторно используемым. +\end{frm} + +Для приведения примера поведения контейнера, который может хранить данные любого типа необходимо создать некоторый класс, который будет хранить внутри \code{Object} (любые данные Java). На примере чисел, в основном методе приложения созданы два экземпляра коробок с числами. Числа, которые сохранены в коробке желательно иметь возможность не только хранить, но и использовать, например, производить операцию сложения. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Контейнер, хранящий любые данные}] +private static class Box { + private Object obj; + public Box(Object obj) { + this.obj = obj; + } + + public Object getObj() { + return obj; + } + public void setObj(Object obj) { + this.obj = obj; + } + public void printInfo() { + System.out.printf("Box (%s): %s\n", + obj.getClass().getSimpleName(), + obj.toString()); + } +} +public static void main(String[] args) { + Box b1 = new Box(20); + Box b2 = new Box(30); + System.out.println(b1.getObj() + b2.getObj()); +} +\end{lstlisting} + +Поскольку в коробке, с точки зрения языка, хранятся объекты, а оператор сложения для объектов не определён, следует применить операцию приведения типов. Средства языка не запрещают создать больше коробок, например, со строками, и будет производиться уже не складывание чисел, а конкатенация строк. + +Проблема такого способа в том, что при каждом получении данных из коробки необходимо делать приведение типов, чтобы указать какие именно в контейнере лежат данные. Данную проблему для программы возможно решить организационно, если, например, называть переменные в венгерской нотации\footnote{Венгерская нотация в программировании -- соглашение об именовании переменных, констант и прочих идентификаторов в коде программ. Тип переменной указывается строчной буквой перед именем переменной, например для типа \code{int} переменная может называться \code{iMyVariable}.}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Согласование типов}] +public static void main(String[] args) { + Box b1 = new Box(20); + Box b2 = new Box(30); + System.out.println((Integer) b1.getObj() + (Integer) b2.getObj()); + + Box b3 = new Box("Hello, "); + Box b4 = new Box("World!"); + System.out.println((String) b3.getObj() + (String) b4.getObj()); +} +\end{lstlisting} + +Вторая проблема в том, что данные в контейнере ничем не защищены. Java не запрещает менять данные внутри контейнера, поскольку для платформы это объект. Проблему возможно решить сделав проверку \code{instanceof} перед приведением типов. + +Третья проблема в том, что все проблемы подобного рода проявляют себя только во время исполнения приложения, то есть у конечного пользователя перед глазами, когда разработчик ничего исправить не может. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Изменение типа хранения во время исполнения}] +public static void main(String[] args) { + Box iBox1 = new Box(20); + Box iBox2 = new Box(30); + if (iBox1.getObj() instanceof Integer && iBox2.getObj() instanceof Integer) { + int sum = (Integer) iBox1.getObj() + (Integer) iBox2.getObj(); + System.out.println("sum = " + sum); + } else { + System.out.println("The contents of the boxes differ by type"); + } + iBox1.setObj("sdf"); // Java: "What can go wrong here? You can do it!" +} +\end{lstlisting} + +Таким образом, в языке Java возможно создавать классы, которые могут работать с любыми типами данных, но при любом обращении к таким классам и данным необходимо делать достаточно сложные проверки. + +\subsubsection{Создание обобщения} +\begin{itemize} +\item Java generics -- это механизм языка, который позволяет создавать обобщенные (шаблонизированные) типы и методы в Java (особый подход к описанию данных и алгоритмов, позволяющий работать с различными типами данных без изменения внешнего описания); +\item Java generics были добавлены в Java 1.5 и стали одной из наиболее важных новых функций в языке; +\item Java generics работают только со ссылочными типами данных; +\item Java generics предоставляют безопасность типов во время компиляции, что означает, что ошибки связанные с типами данных могут быть обнаружены на этапе компиляции, а не во время выполнения программы. +\end{itemize} + +Для описания обобщения в треугольных скобках пишется буква \code{}, чтобы обозначить Type, Тип. На этапе описания класса невозможно сказать, какого типа данные будут лежать в переменной во время исполнения (число, строка или кот). + +\begin{frm} \excl +Если написать \code{T} не в треугольных скобках при описании класса, то Java будет искать реально существующий класс, который она не видит. +\end{frm} + +Таким образом указывается, что это обобщение и тип будет задаваться при создании объекта. Естественно поменять его будет нельзя, потому что Java -- это язык сильной статической типизации. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание обобщённого контейнера}] +private static class GBox { + private T value; + + public GBox(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + public void setValue(T value) { + this.value = value; + } + public void showType() { + System.out.printf("Type is %s, with value %s\\n", + value.getClass().getName(), getValue()); + } +} + +public static void main(String[] args) { + GBox stringBox = new GBox<>("Hello!"); + stringBox.showType(); + GBox integerBox = new GBox<>(12); + integerBox.showType(); +} +\end{lstlisting} + +При вызове конструктора такого объекта, будет указано, что конструктор ожидает указанный ранее тип, а не \code{T}. При указании типа в левой части, этот тип подставляется во все места класса, где компилятор обнаружит \code{T}. При получении значений из \code{integerBox} и \code{stringBox} не требуется преобразование типов, \code{integerBox.getValue()} сразу возвращает \code{Integer}, а \code{stringBox.getValue()} -- \code{String}. + +Если объект создан как \code{Integer}, то становится невозможно записать в него строку. При попытке написать такую строку кода, получится ошибка на этапе компиляции, то есть обобщения отслеживают корректность используемых типов данных. + +По соглашению, переменные типа именуются одной буквой в верхнем регистре. Если обобщённых переменных более одной -- они пишутся через запятую. + +\begin{frm} \info + \begin{itemize} + \item [] \code{E} -- элемент (Element, Entity обширно используется Java Collections); + \item [] \code{K} -- Ключ; + \item [] \code{N} -- Число; + \item [] \code{T} -- Тип; + \item [] \code{V} -- Значение; + \item [] \code{S}, \code{U}, и т. п. — 2-й, 3-й, 4-й типы. + \end{itemize} +\end{frm} + +\subsubsection{Ограничения обобщений} +Обобщения накладывают на работу с собой некоторые ограничения. + +\begin{enumerate} +\item Невозможно внутри метода обобщённого класса создать экземпляр параметризующего класса \code{T}, потому что на этапе компиляции об этом классе ничего не известно. Это ограничение возможно обойти, используя паттерн проектирования \textit{абстрактная фабрика}. +\item Нельзя создавать внутри обобщения массив из обобщённого типа данных. Но всегда можно подать такой массив снаружи. +\item По причине отсутствия информации о параметризируещем классе, невозможно создать статическое поле типа. Конкретный тип для параметра \code{T} становится известен только при создании \textbf{объекта} обобщённого класса. +\item Нельзя создать исключение обобщённого типа. +\end{enumerate} + +\subsubsection{Работа с обобщёнными объектами} +При обращении к обобщённому классу необходимо заменить параметры типа на конкретные классы или интерфейсы, например строку, целое число или кота. + +\begin{frm} \excl + «Параметр типа» и «аргумент типа» -- это два разных понятия. Когда объявляется обобщённый тип \code{GBox}, то \code{T} является параметром типа, а когда происходит обращение к обобщённому типу, передается аргумент типа, например \code{Integer}. +\end{frm} + +Как и любое другое объявление переменной запись вида \code{GBox integerBox} сам по себе не создаёт экземпляр класса \code{GBox}. Такой код объявляет идентифиактор типа \code{GBox}, но сразу уточняет, что это будет коробка с целыми числами. Такой идентификатор обычно называется \textbf{параметризованным типом}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Способы создания обобщённых идентификаторов и объектов}] +GBox integerBox0; +GBox integerBox1 = new GBox(1); +GBox integerBox2 = new GBox<>(1); +\end{lstlisting} + +Чтобы создать экземпляр класса, используется ключевое слово \code{new} и, в дополнение, указывается, что создаётся не просто \code{GBox}, а обобщённый, поэтому пишется \code{}. Компиляторы, начиная с Java 1.7, научились самостоятельно подставлять в треугольные скобки нужный тип (\textbf{выведение типа из контекста}). + +Если тип совпадает с аргументом типа в идентификаторе, в скобках экземпляра его можно не писать. Это называется \textbf{бриллиантовый оператор}. + +\subsection{Варианты обобщений} +\subsubsection{Множество параметризированных типов} +Ограничений на количество параметризированных типов не накладывается. Часто можно встретить обобщения с двумя типами, например, в коллекциях, хранящих пары ключ-значение. Также, нет ограничений на использование типов внутри угловых скобок. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Множество параметризированных типов}] +private static class KVBox { + private K key; + private V value; + + public KVBox(K key, V value) { + this.key = key; + this.value = value; + } + + public V getValue() { + return value; + } + public K getKey() { + return key; + } + public void showType() { + System.out.printf("Type of key is %s, key = %s, " + + "type of value is %s, value = %s\\n", + key.getClass().getName(), getKey(), + value.getClass().getName(), getValue()); + } +} + +public static void main(String[] args) { + KVBox kvb0 = new KVBox<>(1, "Hello"); + KVBox> kvb1 = new KVBox<>("World", new GBox<>("Java")); +} +\end{lstlisting} + +\subsubsection{Raw type (сырой тип)} +Сырой тип -- это имя обобщённого класса или интерфейса без аргументов типа, то есть это, фактически, написание идентификатора и вызов конструктора обобщённого класса как обычного, без треугольных скобок. При использовании сырых типов, программируется поведение, которое существовало до введения обобщений в Java. + +Геттеры сырых типов возвращают объекты. Это логично, потому что ни на одном из этапов не указан аргумент типа. + +\begin{frm} \excl + \code{GBox} -- это сырой тип обобщённого типа \code{GBox}. Однако необобщённый класс или интерфейс не являются сырыми типами. +\end{frm} + +Для совместимости со старым кодом допустимо присваивать параметризованный тип своему собственному сырому типу. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование сырых типов}] +GBox intBox = new GBox<>(1); +GBox box = intBox; + +GBox box = new GBox(1); +GBox intBox = box; + +GBox intBox0 = new GBox<>(1); +GBox box0 = intBox0; +box.setValue(4); +\end{lstlisting} + +Также, если присвоить параметризованному типу сырой тип, или если попытаться вызвать обобщённый метод в сыром типе, то буквально каждое слово в программе будет с предупреждением среды разработки. Предупреждения показывают, что сырой тип обходит проверку обобщённого типа, что откладывает обнаружение потенциальной ошибки на время выполнения программы. + +Предупреждения среды, а на самом деле, предупреждения компилятора, обычно имеют вид, представленный на рис \hrf{pic:ide-unchecked}. + +\begin{figure}[H] + \centering + \includegraphics[width=15cm]{jd-03-ide-unchecked.jpeg} + \caption{Предупреждение среды о непроверенном типе} + \label{pic:ide-unchecked} +\end{figure} + +Термин «unchecked» означает непроверенные, то есть компилятор не имеет достаточного количества информации для обеспечения безопасности типов. По умолчанию этот вид предупреждений выключен, поэтому компилятор в терминале на самом деле даёт подсказку. + +\begin{verbatim} +Note: .java uses unchecked or unsafe operations. +Note: Recompile with -Xlint:unchecked for details. +\end{verbatim} + +Чтобы увидеть все «unchecked» предупреждения нужно перекомпилировать код с опцией \code{-Xlint:unchecked}. + +\begin{figure}[H] + \centering + \includegraphics[width=15cm]{jd-03-lint-unchecked.jpeg} + \caption{Предупреждение компилятора о непроверенном типе} + % \label{pic:} +\end{figure} + +К предупреждениям среды в написанном коде желательно относиться внимательно, и придирчиво перепроверять код на наличие ненадёжных конструкций. + +\subsubsection{Вопросы для самопроверки} +\begin{enumerate} +\item Обобщения – это способ создания общих + \begin{enumerate} + \item классов для разных пакетов; + \item алгоритмов для разных типов данных; + \item библиотек для разных приложений. + \end{enumerate} +\item Что именно значит буква в угловых скобках? + \begin{enumerate} + \item Название обобщённого класса; + \item имя класса, с которым будет работать обобщение; + \item название параметра, используемого в обобщении. + \end{enumerate} +\item Возможно ли передать обобщённым аргументом примитивный тип? + \begin{enumerate} + \item Да; + \item нет; + \item только строку. + \end{enumerate} +\end{enumerate} + +\subsubsection{Обобщённые методы} +Обобщённые методы синтаксически схожи с обобщёнными классами, но параметры типа относятся к методу, а не к классу. Допустимо делать обобщёнными статические и не статические методы, а также конструкторы. + +Синтаксис обобщённого метода включает параметры типа внутри угловых скобок, которые указываются перед возвращаемым типом. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обобщённый метод}] +private static void setIfNull(GBox box, T t) { + if (box.getValue() == null) { + box.setValue(t); + } +} + +public static void main(String[] args) { + GBox box = new GBox<>(null); + setIfNull(box, 13); + System.out.println(box.getValue()); + GBox box0 = new GBox<>(1); + setIfNull(box0, 13); + System.out.println(box0.getValue()); +\end{lstlisting} + +\subsection{Ограниченные параметры типа} +Bounded type parameters позволяют ограничить типы данных, которые могут быть использованы в качестве параметров. + +Использование bounded type parameters в Java является хорошей практикой, которая позволяет более точно определить используемые типы данных и обеспечивает более безопасный и читаемый код. + +\subsubsection{Ограничение «сверху»} +В качестве примера необходимости ограничений будет использоваться уже написанная коробка. В коробке описывается операция сложения. Если сложение чисел и конкатенация строк -- это понятные операции, то сложение котиков или потоков ввода-вывода не определены. Применение \textbf{bounded type parameters} позволяет более точно задавать ограничения на используемые типы данных, что помогает писать более безопасный и читаемый код. То есть, в обобщениях существует возможность ограничиться только теми типами, которые соответствуют определенным требованиям + +Например, коробку предлагается сделать так, чтобы в ней хранились только числа -- наследники класса \code{Number}. Подобное ограничение делается с помощью ограниченного параметра типа (bounded type parameter). Чтобы объявить «ограниченный сверху» параметр типа необходимо после имени параметра указать ключевое слово \code{extends}, а затем указать верхнюю границу (upper bound), которой в данном примере является класс \code{Number}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ограничение параметра типа «сверху»}] +public class Box { + public V getValue() { + return value; + } + public void setValue(V value) { + this.value = value; + } + private V value; +} + +private static void setIfNull(BBox box, T t) { + if (box.getValue() == null) { + box.setValue(t); + } +} + +public static void main(String[] args) { + BBox integerBBox = new BBox<>(); + BBox stringBBox = new BBox<>(); + + setIfNull(integerBBox, 4); + setIfNull(stringBBox, "hello"); +} +\end{lstlisting} + +В рассматриваемом примере типы, которые можно использовать в параметризованных классах \code{Box}, ограничены наследниками класса \code{Number}. Если попытаться создать переменную с типом, например, \code{Box}, то возникнет ошибка компиляции. Аналогичным образом создаются обобщённые методы с ограничением. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Множественные ограничения}] +class Bird{} +interface Animal{} +interface Man{} +class CBox { + // ... +} +\end{lstlisting} + +Также возможно задать несколько границ. При этом, важно помнить, что также, как и в объявлении классов, возможен только один класс-наследник и он указывается первым. Все остальные границы могут быть только интерфейсами и указываются через знак амерсанда. + +\begin{frm} \info + В языке Java, несмотря на строгость типизации, возможно присвоить идентификатору одного типа объект другого типа, если эти типы \textbf{совместимы}. Например, можно присвоить объект типа \code{Integer} переменной типа \code{Object}, так как \code{Object} является одним из супертипов \code{Integer}. В объектно-ориентированной терминологии это называется связью «является» («is a»). +\end{frm} + +Предположим, что существует метод, описанный с generic параметром. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Generic параметр метода}] +private static void boxTest(GBox n) { /* ... */ } + +public static void main(String[] args) { + boxTest(new GBox(10)); + boxTest(new GBox(1)); // compile error + boxTest(new GBox(1.0f)); // compile error +\end{lstlisting} + +На первый взгляд кажется, что в метод возможно передать \code{Box} или \code{Box}, но нельзя, так как \code{Box} и \code{Box} не являются потомками Box. + +\begin{frm} \excl +Это частое недопонимание принципов работы обобщений, и это важно знать. Наследование не работает в Java generics так, как оно работает в обычной Java. +\end{frm} + +Идентификатор коробки с \code{Number} не может в себе хранить коробку с \code{Integer}. Обобщение защищает от попыток положить в коробку, например, строк -- не строку. То есть, предположим, в коробку кладётся \code{Integer}, как наследник \code{Number}, а затем, например \code{Float}, получится путаница. + +Из приведённого примера возможно сделать вывод о том, что если методу с таким параметром передать коробку с \code{Integer} -- он не будет работать. Чтобы допустить передачу таких контейнеров, в аргументе следует указать что в параметре возможен любой тип, являющийся наследником \code{Number}, то есть использовать маску \code{}. Ограничивать такую маску возможно как сверху так и снизу. Таким образом, обобщения защищают самих себя от путаницы и не дают складывать в одни и те же контейнеры разные типы данных. + +Поскольку коробку с чем то ещё, кроме \code{Number} и его наследников создавать нельзя, маск\'{и}рование при вызове метода будет избыточно + +На самом деле обобщения -- это так называемый синтаксический сахар. То есть, когда в коде используется обобщение, во время компиляции произойдёт так называемое «стирание», и все обобщённые типы данных преобразуются в \code{Object}, соответствующие проверки и приведения типов. + +\subsubsection{Ограничение «снизу»} +Ограничение типов возможно вводить как сверху, так и снизу. На примере обобщённых методов. Такие методы необходимы, когда требуется объединить несколько похожих, но всё же разных типов данных. Если подать на вход обобщённого метода два типа -- \code{Integer} и \code{Float} в итоге для работы будет выбран ближайший старший для них обоих -- \code{Number}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример обобщённого метода}] +private static boolean compare(T src, T dst) { + return src.equals(dst); +} + +public static void main(String[] args) { + System.out.println(compare(1, 1.0f)); + System.out.println(compare(1.0f, 1.0f)); + System.out.println(compare(1, 1)); +\end{lstlisting} + +Например, даны два списка -- один с \code{Integer} другой с \code{Number}, и требуется написать метод, который будет перекидывать числа из одного списка в другой. Для совершения этого действия описан метод, при работе которого возможны два сценария -- копировать элементы \code{Number} в список \code{Integer} или элементы \code{Integer} в список \code{Number}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Копирование чисел в списки}] +public static void copyTo(ArrayList src, ArrayList dst) { + for (Object o : src) dst.add(o); +} + +public static void main(String[] args) { + ArrayList ial = new ArrayList<>(Arrays.asList(1, 2, 3)); + ArrayList nal = new ArrayList<>(Arrays.asList(1f, 2, 3.0)); + System.out.println(ial); + System.out.println(nal); + copyTo(ial, nal); + System.out.println(nal); + copyTo(nal, ial); + System.out.println(ial); +\end{lstlisting} + +Правильным рабочим сценарием будет только второй -- копирование более точного типа в более общий список. Java при компиляции пропустит в работу оба сценария, но действительно работающим всё равно остаётся только один. + +Для примера, описаны два класса: «животное» и наследник животного -- «кот». На их основе создаются обобщённые списки и вызывается обобщённый метод копирования списков. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Копирование списков с «животными» и «котами»}] +public static void copyTo(ArrayList src, ArrayList dst) { + for (Object o : src) dst.add(o); +} + +private static class Animal { + protected String name; + protected Animal() { this.name = "Animal"; } + @Override public String toString() { return name; } +} +private static class Cat extends Animal { + protected Cat() { this.name = "Cat"; } +} + +public static void main(String[] args) { + ArrayList cats = new ArrayList<>(Arrays.asList(new Cat())); + ArrayList animals = new ArrayList<>(Arrays.asList(new Animal())); + copyTo(animals, cats); + System.out.println(cats); +\end{lstlisting} + +При компиляции и исполнении ошибок не возникло. К классу кота добавляется метод голос и совершается попытка вызвать голос у объекта из списка. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-lists-merged-problem.jpeg} + \caption{Проблема объединения списков} +\end{figure} + +При попытке вызвать у более общего «животного» метод более частного «кота» выбрасывается исключение о невозможности приведения типов. + +Для того, чтобы избежать подобных проблем, необходимо описать метод таким образом, чтобы он работал только с каким-то одним типом \code{} и списки будут \code{} и в цикле тоже будут перебираться элементы типа \code{T}. Если более не уточнять, получится, что в список котов возможно класть только котов, а в список животных класть только животных. Но кот -- это наследник животного, его присутствие в списке животных уместно. Получается, что источником может быть список из заданного типа или его наследников, а приёмником -- тип или его родители, и далее метод, виртуальная машина и другие механизмы сами разбираются, кто подходит под эти параметры. + +При таком описании метода, неверные варианты будут отсекаться на этапе компиляции. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={}] +public static void copyTo ( + ArrayList src, ArrayList dst) { + for (T o : src) { + dst.add(o); + } +} +\end{lstlisting} + +\subsection{Выведение типов} +Алгоритм выведения типов определяет типы аргументов, а также, если это применимо, тип, в который присваивается результат или в котором возвращается результат. + +\begin{frm} \info +Выведение типов -- это возможность компилятора автоматически определять аргументы типа на основе контекста. +\end{frm} + +Алгоритм работает от наиболее общего типа (\code{Object}) к наиболее точному, подходящему к данной ситуации. Например, добавив к коту реализацию интерфейса \code{Serializable} и написав метод, работающий с одним и только одним типом данных \code{T} будет создана ситуация, в которой никакие аргументы не связаны наследованием. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ситуация, в которой сработает выведение типов}] + private static class Cat extends Animal implements Serializable { + protected Cat() { this.name = "Cat"; } + public void voice(){ System.out.println("meow"); } + } + private static T pick(T first, T second) { return second; } + + public static void main(String[] args) { + Serializable se1 = pick("d", new Cat()); + Serializable se2 = pick("d", new ArrayList()); +\end{lstlisting} + +В таком методе выведение типов определяет, что вторые аргументы метода \code{pick}, а именно \code{Cat} и \code{ArrayList}, передаваемые в метод имеют тип \code{Serializable}, но этого недостаточно, потому что первый аргумент тоже должен быть того же типа. Удачно, что строка -- это тоже \code{Serializable}. + +В описании обобщённых методов, выведение типа делает возможным вызов обобщённого метода так, будто это обычный метод, без указания типа в угловых скобках. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Выведение типов в обобщённых методах},label={lst:generic-type-resolve}] +public class App { + public static void addBox(U u, List> boxes) { + Box box = new Box<>(); + box.setValue(u); + boxes.add(box); + } + + public static void main( String[] args ) { + ArrayList> catsInBoxes = new ArrayList<>(); + App.addBox(new Cat("Kusya"), catsInBoxes);<@\label{lst-line:with-type}@> + addBox(new Cat("Kusya"), catsInBoxes);<@\label{lst-line:no-type}@> + addBox(new Cat("Murka"), catsInBoxes); + printBoxes(catsInBoxes); + } +\end{lstlisting} + +Очевидно, что в листинге \hrf{lst:generic-type-resolve} обобщённый метод \code{addBox()} объявляет один параметр типа \code{U}. В большинстве случаев компилятор Java может вывести параметры типа вызова обобщённого метода, в результате чаще всего вовсе не обязательно их указывать. + +Чтобы вызвать обобщённый метод \code{addBox()}, возможно указать параметры типа (строка \hrf{lst-line:with-type}) либо опустить их, тогда компилятор языка автоматически выведет тип Cat из аргументов метода при вызове (строка \hrf{lst-line:no-type}). + +Выведение типа при создании экземпляра обобщённого класса позволяет заменить аргументы типа, необходимые для вызова конструктора обобщённого класса пустым множеством параметров типа (пустые треугольные скобки, бриллиантовая операция), так как компилятор может вывести аргументы типа из контекста. + +Очевидно, что конструкторы могут быть обобщёнными как в обобщённых, так и в необобщённых классах. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обобщённый конструктор}] +public class Box { + Box(U u){ + // ... + } + public T getValue() { + return value; + } + public void setValue(T value) { + this.value = value; + } + private T value; +} + +public static void main(String[] args) { + Box box = new Box("Some message"); +\end{lstlisting} + +Например, возможно явно указать, что у коробки будет обобщённый аргумент «кот», а у конструктора -- какой-то другой аргумент, например, строка или число. Компилятор выведет тип String для формального параметра U, так как фактически переданный аргумент является экземпляром класса String. + +\begin{frm} \excl +Алгоритм выведения типа использует только аргументы вызова, целевые типы и возможно очевидный ожидаемый возвращаемый тип для выведения типов. Алгоритм выведения не использует последующий код программы. +\end{frm} + +\subsection{Целевые типы} +Компилятор Java пользуется целевыми типами для вывода параметров типа вызова обобщённого метода. + +\begin{frm} \info +Целевой тип выражения -- это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение. +\end{frm} + +Например, как в методе \code{emptyBox()} (листинг \hrf{lst:target-types}, строка \hrf{lst-line:empty-box}). В основном методе инициализация ожидает экземпляр \code{Box}. Этот тип данных является целевым типом. Поскольку метод \code{emptyBox()} возвращает значение обобщённого типа \code{Box}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Целевые типы},label={lst:target-types}] +public class TBox { + public static final TBox EMPTY_BOX = new TBox<>(); + + public T getValue() { return value; } + + public void setValue(T value) { + this.value = value; + } + + private T value; + + static TBox emptyBox(){<@\label{lst-line:empty-box}@> + return (TBox) EMPTY_BOX; + } +} + +public static void main( String[] args ) { + TBox box = TBox.emptyBox(); +} +\end{lstlisting} + +\subsection{Вопросы для самопроверки} +\begin{enumerate} +\item Что из следующего является недопустимым? + \begin{enumerate} + \item \code{ArrayList al1 = new ArrayList();} + \item \code{ArrayList al2 = new ArrayList();} + \item \code{ArrayList al3 = new ArrayList();} + \item Всё допустимо. + \end{enumerate} +\item параметры метода \code{ArrayList src, ArrayList dst} вызов метода \code{copyTo(cats, animals);} Какой тип данных будет взят в качестве \code{Т}? + \begin{enumerate} + \item Animal; + \item Cat; + \item Object. + \end{enumerate} +\end{enumerate} + +\subsection{Подстановочный символ \code{} (wildcard)} +В обобщённом коде знак вопроса, называемый подстановочным символом, означает неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа. Подстановочный символ никогда не используется (рис \hrf{pic:wildcard-errors}) в качестве аргумента типа для вызова обобщённого метода, создания экземпляра обобщённого класса или супертипа. + +\begin{figure}[H] + \centering + \includegraphics[width=16cm]{jd-03-wildcard-error.jpeg} + \caption{Ошибки при работе с подстановочным символом} + \label{pic:wildcard-errors} +\end{figure} + +Передаваемые типы для уточнения, возможно \textbf{ограничивать} сверху и снизу. Ограниченный сверху подстановочный символ применяется, чтобы \textbf{ослабить ограничения} переменной. + +\begin{figure}[H] + \centering + \fontsize{12}{1}\selectfont + \includesvg[scale=1.01]{pics/jd-03-wildext.svg} +\end{figure} + +Чтобы написать метод, который работает с коробками, в которых содержится \code{Number} и дочерние от \code{Number} типы, например \code{Integer}, \code{Double} и другие, необходимо указать \code{Box}. + +\begin{figure}[H] + \centering + \fontsize{12}{1}\selectfont + \includesvg[scale=1.01]{pics/jd-03-nowild.svg} +\end{figure} + +Пример кода будет приводиться с классами животного и наследников. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Подготовка классов для демнстрации},label={}] +private static class Animal { + protected String name; + protected Animal(String name) { this.name = name; } + @Override public String toString() { + return this.getClass().getSimpleName() + " with name " + name; + } +} +private static class Cat extends Animal { + protected Cat(String name) { super(name); } +} +private static class Dog extends Animal { + protected Dog(String name) { super(name); } +} +\end{lstlisting} + +Внутри метода, выводящего информацию о содержимом коробки присутствует ограниченный сверху подстановочный символ \code{}, где вместо \code{Animal} может быть любой тип. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование ограниченного подстановочного символа},label={lst:wildcard-extended}] +public static class TBox { + public static final TBox EMPTY_BOX = new TBox<>(); + private T value; + + public T getValue() { return value; } + public void setValue(T value) { this.value = value; } + static TBox emptyBox() { + return (TBox) EMPTY_BOX; + } + @Override public String toString() { + return value.toString(); + } +} +static void printInfo(TBox animalInBox) {<@\label{lst-line:printinfo}@> + System.out.println("Information about animal: " + animalInBox); +} +\end{lstlisting} + +Создав соответствующие объекты, положив их в коробки и запустив код возможно наблюдать, что коробка вмещает как животных, так и наследников животного, чего не произошло бы при использовании в параметре типа животного без подстановочного символа. + +\begin{frm} \info +Если просто использовать подстановочный символ \code{}, то получится подстановочный символ без ограничений. \code{Box} означает коробку с неизвестным содержимым (неизвестным типом). +\end{frm} + +Такой синтаксис существует для того, чтобы продолжать использовать обобщённый тип без уточнения типа, как следствие, без использования обобщённой функциональности. Неограниченный подстановочный символ полезен, если нужен метод, который может быть реализован с помощью функциональности класса \code{Object}. Когда код использует методы обобщённого класса, которые не зависят от параметра типа. + +\begin{frm} \info + В программах, использующих Reflection API конструкция \code{Class} используется чаще других конструкций, потому что большинство методов объекта Класс не зависят от расположенного внутри типа. +\end{frm} + +Например, метод \code{printInfo()} (строка \hrf{lst-line:printinfo}) из листинга \hrf{lst:wildcard-extended}, не использует никаких методов животного, цель метода -- вывод в консоль информации об объекте в коробке любого типа, поэтому в параметре метода можно заменить «коробку с наследниками животного» на «коробку с чем угодно», ведь в методе будет использоваться только метод коробки \code{toString()}. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Исправление метода вывода информации на экран},label={}] +static void printInfo(TBox animalInBox) { + System.out.println("Information about animal: " + animalInBox); +} +\end{lstlisting} + +\begin{frm} \excl +Важно запомнить, что \code{Box} и \code{Box} -- это не одно и то же. +\end{frm} + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-wildcard-compile-error.jpeg} + \caption{Ошибка компиляции при использовании параметра типа \code{Object}} +\end{figure} + +Ограниченный снизу подстановочный символ ограничивает неизвестный тип так, чтобы он был либо указанным типом, либо одним из его предков. + +В обобщённых конструкциях возможно указать либо только верхнюю границу для подстановочного символа, либо только нижнюю. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-no-both-borders.jpeg} +\end{figure} + +То есть, если написать метод \code{printInfo()}, с параметром коробки и обобщённым аргументом не \code{}, а \code{}, токод также не будет работать, потому что метод будет ожидать не «животное и наследников», а «животное и родителей», то есть \code{Object}. + +\begin{figure}[H] + \centering + \fontsize{12}{1}\selectfont + \includesvg[scale=1.01]{pics/jd-03-wildsup.svg} +\end{figure} + +Обобщённые классы или интерфейсы связаны не только из-за связи между их типами. С обычными, необобщёнными классами, наследование работает по правилу подчинённых типов: класс \code{Cat} является подклассом класса \code{Animal}, и расширяет его. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Наследование необобщённых классов},label={}] +private static class Animal { + protected String name; + protected Animal(String name) { this.name = name; } + @Override public String toString() { + return this.getClass().getSimpleName() + " with name " + name; + } +} +private static class Cat extends Animal { + protected Cat(String name) { super(name); } +} + +public static void main(String[] args) { + Cat cat = new Cat("Vasya"); + Animal animal = cat; + + TBox catInBox = new TBox<>(); + TBox animalInBox = catInBox; // Incompatible types +\end{lstlisting} + +Данное правило не работает для обобщённых типов. Несмотря на то, что \code{Cat} является подтипом \code{Animal}, \code{Box} не является подтипом \code{Box}. Общим предком для \code{Box} и \code{Box} является \code{Box}. + +\begin{figure}[H] + \centering + \fontsize{12}{1}\selectfont + \includesvg[scale=1.01]{pics/jd-03-wildparent.svg} +\end{figure} + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Наследование в параметрах типа через подстановочнвй символ},label={}] +TBox catInBox = new TBox<>(); +TBox animalInBox = catInBox; // OK +\end{lstlisting} + +В некоторых случаях компилятор может вывести тип подстановочного символа. Коробка может быть определена как \code{Box}, но при вычислении выражения компилятор выведет конкретный тип из кода, такой сценарий называется \textbf{захватом подстановочного символа}. В большинстве случаев нет нужды беспокоиться о захвате подстановочного символа, кроме случаев, когда в сообщении об ошибке появляется фраза \code{capture of}. В примере, приведённом в листинге \hrf{lst:capture-error} компилятор обрабатывает параметр коробки как тип \code{Object}. Когда вызывается метод внутри \code{testError()}, компилятор не может подтвердить тип объекта, который будет присутствовать внутри коробки и генерирует ошибку. Обобщения были добавлены в Java именно для этого -- чтобы усилить безопасность типов на этапе компиляции. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ошибка захвата подстановочного символа},label={lst:capture-error}] +static void testError(Box box){ + box.setValue(box.getValue()); // capture of ? +} +\end{lstlisting} + +Когда есть уверенность в том, что операция безопасна, ошибку возможно исправить написав приватный вспомогательный метод (в англоязычной литературе private helper method), захватывающий подстановочный символ. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование вспомогательного метода},label={}] +private static void testErrorHelper(TBox box) { + box.setValue(box.getValue()); +} + +static void testError(TBox box) { + testErrorHelper(box); +} +\end{lstlisting} + +\subsubsection{Краткое руководство по использованию подстановочных символов} +\textbf{Входная переменная.} Предоставляет данные для кода. Для метода \code{copy(src, dst)} параметр \code{src} предоставляет данные для копирования, поэтому он считается входной переменной. + +\textbf{Выходная переменная.} Содержит данные для использования в другом месте. В примере \code{copy(src, dst)} параметр \code{dst} принимает данные и будет считаться выходной переменной. + +\begin{enumerate} +\item Входная переменная определяется с ограничением сверху. +\item Выходная переменная определяется с ограничением снизу. +\item Если ко входной переменной можно обращаться только как к \code{Object} -- неограниченный подстановочный символ. +\item Если переменная должна использоваться как входная и как выходная одновременно, НЕ использовать подстановочный символ. +\item Не использовать подстановочные символы в возвращаемых типах. +\end{enumerate} + +\subsubsection{Вопросы для самопроверки} +\begin{enumerate} +\item Ограниченный снизу подстановочный символ позволяет передать в качестве аргумента типа + \begin{enumerate} + \item только родителей типа; + \item только наследников типа; + \item сам тип и его родителей; + \item сам тип и его наследников. + \end{enumerate} +\item Конструкция + \begin{enumerate} + \item не скомпилируется; + \item не имеет смысла; + \item не позволит ничего передать в аргумент типа; + \item не является чем-то необычным. + \end{enumerate} +\end{enumerate} + +\subsection{Стирание типа и загрязнение кучи} +Обобщения были введены в язык программирования Java для обеспечения более жёсткого контроля типов во время компиляции и для поддержки обобщённого программирования. Для реализации обобщения компилятор Java применяет стирание типа. + +\subsubsection{Стирание типа} +\begin{itemize} +\item Механизм стирания типа фактически заменяет все параметры типа в обобщённых типах их границами или \code{Object}, если параметры типа не ограничены. +\item Сгенерированный байткод содержит только обычные классы, интерфейсы и методы. +\item Вставляет явное приведение типов где необходимо. +\item Генерирует связующие методы, чтобы сохранить полиморфизм в расширенных обобщённых типах. +\item Гарантирует, что никакие новые классы не будут созданы для параметризованных типов, следовательно обобщения не приводят к накладным расходам во время исполнения. +\end{itemize} + +Компилятор Java также стирает параметры типа обобщённых методов. Обобщённый метод в котором используется неограниченный тип будет заменён компилятором на \code{Object}. + +Аналогично классам для методов происходит стирание типа при расширении ключевым словом \code{extends} -- параметр в угловых скобках заменяется на максимально возможного родителя. + +\begin{lstlisting}[language=Java,style=JCodeStyle,caption={},label={}] +private static void setIfNull(TBox box, T t) { + if (box.getValue() == null) { + box.setValue(t); + } +} +// ... both methods have same erasure +private static void setIfNull(TBox box, Object t) { + if (box.getValue() == null) { + box.setValue(t); + } +} +\end{lstlisting} + +Стирание типа имеет последствия, связанные с произвольным количеством параметров (varargs). + +\textbf{Материализуемые типы} -- это типы, информация о которых полностью доступна во время выполнения, такие как примитивы, необобщённые типы, сырые типы, обращения к неограниченным подстановочным символам. + +\textbf{Нематериализуемые типы} -- это типы, информация о которых удаляется во время компиляции стиранием типов, например, обращения к обобщённым типам, которые не объявлены с помощью неограниченных подстановочных символов. Во время выполнения о нематериализуемых типах нет всей информации. Виртуальная машина Java не может узнать разницу между нематериализуемыми типами во время выполнения. + +\subsubsection{Загрязнение кучи} +Загрязнение кучи (heap pollution) возникает, когда переменная параметризованного типа ссылается на объект, который не является параметризованным типом. Такая ситуация возникает, если программа выполнила операцию, генерирующую предупреждение \code{unchecked warning} во время компиляции. Предупреждение \code{unchecked warning} генерируется, если правильность операции, в которую вовлечён параметризованный тип (например, приведение типа или вызов метода) не может быть проверена. + +Если компилируются различные части кода отдельно, то становится трудно определить потенциальную угрозу загрязнения кучи. + +\subsection{Ограничения обобщений (резюме)} +В этом разделе приводится некоторое резюме вышесказанного в части наиболее частых ошибок при работе с обобщениями. + +Нельзя использовать при создании экземпляра примитивы. Выход из ситуации -- использование классов-обёрток. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-01.jpeg} +\end{figure} + +Нельзя создавать экземпляры параметров типа. В качестве выхода из ситуации -- передавать вновь созданный объект в обобщённые методы в качестве параметра. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-02.jpeg} +\end{figure} + +Статические поля класса являются общими для всех объектов этого класса, поэтому статические поля с типом параметра типа запрещены. Так как статическое поле является общим для коробки с животным, коробки с котом и коробки с собакой, то какого типа это поле? Также запрещено использовать приведение типа к параметризованному типу, если он не использует неограниченный подстановочный символ. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-03.jpeg} +\end{figure} + +Запрещено использовать приведения типов для обобщённых объектов. Так как компилятор стирает все параметры типа из обобщённого кода, то нельзя проверить во время выполнения, какой параметризованный тип используется для обобщённого типа. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-04.jpeg} +\end{figure} + +Запрещено создавать массивы параметризованных типов. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-05.jpeg} +\end{figure} + +Обобщённый класс не может расширять класс \code{Throwable} напрямую или опосредовано. Метод не может ловить (\code{catch}) экземпляр параметра типа. Однако можно использовать параметр типа в \code{throws}. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-06.jpeg} +\end{figure} + +Класс не может иметь два перегруженных метода, которые будут иметь одинаковую сигнатуру после стирания типов. Такой код не скомпилируется. + +\begin{figure}[H] + \centering + \includegraphics[width=12cm]{jd-03-38-08.jpeg} +\end{figure} + +\subsection*{Практическое задание} +\begin{enumerate} +\item Написать метод, который меняет два элемента массива местами (массив может быть любого ссылочного типа); +\item Большая задача: + \begin{itemize} + \item Есть классы \code{Fruit} -> \code{Apple}, \code{Orange}; (больше не надо); + \item Класс \code{Box} в который можно складывать фрукты, коробки условно сортируются по типу фрукта, поэтому в одну коробку нельзя сложить и яблоки, и апельсины; Для хранения фруктов внутри коробки можете использовать \code{ArrayList}; + \item Сделать метод \code{getWeight()} который высчитывает вес коробки, зная количество фруктов и вес одного фрукта(вес яблока -- \code{1.0f}, апельсина -- \code{1.5f}, не важно в каких единицах); + \item Внутри класса коробки сделать метод \code{compare()}, который позволяет сравнить текущую коробку с той, которую подадут в \code{compare()} в качестве параметра, \code{true} -- если их веса равны, \code{false} в противном случае (коробки с яблоками возможно сравнивать с коробками с апельсинами); + \item Написать метод, который позволяет пересыпать фрукты из текущей коробки в другую коробку (при этом, нельзя яблоки высыпать в коробку с апельсинами), соответственно, в текущей коробке фруктов не остается, а в другую перекидываются объекты, которые были в этой коробке. + \end{itemize} +\end{enumerate} + +\newpage +\printnomenclature[40mm] + +\end{document} diff --git a/jtd3-08-workshop.tex b/jtd3-08-workshop.tex new file mode 100644 index 0000000..8c020ec --- /dev/null +++ b/jtd3-08-workshop.tex @@ -0,0 +1,624 @@ +\documentclass[j-spec.tex]{subfiles} +\usepackage{spreadtab} + +\begin{document} +\section{Семинар: Обобщения} +\subsection{Инструментарий} +\begin{itemize} +\item \href{https://docs.google.com/presentation/d/1rWHGQBITRElvyKFEjwe8dOinDbAV2ZW18tAK1FXU48U/edit?usp=share_link}{Презентация} для преподавателя, ведущего семинар; +\item \href{https://drive.google.com/file/d/1LWyE8aEy4-1gsognqhXIXwDcoLviVge4/view}{Фон} GeekBrains для проведения семинара в Zoom; +\item JDK любая 11 версии и выше; +\item \href{https://www.jetbrains.com/idea/download}{IntelliJ IDEA Community Edition} для практики и примеров используется IDEA. +\end{itemize} + +\subsection{Цели семинара} +\begin{itemize} +\item Практика создания простых обобщений; +\item Создание собственного обобщённого контейнера с описанием базовых алгоритмов (введение в коллекции); +\item Изучение поведения объекта итератора; +\item Закрепление понимания механизмов работы коллекций. +\end{itemize} + +\subsection{План-содержание} +\noindent +\begin{spreadtab}{{longtable}{|p{37mm}|l|l|p{90mm}|}} +\hline + @ Что происходит & @ Время & @ Слайды & @ Описание \\ +\hline +\endhead + @ Организационный момент & 5 tag(beg) & @ 1-5 & @ Преподаватель ожидает студентов, поддерживает активность и коммуникацию в чате, озвучивает цели и планы на семинар. Важно упомянуть, что выполнение домашних заданий с лекции является, фактически, подготовкой к семинару \\ +\hline + @ Quiz & 5 & @ 6-18 & @ Преподаватель задаёт вопросы викторины, через 30 секунд демонстрирует слайд-подсказку и ожидает ответов (по минуте на ответ) \\ +\hline + @ Рассмотрение ДЗ лекции & 10 & @ 19-23 & @ Преподаватель демонстрирует свой вариант решения домашнего задания с лекции, возможно, по предварительному опросу, демонстрирует и разбирает вариант решения одного из студентов \\ +\hline + @ Вопросы и ответы & 10 & @ 24 & @ Преподаватель ожидает вопросов по теме прошедшей лекции, викторины и продемонстрированной работы \\ +\hline + @ Задание 1 & 10 & @ 25-27 & @ Формирование навыков базовой работы с обобщениями \\ +\hline + @ Задание 2 & 20 & @ 28-30 & @ Практика написания обобщённого кода с дополнительным описанием алгоритмов манипулирования данными \\ +\hline + @ Перерыв (если нужен) & 5 & @ 31 & @ Преподаватель предлагает студентам перерыв на 5 минут (студенты голосуют) \\ +\hline + @ Задание 3 & 20 & @ 32-34 & @ Изучение поведения итератора для понимания механизмов его действия и необходимости его использования в работе \\ +\hline + @ Задание 4 & 20 & @ 35-38 & @ Закрепление понимания механизмов работы коллекций (в том числе с применением итераторов) \\ +\hline + @ Домашнее задание & 5 & @ 39-40 & @ Объясните домашнее задание, подведите итоги урока \\ +\hline + @ Рефлексия & 10 tag(end) & @ 41-42 & @ Преподаватель запрашивает обратную связь \\ +\hline + @ Длительность & sum(cell(beg):cell(end)) & & \\ +\hline +\end{spreadtab} + +\subsection{Подробности} +\subsubsection*{Организационный момент} +\begin{itemize} +\item \textbf{Цель этапа:} Позитивно начать урок, создать комфортную среду для обучения. +\item \textbf{Тайминг:} 3-5 минут. +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Запрашивает активность от аудитории в чате; + \item Презентует цели курса и семинара; + \item Презентует краткий план семинара и что студент научится делать. + \end{itemize} +\end{itemize} + +\subsubsection*{Quiz} +\begin{itemize} +\item \textbf{Цель этапа:} Вовлечение аудитории в обратную связь. +\item \textbf{Тайминг:} 5--7 минут (4 вопроса, по минуте на ответ). +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Преподаватель задаёт вопросы викторины, представленные на слайдах презентации; + \item через 30 секунд демонстрирует слайд-подсказку и ожидает ответов. + \end{itemize} +\item \textbf{Вопросы и ответы:} + \begin{enumerate} + \item Какая основная цель использования Java generics? (1) + \begin{enumerate} + \item Упрощение программирования и повышение безопасности типов + \item Ускорение работы программы и сокращение объема кода + \item Позволяют работать с различными базами данных одновременно + \end{enumerate} +% Неверные ответы: b) и c) - generics не связаны с ускорением работы программы или работой с базами данных, их основная цель - добавить типовую безопасность и упростить программирование. + \item Что такое параметризованный класс в Java generics? (1) + \begin{enumerate} + \item Класс, который принимает параметр типа + \item Класс, который имеет только один параметр типа + \item Класс, который можно использовать только с определенным типом данных + \end{enumerate} +% Неверные ответы: b) и c) - параметризованный класс может иметь любое количество параметров типа и может быть использован с различными типами данных. + \item Какие ограничения можно использовать с wildcard в Java generics? (1) + \begin{enumerate} + \item extends и super + \item only + \item extends + \end{enumerate} +% Неверные ответы: b) и c) - в generics для ограничения типа используются ключевые слова extends и super, а не only или extends. + + \item Можно ли создать экземпляр обобщенного типа в Java, внутри класса, описывающего этот тип? (2) + \begin{enumerate} + \item Да, это возможно + \item Нет, такое создание экземпляров обобщенных типов запрещено + \item Это зависит от контекста использования обобщенного типа + \end{enumerate} +% Неверные ответы: a) и c) - экземпляры обобщенных типов не могут быть созданы в Java из-за стирания типов, однако можно создать экземпляр обобщенного класса, указав конкретный тип данных. + \end{enumerate} +\end{itemize} + +\subsubsection*{Рассмотрение ДЗ} +\begin{itemize} +\item \textbf{Цель этапа:} Пояснить не очевидные моменты в формулировке ДЗ с лекции, синхронизировать прочитанный на лекции материал к началу семинара. +\item \textbf{Тайминг:} 15-20 минут. +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Преподаватель демонстрирует свой вариант решения домашнего задания из лекции; + \item возможно, по предварительному опросу, демонстрирует и разбирает вариант решения одного из студентов. + \end{itemize} +\item \textbf{Домашнее задание из лекции:} + \begin{itemize} + \item Написать метод, который меняет два элемента массива местами (массив может быть любого ссылочного типа); + + \textbf{Вариант решения} + + Задание с подвохом, при решении необязательно было использовать дженерики, формулировка задания не ограничивает используемые в массиве типы, наоборот, явно проговаривается, что массив может быть любого типа. + \begin{lstlisting}[language=Java,style=JCodeStyle] +private static void swap(Object[] arr, int from, int to) { + Object temp = arr[from]; + arr[from] = arr[to]; + arr[to] = temp; +} + +public static void main(String[] args) { + Object[] arr = {1, 2.0f, "hello"}; + System.out.println(Arrays.toString(arr)); + swap(arr, 0, 2); + System.out.println(Arrays.toString(arr)); +} +\end{lstlisting} + +\item Большая задача: + \begin{itemize} + \item Есть классы \code{Fruit} -> \code{Apple}, \code{Orange}; (больше не надо); + \item Класс \code{Box} в который можно складывать фрукты, коробки условно сортируются по типу фрукта, поэтому в одну коробку нельзя сложить и яблоки, и апельсины; Для хранения фруктов внутри коробки можете использовать \code{ArrayList}; + \item Сделать метод \code{getWeight()} который высчитывает вес коробки, зная количество фруктов и вес одного фрукта(вес яблока -- \code{1.0f}, апельсина -- \code{1.5f}, не важно в каких единицах); + \item Внутри класса коробки сделать метод \code{compare()}, который позволяет сравнить текущую коробку с той, которую подадут в \code{compare()} в качестве параметра, \code{true} -- если их веса равны, \code{false} в противном случае (коробки с яблоками возможно сравнивать с коробками с апельсинами); + \item Написать метод, который позволяет пересыпать фрукты из текущей коробки в другую коробку (при этом, нельзя яблоки высыпать в коробку с апельсинами), соответственно, в текущей коробке фруктов не остается, а в другую перекидываются объекты, которые были в этой коробке. + \end{itemize} + + \textbf{Вариант решения} +Создали фрукты с каким то разным весом, создали коробку, которая может вместить только фрукты. Надо фрукты там как то хранить, поэтому -- список. Соответственно при создании можем в нашу коробку какие то фрукты накидать. Соответственно метод, который будет уметь добавлять фрукты в коробку + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={ }] +private interface Fruit { float getWeight(); } +private static class Apple implements Fruit { + static final float weight = 1.0f; + @Override public float getWeight() { return weight; } +} +private static class Orange implements Fruit { + static final float weight = 1.5f; + @Override public float getWeight() { return weight; } +} +public static class Box { + private final List container; + Box(T[] init) { + container = new ArrayList<>(); + for (T f : init) { add(f); } + } + void add(T fruit) { container.add(fruit); } + void print() { System.out.println(getWeight()); } +} +\end{lstlisting} + +Поскольку в коробке у нас могут быть только одно типа фрукты -- мы можем не перебирать каждый из них, а умножить вес одного на количество. + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={ }] +float getWeight() { + return (container.isEmpty()) + ? 0 + : container.get(0).getWeight() * container.size(); +} +\end{lstlisting} + +Можно было, по большому счёту, переопределить метод equals() или написать свой метод compare() который принимает вторую коробку и сравнивает вес. Сравнивать можно любые фрукты, значит в методе не подходит параметр типа \code{} + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={ }] +boolean compare(Box with) { + return this.getWeight() - with.getWeight() < 0.0001; +} + \end{lstlisting} + +Осталось только уметь пересыпать из одной коробки в другую не смешивая. На входе должна быть коробка того же типа, что и у объекта, и тут два пути, либо пересыпать всё из текущей коробки в другую, либо из другой в текущую, метод назван \code{transferTo()}, поэтому пересыпаться фрукты будут из текущей коробки. Возникает вопрос: если создаётся коробка с яблоками \code{Box} и коробка с фруктами \code{Box}, должна ли быть возможность перекинуть из яблочной во фруктовую? Однозначно, нет. Поэтому, в аргументе метода используется \code{Box}, таким образом яблоки возможно кидать в яблоки или в яблочных родителей, но не в апельсины. + \begin{lstlisting}[language=Java,style=JCodeStyle,caption={ }] +void transferTo(Box dest) { + dest.container.addAll(container); + container.clear(); +} + \end{lstlisting} + + \end{itemize} +\end{itemize} + +\subsubsection*{Вопросы и ответы} +\begin{itemize} +\item \textbf{Ценность этапа} Вовлечение аудитории в обратную связь, пояснение неочевидных моментов в материале лекции и другой проделанной работе. +\item \textbf{Тайминг} 5-15 минут +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Преподаватель ожидает вопросов по теме прошедшей лекции, викторины и продемонстрированной работы; + \item Если преподаватель затрудняется с ответом, необходимо мягко предложить студенту ответить на его вопрос на следующем семинаре (и не забыть найти ответ на вопрос студента!); + \item Предложить и показать пути самостоятельного поиска студентом ответа на заданный вопрос; + \item Посоветовать литературу на тему заданного вопроса; + \item Дополнительно указать на то, что все сведения для выполнения домашнего задания, прохождения викторины и работы на семинаре были рассмотрены в методическом материале к этому или предыдущим урокам. + \end{itemize} +\end{itemize} + +\subsubsection*{Задание 1} +\begin{itemize} +\item \textbf{Ценность этапа} Формирование навыков базовой работы с обобщениями. +\item \textbf{Тайминг} 10-15 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \item Пояснить студентам пользу и необходимость следования паттерну MVC, проговорить важность разделения логики и открывающиеся возможности при правильном проектировании. + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item Создать обобщенный класс с тремя параметрами (\code{T}, \code{V}, \code{K}). Класс содержит три переменные типа (\code{T}, \code{V}, \code{K}), конструктор, принимающий на вход параметры типа (\code{T}, \code{V}, \code{K}), методы возвращающие значения трех переменных. Создать метод, выводящий на консоль имена классов для трех переменных класса. Наложить ограничения на параметры типа: \code{T} должен реализовать интерфейс \code{Comparable} (классы оболочки, \code{String}), \code{V} должен реализовать интерфейс \code{DataInput} и расширять класс \code{InputStream}, \code{K} должен расширять класс \code{Number}. + + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] + private static class MyClass { + T t; V v; K k; + + MyClass(T t, V v, K k) { + this.t = t; this.v = v; this.k = k; + } + + public T getT() { return t; } + public V getV() { return v; } + public K getK() { return k; } + + void print() { + System.out.printf("t = %s, v = %s, k = %s", + t.getClass().getName(), + v.getClass().getName(), + k.getClass().getName()); + } + } + \end{lstlisting} + + \end{itemize} +\end{itemize} + +\subsubsection*{Задание 2} +\begin{itemize} +\item \textbf{Ценность этапа} Практика написания обобщённого кода с дополнительным описанием алгоритмов манипулирования данными. +\item \textbf{Тайминг} 15-20 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \item Если группа студентов справилась с заданием, а времени осталось более 5 минут, выдавать группе задания «со звёздочкой». + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item Описать собственную коллекцию -- список на основе массива. Коллекция должна иметь возможность хранить любые типы данных, иметь методы добавления и удаления элементов. + + \textbf{Вариант решения} + +\begin{lstlisting}[language=Java,style=JCodeStyle] +private static class OwnList { + Object[] arr; + int count; + + OwnList() { + arr = new Object[1]; + count = 0; + } + + void add(T item) { + if (count == arr.length) { + Object[] newArr = new Object[arr.length * 2]; + System.arraycopy(arr, 0, newArr, 0, arr.length); + arr = newArr; + } + arr[count++] = item; + } + + T remove() { + if (count == 0) throw new NoSuchElementException(); + T temp = (T) arr[--count]; + arr[count] = null; + return temp; + } + + @Override + public String toString() { + return Arrays.toString(arr); + } +} +\end{lstlisting} + + \end{itemize} +\end{itemize} + +\subsubsection*{Задание 3} +\begin{itemize} +\item \textbf{Ценность этапа} Изучение поведения итератора для понимания механизмов его действия и необходимости его использования в работе. +\item \textbf{Тайминг} 20-25 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item Написать итератор по массиву. Итератор -- это объект, осуществляющий движение по коллекциям любого типа, содержащим любые типы данных. Итераторы обычно имеют только два метода -- проверка на наличие следующего элемента и переход к следующему элементу. Но также, особенно в других языках программирования, возможно встретить итераторы, реализующие дополнительную логику. + + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] + private static class ArrayIterator { + + private final T[] array; + private int index = 0; + + public ArrayIterator(T[] array) { + this.array = array; + } + + public boolean hasNext() { + return index < array.length; + } + + public T next() { + if(!hasNext()) + throw new NoSuchElementException(); + return array[index++]; + } + } + + public static void main(String[] args) { + Integer[] arr = {1,2,3,4}; + ArrayIterator it = new ArrayIterator<>(arr); + while (it.hasNext()) { + System.out.print(it.next() + " "); + } + } + \end{lstlisting} + \item [$*_1$] Написать итератор таким образом, чтобы его возможно было использовать для цикла \code{foreach}. + + \textbf{Вариант решения} + + Указать, что для этого достаточно реализовать интерфейс \code{Iterator}. + + \begin{lstlisting}[language=Java,style=JCodeStyle] +class ArrayIterator implements Iterator{ + + private T[] array; + private int index = 0; + + public ArrayIterator(T[] array) { + this.array = array; + } + + @Override + public boolean hasNext() { + return index < array.length; + } + + @Override + public T next() { + if(!hasNext()) + throw new NoSuchElementException(); + return array[index++]; + } +} + \end{lstlisting} + +\end{itemize} +\end{itemize} + +\subsubsection*{Задание 4} +\begin{itemize} +\item \textbf{Ценность этапа} Закрепление понимания механизмов работы коллекций (в том числе с применением итераторов). +\item \textbf{Тайминг} 20-25 мин +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Выдать задание студентам; + \item Подробно объяснить, что именно требуется от студентов, избегая упоминания конкретных языковых конструкций; + \end{itemize} +\item \textbf{Задание}: + \begin{itemize} + \item Описать интерфейс \code{Person} с методами \code{doWork()} и \code{haveRest()}. Написать два класса работник и бездельник, реализующих интерфейс. Работник работает, и не умеет бездельничать, в то время как бездельник не умеет работать, но умеет отдыхать. Написать обобщённые классы \code{Workplace} и \code{Club}, содержащие массив из \code{Person}. В классах необходимо вызывать у всего массива людей вызывать соответствующие методы. + + \textbf{Вариант решения} + + \begin{lstlisting}[language=Java,style=JCodeStyle] + interface Person { + void doWork(); + void haveRest(); + } + + private static class Worker implements Person { + @Override public void doWork() { + System.out.println("Work!"); } + @Override public void haveRest() { + System.out.println("What?"); + } + } + + private static class Idler implements Person { + @Override public void doWork() { + System.out.println("No!"); } + @Override public void haveRest() { + System.out.println("Chill!"); + } + } + + private static class Workplace { + Person[] arr; + public Workplace(T... people) { + arr = people; + } + void work() { + for (Person person : arr) { person.doWork(); } + } + } + private static class Club { + Person[] arr; + public Club(T... people) { + arr = people; + } + void chill() { + for (Person person : arr) { person.haveRest(); } + } + } + + public static void main(String[] args) { + Workplace w = new Workplace<>(new Worker(), new Worker(), new Idler()); + Club c = new Club<>(new Worker(), new Worker(), new Idler()); + w.work(); + c.chill(); + } + \end{lstlisting} + +\end{itemize} +\end{itemize} + +\subsubsection*{Домашнее задание} +\begin{itemize} +\item \textbf{Ценность этапа} Задать задание для самостоятельного выполнения между занятиями. +\item \textbf{Тайминг} 5-10 минут. +\item \textbf{Действия преподавателя} + \begin{itemize} + \item Пояснить студентам в каком виде выполнять и сдавать задания + \item Уточнить кто будет проверять работы (преподаватель или ревьювер) + \item Объяснить к кому обращаться за помощью и где искать подсказки + \item Объяснить где взять проект заготовки для дз + \end{itemize} +\item \textbf{Задания} + \begin{enumerate} + \item [5-25 мин] Выполнить все задания семинара, если они не были решены, без ограничений по времени; + + \textbf{Все варианты решения приведены в тексте семинара выше} + \item [25 мин] 1. Внедрить итератор из задания 2 в коллекцию, написанную в задании 3 таким образом, чтобы итератор был внутренним классом и, соответственно, объектом в коллекции. + + \begin{lstlisting}[language=Java,style=JCodeStyle] +private static class OList implements Iterable { + class ArrayIterator implements Iterator { + private int index = 0; + + @Override + public boolean hasNext() { + return index < count; + } + + @Override + public T next() { + if(!hasNext()) + throw new NoSuchElementException(); + return (T) arr[index++]; + } + } + + Object[] arr; + int count; + Main.OList.ArrayIterator iter; + + OList() { + arr = new Object[1]; + count = 0; + this.iter = new ArrayIterator(); + } + + void add(T item) { + if (count == arr.length) { + Object[] newArr = new Object[arr.length * 2]; + System.arraycopy(arr, 0, newArr, 0, arr.length); + arr = newArr; + } + arr[count++] = item; + } + T remove() { + if (count == 0) throw new NoSuchElementException(); + --count; + T temp = (T) arr[count]; + arr[count] = null; + return temp; + } + + @Override + public Iterator iterator() { + return iter; + } + + @Override + public Spliterator spliterator() { + return Iterable.super.spliterator(); + } + + @Override + public String toString() { + return Arrays.toString(arr); + } +} + \end{lstlisting} + + \item [15 мин] 2. Написать класс Калькулятор (необобщенный), который содержит обобщенные статические методы: \code{sum()}, \code{multiply()}, \code{divide()}, \code{subtract()}. Параметры этих методов -- два \textbf{числа} разного типа, над которыми должна быть произведена операция. + \begin{lstlisting}[language=Java,style=JCodeStyle] +private static class Calculator { + public static double sum(T num1, U num2) { + return num1.doubleValue() + num2.doubleValue(); + } + + public static double multiply(T num1, U num2) { + return num1.doubleValue() * num2.doubleValue(); + } + + public static double divide(T num1, U num2) { + return num1.doubleValue() / num2.doubleValue(); + } + + public static double subtract(T num1, U num2) { + return num1.doubleValue() - num2.doubleValue(); + } +} + +public static void main(String[] args) { + System.out.println(Calculator.sum(2, 2.0)); + System.out.println(Calculator.multiply(2.0f, 2.0)); + System.out.println(Calculator.divide(2L, 2.0)); + System.out.println(Calculator.subtract(2, 2)); +} + \end{lstlisting} + \item [10 мин] 3. Напишите обобщенный метод \code{compareArrays()}, который принимает два массива и возвращает \code{true}, если они одинаковые, и \code{false} в противном случае. Массивы могут быть любого типа данных, но должны иметь одинаковую длину и содержать элементы одного типа. + + \begin{lstlisting}[language=Java,style=JCodeStyle] +private static boolean compareArrays(T[] array1, T[] array2) { + if (array1.length != array2.length) { + return false; + } + + for (int i = 0; i < array1.length; i++) { + if (!array1[i].equals(array2[i])) { + return false; + } + } + + return true; +} + \end{lstlisting} + \item [10 мин] 4. Напишите обобщенный класс \code{Pair}, который представляет собой пару значений разного типа. Класс должен иметь методы \code{getFirst()}, \code{getSecond()} для получения значений пары, а также переопределение метода \code{toString()}, возвращающее строковое представление пары. + \begin{lstlisting}[language=Java,style=JCodeStyle] +private static class Pair { + private T1 first; + private T2 second; + + public Pair(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + public T1 getFirst() { return first; } + public T2 getSecond() { return second; } + + @Override public String toString() { + return "(" + first + ", " + second + ")"; + } +} + +public static void main(String[] args) { + Pair pair1 = new Pair<>(1, "one"); + System.out.println(pair1.getFirst()); // 1 + System.out.println(pair1.getSecond()); // "one" + System.out.println(pair1); // "(1, one)" + + Pair pair2 = new Pair<>("pi", 3.14); + System.out.println(pair2.getFirst()); // "pi" + System.out.println(pair2.getSecond()); // 3.14 + System.out.println(pair2); // "(pi, 3.14)" +} + \end{lstlisting} + \end{enumerate} +\end{itemize} + +\subsubsection*{Рефлексия и завершение семинара} +\begin{itemize} +\item \textbf{Цель этапа:} Привести урок к логическому завершению, посмотреть что студентам удалось, что было сложно и над чем нужно еще поработать +\item \textbf{Тайминг:} 5-10 минут +\item \textbf{Действия преподавателя:} + \begin{itemize} + \item Запросить обратную связь от студентов. + \item Подчеркните то, чему студенты научились на занятии. + \item Дайте рекомендации по решению заданий, если в этом есть необходимость + \item Дайте краткую обратную связь студентам. + \item Поделитесь ощущением от семинара. + \item Поблагодарите за проделанную работу. + \end{itemize} +\end{itemize} + +\newpage +\end{document} diff --git a/pdfit.sh b/pdfit.sh old mode 100755 new mode 100644 diff --git a/pics/01-ess-00-fon-neyman.svg b/pics/01-ess-00-fon-neyman.svg new file mode 100644 index 0000000..8944564 --- /dev/null +++ b/pics/01-ess-00-fon-neyman.svg @@ -0,0 +1,137 @@ + + + + + + + + + Ядро + + Память + + + + + шинаинструкций + шинаданных + фон-Нейман + + diff --git a/pics/01-ess-00-harvard.svg b/pics/01-ess-00-harvard.svg new file mode 100644 index 0000000..d18a97d --- /dev/null +++ b/pics/01-ess-00-harvard.svg @@ -0,0 +1,156 @@ + + + + + + + + + Ядро + + Память + + + + + + + + + + + шинаинструкций + шинаданных + гарвард + + diff --git a/pics/jd-01-btn-01.png b/pics/jd-01-btn-01.png new file mode 100644 index 0000000..8183672 Binary files /dev/null and b/pics/jd-01-btn-01.png differ diff --git a/pics/jd-01-btn-02.png b/pics/jd-01-btn-02.png new file mode 100644 index 0000000..a604559 Binary files /dev/null and b/pics/jd-01-btn-02.png differ diff --git a/pics/jd-01-btn-03.png b/pics/jd-01-btn-03.png new file mode 100644 index 0000000..2d8179e Binary files /dev/null and b/pics/jd-01-btn-03.png differ diff --git a/pics/jd-01-jframe-01.png b/pics/jd-01-jframe-01.png new file mode 100644 index 0000000..a0a3720 Binary files /dev/null and b/pics/jd-01-jframe-01.png differ diff --git a/pics/jd-01-ttt-hum-turn.png b/pics/jd-01-ttt-hum-turn.png new file mode 100644 index 0000000..48b1117 Binary files /dev/null and b/pics/jd-01-ttt-hum-turn.png differ diff --git a/pics/jd-01-ttt-lined-result.png b/pics/jd-01-ttt-lined-result.png new file mode 100644 index 0000000..58ab02b Binary files /dev/null and b/pics/jd-01-ttt-lined-result.png differ diff --git a/pics/jd-01-ttt-rslt-cmp.png b/pics/jd-01-ttt-rslt-cmp.png new file mode 100644 index 0000000..69fd1b8 Binary files /dev/null and b/pics/jd-01-ttt-rslt-cmp.png differ diff --git a/pics/jd-01-ttt-rslt-drw.png b/pics/jd-01-ttt-rslt-drw.png new file mode 100644 index 0000000..996e1c7 Binary files /dev/null and b/pics/jd-01-ttt-rslt-drw.png differ diff --git a/pics/jd-01-ttt-rslt-hum.png b/pics/jd-01-ttt-rslt-hum.png new file mode 100644 index 0000000..98883dc Binary files /dev/null and b/pics/jd-01-ttt-rslt-hum.png differ diff --git a/pics/jd-02-adapter-impl.png b/pics/jd-02-adapter-impl.png new file mode 100644 index 0000000..63377be Binary files /dev/null and b/pics/jd-02-adapter-impl.png differ diff --git a/pics/jd-02-except-handle.png b/pics/jd-02-except-handle.png new file mode 100644 index 0000000..08712eb Binary files /dev/null and b/pics/jd-02-except-handle.png differ diff --git a/pics/jd-02-except-not-seen.png b/pics/jd-02-except-not-seen.png new file mode 100644 index 0000000..fecc132 Binary files /dev/null and b/pics/jd-02-except-not-seen.png differ diff --git a/pics/jd-02-listener-is-this.png b/pics/jd-02-listener-is-this.png new file mode 100644 index 0000000..b4b3251 Binary files /dev/null and b/pics/jd-02-listener-is-this.png differ diff --git a/pics/jd-02-mouse-adapter.png b/pics/jd-02-mouse-adapter.png new file mode 100644 index 0000000..e1e6a81 Binary files /dev/null and b/pics/jd-02-mouse-adapter.png differ diff --git a/pics/jd-02-mouse-listener.png b/pics/jd-02-mouse-listener.png new file mode 100644 index 0000000..6718914 Binary files /dev/null and b/pics/jd-02-mouse-listener.png differ diff --git a/pics/jd-02-result-circles.png b/pics/jd-02-result-circles.png new file mode 100644 index 0000000..2338ac1 Binary files /dev/null and b/pics/jd-02-result-circles.png differ diff --git a/pics/jd-03-38-01.jpeg b/pics/jd-03-38-01.jpeg new file mode 100644 index 0000000..a699c84 Binary files /dev/null and b/pics/jd-03-38-01.jpeg differ diff --git a/pics/jd-03-38-02.jpeg b/pics/jd-03-38-02.jpeg new file mode 100644 index 0000000..db3ebdd Binary files /dev/null and b/pics/jd-03-38-02.jpeg differ diff --git a/pics/jd-03-38-03.jpeg b/pics/jd-03-38-03.jpeg new file mode 100644 index 0000000..0005584 Binary files /dev/null and b/pics/jd-03-38-03.jpeg differ diff --git a/pics/jd-03-38-04.jpeg b/pics/jd-03-38-04.jpeg new file mode 100644 index 0000000..90a43f6 Binary files /dev/null and b/pics/jd-03-38-04.jpeg differ diff --git a/pics/jd-03-38-05.jpeg b/pics/jd-03-38-05.jpeg new file mode 100644 index 0000000..79141b3 Binary files /dev/null and b/pics/jd-03-38-05.jpeg differ diff --git a/pics/jd-03-38-06.jpeg b/pics/jd-03-38-06.jpeg new file mode 100644 index 0000000..e198f40 Binary files /dev/null and b/pics/jd-03-38-06.jpeg differ diff --git a/pics/jd-03-38-08.jpeg b/pics/jd-03-38-08.jpeg new file mode 100644 index 0000000..20fea11 Binary files /dev/null and b/pics/jd-03-38-08.jpeg differ diff --git a/pics/jd-03-ide-unchecked.jpeg b/pics/jd-03-ide-unchecked.jpeg new file mode 100644 index 0000000..a30a828 Binary files /dev/null and b/pics/jd-03-ide-unchecked.jpeg differ diff --git a/pics/jd-03-lint-unchecked.jpeg b/pics/jd-03-lint-unchecked.jpeg new file mode 100644 index 0000000..e0e2f39 Binary files /dev/null and b/pics/jd-03-lint-unchecked.jpeg differ diff --git a/pics/jd-03-lists-merged-problem.jpeg b/pics/jd-03-lists-merged-problem.jpeg new file mode 100644 index 0000000..59f6ecd Binary files /dev/null and b/pics/jd-03-lists-merged-problem.jpeg differ diff --git a/pics/jd-03-no-both-borders.jpeg b/pics/jd-03-no-both-borders.jpeg new file mode 100644 index 0000000..992db77 Binary files /dev/null and b/pics/jd-03-no-both-borders.jpeg differ diff --git a/pics/jd-03-wildcard-compile-error.jpeg b/pics/jd-03-wildcard-compile-error.jpeg new file mode 100644 index 0000000..f8de6d1 Binary files /dev/null and b/pics/jd-03-wildcard-compile-error.jpeg differ diff --git a/pics/jd-03-wildcard-error.jpeg b/pics/jd-03-wildcard-error.jpeg new file mode 100644 index 0000000..a6556e3 Binary files /dev/null and b/pics/jd-03-wildcard-error.jpeg differ diff --git a/pics/jd-04-04-one-proc.svg b/pics/jd-04-04-one-proc.svg new file mode 100644 index 0000000..c50a0ae --- /dev/null +++ b/pics/jd-04-04-one-proc.svg @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ... + Процесс 3 + Процесс 2 + Процесс 1 + Процесс 0 + + Процессор компьютера + Время + ... + вкл + HyperThreading + + + + + + + + + + + + + diff --git a/pics/jd-04-04-threading.svg b/pics/jd-04-04-threading.svg new file mode 100644 index 0000000..1e1fde1 --- /dev/null +++ b/pics/jd-04-04-threading.svg @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ... + Процесс 3 + Процесс 2 + Процесс 1 + Процесс 0 + + + Процессор компьютера + Время + ... + вкл + HyperThreading + + + + + + + + + + + + + + + + + + ... + Поток 3 + Поток 2 + Поток 1 + Поток 0 + + Один процесс с системе + Время + ... + старт + Многопоточность + + + + + + + + + + + + + + + + diff --git a/pics/jd-04-04-two-proc.svg b/pics/jd-04-04-two-proc.svg new file mode 100644 index 0000000..94f5ad5 --- /dev/null +++ b/pics/jd-04-04-two-proc.svg @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ... + Процесс 3 + Процесс 2 + Процесс 1 + Процесс 0 + + + Процессор компьютера + Время + ... + вкл + HyperThreading + + + + + + + + + + + + + + + + + + + diff --git a/pics/jd-04-05-0.svg b/pics/jd-04-05-0.svg new file mode 100644 index 0000000..560cf4b --- /dev/null +++ b/pics/jd-04-05-0.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pics/jd-04-05-1.svg b/pics/jd-04-05-1.svg new file mode 100644 index 0000000..4ec396c --- /dev/null +++ b/pics/jd-04-05-1.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + 1 + + diff --git a/pics/jd-04-05-10.svg b/pics/jd-04-05-10.svg new file mode 100644 index 0000000..d426a50 --- /dev/null +++ b/pics/jd-04-05-10.svg @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + EventQueue + + OverrideAction Performed + + diff --git a/pics/jd-04-05-11.svg b/pics/jd-04-05-11.svg new file mode 100644 index 0000000..d53c39c --- /dev/null +++ b/pics/jd-04-05-11.svg @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 11 + + + + + + + + + + + + + EventQueue + OverrideAction Performed + + + diff --git a/pics/jd-04-05-2.svg b/pics/jd-04-05-2.svg new file mode 100644 index 0000000..6bb1c80 --- /dev/null +++ b/pics/jd-04-05-2.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + 2 + + diff --git a/pics/jd-04-05-3.svg b/pics/jd-04-05-3.svg new file mode 100644 index 0000000..1f0d86d --- /dev/null +++ b/pics/jd-04-05-3.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + 3 + + diff --git a/pics/jd-04-05-4.svg b/pics/jd-04-05-4.svg new file mode 100644 index 0000000..3303ad9 --- /dev/null +++ b/pics/jd-04-05-4.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + 4 + + diff --git a/pics/jd-04-05-5.svg b/pics/jd-04-05-5.svg new file mode 100644 index 0000000..448a1a4 --- /dev/null +++ b/pics/jd-04-05-5.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + diff --git a/pics/jd-04-05-6.svg b/pics/jd-04-05-6.svg new file mode 100644 index 0000000..757e18f --- /dev/null +++ b/pics/jd-04-05-6.svg @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6 + + + + + + + + + + + + + EventQueue + + + diff --git a/pics/jd-04-05-7.svg b/pics/jd-04-05-7.svg new file mode 100644 index 0000000..5f94de0 --- /dev/null +++ b/pics/jd-04-05-7.svg @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7 + + + + + + + + + + + + + EventQueue + + + diff --git a/pics/jd-04-05-8.svg b/pics/jd-04-05-8.svg new file mode 100644 index 0000000..5379999 --- /dev/null +++ b/pics/jd-04-05-8.svg @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 + + + + + + + + + + + + + EventQueue + OverrideAction Performed + + diff --git a/pics/jd-04-05-9.svg b/pics/jd-04-05-9.svg new file mode 100644 index 0000000..287692b --- /dev/null +++ b/pics/jd-04-05-9.svg @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 9 + + + + + + + + + + + + + EventQueue + + OverrideAction Performed + + diff --git a/pics/jd-04-16-01.svg b/pics/jd-04-16-01.svg new file mode 100644 index 0000000..fe35003 --- /dev/null +++ b/pics/jd-04-16-01.svg @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + Thread-3 + main + now + Thread-0 + Thread-1 + Thread-2 + + + + + + + + + + + + + + + + diff --git a/pics/jd-04-16-02.svg b/pics/jd-04-16-02.svg new file mode 100644 index 0000000..f27e20c --- /dev/null +++ b/pics/jd-04-16-02.svg @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + 10000x10000 + + + + + + + + diff --git a/pics/jd-04-16-04.svg b/pics/jd-04-16-04.svg new file mode 100644 index 0000000..d54931f --- /dev/null +++ b/pics/jd-04-16-04.svg @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + Thread-3 + main + join + now + Thread-0 + Thread-1 + Thread-2 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pics/jd-04-18-01.svg b/pics/jd-04-18-01.svg new file mode 100644 index 0000000..55e1aab --- /dev/null +++ b/pics/jd-04-18-01.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Thread-1 + Thread-2 + + Ожидает ответадля продолженияработы + Ожидает ответадля продолженияработы + + + + + + diff --git a/scenarios/jtd1-06b.tex b/scenarios/jtd1-06b.tex index 4e34726..5d8b140 100644 --- a/scenarios/jtd1-06b.tex +++ b/scenarios/jtd1-06b.tex @@ -80,7 +80,7 @@ 06-10 & Менеджер размещения или компоновщик, как следует из названия -- это специальный объект, который помещается на некоторые компоненты и осуществляет автоматическую расстановку добавляемых к нему, компоненту, согласно каким-то прописанным внутри данного менеджера размещения правилам. Чуть забегая вперёд можно сказать, что это контейнеры, реализующие программный интерфейс под названием RootPaneContainer, но это так, шестерёнки, которые заучивать не нужно. Важно понимать, что именно делают компоновщики -- размещают компоненты. Список менеджеров можно увидеть на слайде. Все их можно создать, так сказать пощупать, попробовать в работе и это достаточно сильно зависит от задачи -- какой именно выбрать для использования на том или ином экране. О трёх самых популярных сейчас поговорим.\\ \hline -06-11 & По умолчанию в библиотеке swing используется компоновщик borderLayout. Он располагает всё, что вы ему передаёте в центре, но также у него есть ещё четыре положения. маленькие области по краям. Если мы не занимаем какую-то область компонентом, она схломывается до нулевых размеров, оставляя место другим компонентам. Поэтому, если мы хотим какой-то наш компонент расположить где-то не в центре, мы должны это явно указать при добавлении. Тут немного неочевидно, поэтому запомните, пожалуйста, что при добавлении надо указать ещё один параметр, константу, например, BorderLayout.SOUTH. вот так неожиданно сюда сторона света приплелась, юг. Или вот, например, FlowLayout() будет располагать элементы друг за другом в том порядке, как мы пишем, слева направо, сверху вниз, как мы привыкли писать рукой на бумаге. Компоновщик-сетка при создании принимает на вход число строк и столбцов и располагает компоненты в получившейся сетке. Ну и так далее всякие ГридБэгЛэйауты и прочие. Ещё раз, основная идея, которую надо понять, это не названия компоновщиков, а то, что в свинге вся работа происходит через компоновщики - вот эти вот самые лэйауты, которые как-то по-своему располагают элементы в окошке. Кстати, если это ещё не стало очевидным, кнопки расположились одна поверх другой потому что по умолчанию к фрейму применён БодэЛэйаут, который всё располагает по-умолчанию в центре. \\ \hline +06-11 & По умолчанию в библиотеке swing используется компоновщик borderLayout. Он располагает всё, что вы ему передаёте в центре, но также у него есть ещё четыре положения. маленькие области по краям. Если мы не занимаем какую-то область компонентом, она схломывается до нулевых размеров, оставляя место другим компонентам. Поэтому, если мы хотим какой-то наш компонент расположить где-то не в центре, мы должны это явно указать при добавлении. Тут немного неочевидно, поэтому запомните, пожалуйста, что при добавлении надо указать ещё один параметр, константу, например, BorderLayout.SOUTH. вот так неожиданно сюда сторона света приплелась, юг. Или вот, например, FlowLayout() будет располагать элементы друг за другом в том порядке, как мы пишем, слева направо, сверху вниз, как мы привыкли писать рукой на бумаге. Компоновщик-сетка при создании принимает на вход число строк и столбцов и располагает компоненты в получившейся сетке. Ну и так далее всякие ГридБэгЛэйауты и прочие. Ещё раз, основная идея, которую надо понять, это не названия компоновщиков, а то, что в свинге вся работа происходит через компоновщики - вот эти вот самые лэйауты, которые как-то по-своему располагают элементы в окошке. Кстати, если это ещё не стало очевидным, кнопки расположились одна поверх другой потому что по умолчанию к фрейму применён БодэЛэйаут, который всё располагает по умолчанию в центре. \\ \hline 06-12 & Одними лэйаутами сыт не будешь, так что разработчики джавы придумали использовать не только компоненты сами по себе, но ещё и группы элементов, группы элементов складывают на так называемые JPanel. И внутри каждой панели мы можем использовать свой лэйаут. Jpanel - это по умолчанию невидимый прямоугольник, основным свойством которого для нас в данный момент является то, что на нём может находиться собственный компоновщик, что открывает для нас близкие к бесконечным возможности по расстановке компонентов на экране. Например, мы можем создать для нашего окна некую панель с кнопками внизу (или вверху, не важно), а всё остальное пространство оставить под другие важные вещи. На слайде можно увидеть код панели, добавление её в нижнюю часть основного экрана, расположение внутри панели компоновщика и двух кнопок. И, соответственно, результат. Обратите особенное внимание на 25ю строку, на экран мы добавляем не кнопки по отдельности, а компонент, на который предварительно добавили кнопки. \\ \hline @@ -207,4 +207,3 @@ \end{longtable} \end{document} - diff --git a/scenarios/jtd3-08b.tex b/scenarios/jtd3-08b.tex index 5dc4a2e..0211384 100644 --- a/scenarios/jtd3-08b.tex +++ b/scenarios/jtd3-08b.tex @@ -217,9 +217,7 @@ То есть, если написать тот же самый метод принтИнфо, с параметром коробка и обобщённым аргументом не экстендс, а супер животное, то этот код также не будет работать, потому что метод будет ожидать не животное и наследников, а животное и родителей, то есть обжект. \\ \hline - -08-29 & -Обобщённые классы или интерфейсы связаны не только из-за связи между их типами. Однако можно использовать подстановочные символы (wildcards) для создания связи между обобщёнными классами и интерфейсами. С обычными необобщёнными классами Кота и Животного имеет смысл писать какой-нибудь простой незатейливый код, который мы уже десятки, если не сотни раз видели и писали сами. Этот код показывает, что наследование работает по правилу подчинённых типов: класс Cat является подклассом класса Animal, и расширяет его. +08-29 & Обобщённые классы или интерфейсы связаны не только из-за связи между их типами. Однако можно использовать подстановочные символы (wildcards) для создания связи между обобщёнными классами и интерфейсами. С обычными необобщёнными классами Кота и Животного имеет смысл писать какой-нибудь простой незатейливый код, который мы уже десятки, если не сотни раз видели и писали сами. Этот код показывает, что наследование работает по правилу подчинённых типов: класс Cat является подклассом класса Animal, и расширяет его. % 02 И правило не работает для обобщённых типов. Но, если Cat является дочерним типом для Animal, то какая связь между Коробкой с и Коробкой с ? Несмотря на то, что Cat является подтипом Animal, Box не является подтипом Box. Это разные типы. Общим предком для Box и Box является Box с подстановочным символом . diff --git a/scenarios/jtd3-09b.tex b/scenarios/jtd3-09b.tex index fb2e1b3..ed7b79d 100644 --- a/scenarios/jtd3-09b.tex +++ b/scenarios/jtd3-09b.tex @@ -1,25 +1,465 @@ \documentclass[../j-spec.tex]{subfiles} \begin{document} -\section{Обобщения} +\section{Многопоточность} \begin{longtable}{|p{35mm}|p{135mm}|} \hline Экран & Слова \\ \hline \endhead -Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline +Титул & Здравствуйте, рад всех приветствовать на курсе посвящённом инструментарию разработчика на джава \\ \hline -Отбивка & в сегодняшней лекции коснёмся одного из ключевых понятий программирования на языке джава -- обобщённого программирования. \\ \hline +Отбивка & в сегодняшней лекции будем говорить о чрезвычайно важном понятии для всего современного программирования -- многопоточности. \\ \hline + +На прошлом уроке & На прошлом уроке поговорили о крутом полиморфизме. Поговорили про дженерики. Попробовали обойтись без них, потом сделали с ними, сравнили. Совсем чуть-чуть проговорили про дженерик интерфейсы, обобщённые методы и подстановочные символы, а именно, ограничения сверху и снизу, дополнительно снова немного поковырялись в шестерёнках, поверхностно рассказал о том, что такое загрязнение кучи. \\ \hline + +На этом уроке & Нас ждут великие дела. Будем говорить о Многопроцессности и многопроцессорности. вполне очевидно, что от неё придём к нашей основной теме -- Многопоточности. Если говорить о многопоточности, нельзя не сказать об асинхронности. Соответственно, обсудим эти прекрасные явления, опираясь на изучаемый нами язык, а значит обязательно рассмотрим Интерфейс Runnable и класс Thread. ExecutorService. В многопоточности, как и в остальных областях программирования не обошлось без проблем и сегодня наконец-то закроем вопрос методов класса Object. об этом действительно нужно сказать отдельно. \\ \hline + +09-01 & Многопоточность -- это возможность одновременного выполнения нескольких задач в рамках одного процесса. Она является одним из ключевых понятий в программировании и широко используется в современных приложениях. Многопоточность позволяет улучшить производительность программы, в некоторых случаях даже распределяя нагрузку между несколькими ядрами процессора. В отличие от многопроцессности, где каждый процесс имеет своё отдельное адресное пространство, при многопоточности все потоки работают в рамках одного адресного пространства. Это позволяет им обмениваться информацией и синхронизировать свою работу. Кстати об этом, асинхронность -- это ещё одна важная концепция, которая позволяет выполнять задачи без блокирования основного потока выполнения. Архитектура современных процессоров становится всё более сложной, и теперь они имеют несколько ядер, что позволяет параллельно выполнять несколько задач. Однако, создание реальных параллельных вычислителей остается сложной задачей, и требует больших затрат на исследования и разработку. \\ \hline + +09-02 & Вместо введения я хочу вернуть вас на пару-тройку лекций назад, кое что напомнить и постараюсь нагнать немного жути. Но вас же уже не так просто испугать, верно? Расскажу я про уже известные вам вещи с одной целью -- показать, что все вот эти многопоточности -- это не какая-то абстракция, а то, с чем придётся сталкиваться при работе с языком чуть чаще, чем каждый день. Итак, когда Вы создаёте приложение на любой платформе, работающей поверх операционной системы компьютера, у вас создаётся процесс, и как минимум создаётся один поток (мэйн). В джаве таких потоков для каждого процесса создаётся несколько -- Main, Error, GC, EDT, мы их обсуждали когда разговаривали о графических интерфейсах пользователя, эти потоки нужны были для понимания процесса работы с окошком. Поскольку процессоры у нас ещё не очень хорошо работают с параллельной логикой -- создаётся псевдо-параллельность, об этом сейчас тоже поговорим. Если в одном из потоков происходит исключение, другой поток не обязательно об этом узнает. На слайде то мы всё видим, но это всё консоль, среда разработки, понятно, что мы всё видим. А что будет, если мы создадим jar файл, и запустим наше приложение отдельно от среды разработки? Вариантов, если подумать, может быть масса, а вы как думаете? Я думаю тут могут быть два основных варианта - визуально ничего не произойдёт, или приложение полностью грохнется. Второй вариант, очевидно, лучше. Надо ли объяснять почему? (естественно, что-то в приложении произошло, а ни мы ни пользователь не в курсе, очень плохо). На самом деле в джава -- на экране пользователя ничего не произойдёт и это очень плохо. Прониклись проблемой? \\ \hline + +09-03 & Идеи многопроцессности начали развиваться задолго до того, как появился первый персональный компьютер. Тут можно уйти в глубокие дебри не только цифровой схемотехники, но и архитектуры компьютеров, потому что люди старались и стараются, придумывают, как улучшить. Сейчас существует чуть больше, чем очень много разных архитектур компьютеров, но основные пока что сводятся к двум видам -- гарвардская архитектура и фон-Нейманова архитектура, а также их разнообразные сочетания. Уже в 50-х годах прошлого века были запущены проекты, направленные на создание компьютерных систем, способных выполнять одновременно несколько процессов. Однако, тогдашняя технология не позволяла реализовать такие системы в полной мере. Со временем технологии стали развиваться, и появилась возможность создавать компьютеры с несколькими процессорами, их ещё называют мультипроцессорные системы. Многопроцессорность позволяет увеличить производительность компьютера, распределяя нагрузку на несколько ядер процессора. Более современные компьютеры часто имеют более одного процессора на материнской плате. Использование многопроцессорности позволяет ускорить выполнение задач, требующих больших вычислительных мощностей, таких как научные расчеты, 3D-моделирование и игры. В мультипроцессорных системах можно использовать разные типы процессоров, которые могут выполнять различные задачи, что увеличивает гибкость и эффективность работы системы в целом. Однако, часто, написание программ для мультипроцессорных систем может быть сложнее, чем для однопроцессорной системы, из-за необходимости управления распределением нагрузки между процессорами. Кроме того, существуют и разные подходы к реализации многопроцессорности, такие как симметричная многопроцессорность, где все процессоры равноправны, и асимметричная многопроцессорность, где один процессор является основным, а остальные используются для выполнения второстепенных задач. Но нам это не особенно интересно, на самом деле за нас балансировку выполняет виртуальная машина джава. А многопроцессность, о которой мы начали говорить -- это возможность компьютером выполнять одновременно несколько процессов. \\ \hline + +09-04 & То есть, основная идея многопроцессности -- удобство пользователя -- проще всего реализуется через мультипроцессорность. Но до мультипроцессорности технологии дошли сравнительно недавно, как же инженеры выкручивались? Надо было как-то одновременно и показывать окно какого-нибудь редактора и обрабатывать запросы в сеть, проверять электронную почту и получать уведомления от подсистем операционной системы. Выкрутились. Придумали псевдо-параллельность, то есть технологию, при которой какое-то количество времени исполняется один процесс, какое-то время другой, потом снова первый и третий и другие и так далее. А поскольку процессоры сейчас с бешеными гигагерцами, но уже тогда были в сотню кило или даже мегагерц, то для человека создаётся впечатление, что это происходит параллельно, напомню, человеческий глаз очень плохо различает картинки, меняющиеся быстрее, чем с частотой 60 герц, а тут мегагерцы. Технология называется HyperThreading. Потом уже, когда придумали, как вкрутить в один компьютер несколько действительно параллельных процессоров, сделали реальную многопроцессность с действительно параллельными вычислениями. \\ \hline + +09-05 & Ну а дальше, поняли насколько это всё классно и уже не смогли остановиться, поняв, что многопроцессность -- это удобно, но гораздо удобнее, если ещё и приложения смогут выполнять какие-нибудь вычисления в несколько потоков. Так и назвали -- многопоточность. Не путайте с потоками ввода-вывода, то были потоки в смысле Stream, тут потоки в смысле Thread, то есть буквально, нити выполнения. Внезапно повеяло теорией мультивселенных, не обращайте внимания, так всегда происходит. Многопоточность -- это способность одного приложения выполнять одновременно несколько действий. Реальная это многопоточность или только так кажется -- нам сейчас не особенно важно, нам важно, что это уже существует, мы неоднократно видели это в работе и важно понимание механизма. То есть, ещё раз. Многопоточность -- это параллельность в рамках одного процесса, многопроцессность -- это параллельность в глазах человека, многопроцессорность -- это архитектурная особенность, благодаря которой облегчается реализация задач многопроцессности и многопоточности. Единственное, что пока не обговорили -- это асинхронность. не переживайте, я помню, обязательно обсудим. +%02 + +Давайте резюмируем, пока мы смотрим на эту, хорошо объясняющую картинку. Как я уже неоднократно, по-разному формулируя, говорил, поток -- это одна нить выполнения. Это некая абстракция. Все операционные системы делятся на однозадачные и многозадачные. Раньше такие были, может кто-то помнит, MS-DOS например или какие-нибудь старые операционки для встраиваемых систем. Соответственно, на картинке хорошо видно, как реализуется многозадачность на одном и двух ядрах, для многозадачных систем. Это псевдо-многозадачность, мощные процессоры очень-очень быстро переключаются между задачами, и для человеческого глаза кажется, что всё параллельно. Или, допустим, у нас есть задача какая-то большая, и если мы можем эту задачу выполнять как-то разбив на подзадачи, а потом собрав в одну кучу обратно, получить прирост производительности, то это делается, отдавая задачу двум потокам одновременно. А когда завершается процесс? Когда завершается последний поток. Остаётся только вопрос, как это синхронизировать, когда подзадачи выполнились. И вот снова синхронность/асинхронность, привыкайте-привыкайте. \\ \hline + +09-06 & Может показаться, что это вступление затянулось, но я хочу, чтобы это очень прочно закрепилось -- многопоточность повсюду, и так вышло, что мы уже коснулись разработки графических интерфейсов. А значит на примере графических интерфейсов это будет объяснять проще. Каждое окно это отдельный процесс, внутри которого крутится бесконечный цикл, который опрашивает так называемую очередь сообщений. Что это такое? Вот например можно водить по окошку мышкой, и этому окну в очередь прилетает куча много ActionEvent, о том, что на окне мышка шевельнулась, и координаты указателя. А этот бесконечный цикл когда-то, когда ему удобно, опрашивает эту очередь, и как-то обрабатывает события. Вот во фреймворке Swing этот поток называется EDT и его обработчики мы описываем, когда хотим как-то по-своему реагировать на события. +% 02 + +То есть, например, мы кликнули по окошку и у нас появился курсор для ввода текста. А на самом деле довольно много всего произошло: + - я кликнул мышку (окно об этом ещё вообще ничего не знает) + - внутри мышки замкнулась куча цепей, контроллер внутри мышки отправил сигнал в провод +% 03 + + - операционная система увидела что пришло событие от аппаратной части (мышка, координаты на рабочем столе) + - ОС смотрит, О, у меня ж по этим координатам сейчас окошко наверху висит +% 04 + + - ОС посылает окошку сообщение: эй, окошко, по тебе тут кликнули, вот тут + - Окошко крутит себе свой бесконечный цикл, который читает такие сообщения +% 05 + + - Окошко читает сообщение от ОС и думает О, по мне ткнули мышкой вот тут по локальным координатам окна + - Окошко смотрит, а в какой именно мой компонент ткнули +% 06 + + - Окошко смотрит, есть-ли соответствующий обработчик этого события + - Окошко отдаёт это событие обработчику компонента +% 07 + + - Обработчик компонента зажигает по данным координатам курсор. +Из этой длинной истории надо понять одну вещь - даже если я совершил очень долгое последовательное действие, оно выполняется с какими-то другими действиями параллельно и обработка событий - это отдельный поток, у которого есть очередь сообщений. \\ \hline + +09-07 & Давайте делать на основе вышесказанного гадости, исключительно, ради подтверждения. Вот, предположим, у нас есть какое-то окошко с кучей компонентов, возможно даже с клиентом чата. Здесь я предлагаю вам открыть среду разработки, особенно если вы смотрите это в записи. Мы можем по окошку кликать, двигать ползунки, и всё такое, это всё как-то обрабатывается и делается вид, что оно живое. Положим, повесим обработчик нажатия на нашу кнопку Send. +% 02 + +и зациклим его бесконечным циклом, то есть будет очевидно, что работа окна - это один поток, который мы убили так, что восстановить его уже будет нереально. То есть оно не залагало, а прям больше вообще не реагирует на внешние раздражители. События то прилетают, а вот обработчик занят циклом. Можно видеть, что даже отрисовка отжатия кнопки обратно не произошла и окно не реагирует в том числе на нажатие крестика. Это какая-то прям крайняя ситуация. Можно облегчить, вместо бесконечного цикла создать какой-то просто очень долгий. +% 03 + +Пока обработчик висит в цикле, что-то подвигаем и нажмём и увидим, что поток значительно загрузился, но потом его всё-таки отпустило и всё, что мы нажимали -- прилетело в окно и обработалось им, кстати, это хорошо показывает полезность множественных кликов по кнопкам в надежде, что что-то перестанет зависать. Так вот именно такие задачи, которые надолго вешают наши потоки -- принято отдавать другим потокам, называемым фоновыми. Вся сложность будет выполняться в нём, а основной поток, например, с графическим интерфейсом мы отпускаем, чтобы он дальше обрабатывал очередь событий. Мы должны создать то, что называется асинхронность. Ведь правда, пользователю не хочется ждать пока мы что-то посчитаем, а хочется сразу что-то ещё подвигать на окошке. То есть потоки создаются для распределения больших задач, или для отвода глаз, то есть создания фоновых задач. \\ \hline + +09-08 & Пользоваться потоками это классно и удобно, но хорошо бы уметь создавать свои собственные. Одним из самых популярных способов создания собственных потоков в языке джава является использование класса Thread. \\ \hline + +09-09 & Создание потока может производиться двумя способами, самый популярный -- создание собственного класса -- наследника класса Thread. Здесь всё просто -- создаём свой класс, например, MyThread, наследник класса Thread. Но внезапно этого оказывается недостаточно, то есть выполнение действия внутри класса-наследника тред -- не гарантирует того, что действие будет выполнено в отдельном потоке, на слайде видно, что в результате вызова некоторых, пока что магических, методов, мы получаем идентичные названия потоков для главного метода и метода в классе-наследнике. +% 02 + +Любое учебное пособие утверждает, что для того, чтобы использовать класс-наследник треда, нужно переопределить метод ран, и если так сделать, то всё написанное в это методе будет выполнено в отдельном потоке. проверим это утверждение на практике и увидим, что учебники немного лукавят, мы переопределили метод run, вызвали из него метод с так называемой бизнес-логикой, то есть с действиями, которые предполагаем в другом потоке, а работает всё равно вся программа в одном и том же потоке. Выходит, что никакой многопоточности не существует? можно подумать так, но среда разработки начинает нам подсказывать, что с методом ран что-то не совсем так. +% 03 + +Если приглядеться к сообщению среды разработки, можно будет увидеть, что она предполагает, что мы ошибочно вызвали ран вместо старт. И правда, если внимательно почитать документацию, то там написано -- метод старт разделяет выполнение программы на два потока -- основной, в котором вызывался собственно метод старт, и второй, в котором будет выполнена логика, описанная в переопределённом методе ран. На снимке экрана видно, что первое сообщение прилетело от потока с названием мейн, а второе от потока с названием Тред-0 \\ \hline + +09-10 & Итак, как правильно создавать поток с помощью наследования? Переопределяем метод run(){} и всё, что мы в нём напишем, будет выполнено в отдельном потоке. Но при условии, что мы верно запустим созданный поток. Из только что приведённый примеров мы узнали, что у класса Тред есть и статические методы. например, статический метод каррентТред, который является репрезентацией текущего потока, выражаясь проще -- это объект текущего потока, мы можем им как-то управлять, мы можем узнать его имя и совершить какие-то ещё интересные действия, вроде создания демона или изменения приоритета, которые совершаются соответствующими методами. Во вновь созданном потоке сделали единственную вещь -- вывели в консоль его имя. Далее создали экземпляр потока, дополнительно обратите внимание, что просто создание объекта потока ничего не даёт, он не запущен и не работает, и далее в мэйне мы его запустили вызвав метод старт. +% 02 + +Это самый простой способ запустить второй третий и вообще любое количество одинаковых потоков. У объектов класса тред есть методы .setName(); setPriority() и так далее, но это уже мелочи, посмотреть на список методов можно самостоятельно. Например у класса Thread есть конструктор, который принимает на вход строку, которая будет означать имя создаваемого конструктором потока. А для того, чтобы им воспользоваться нам надо сделать что? правильно, создать в потоке-наследнике конструктор, и вызвать супер(имя)). Вообще, можно довольно много интересного сделать с потоками при помощи этих методов и конструкторов, например сразу сделать чтобы создаваемый поток стартовал сам себя, для этого в конструкторе пишем вызов метода start(). И соответственно зачем сохранять идентификатор объекта, если мы можем стартовать поток анонимно. Но это уже такие вещи, больше просто объектно-ориентированные, нежели касающиеся непосредственно потоков. \\ \hline + +09-11 & Что можно сделать с потоком? давайте предположим, что у нас есть какая-то долгая задача, чтобы это имитировать, в наследнике можем длинный цикл написать, либо там что-то вообще сильно навсегда зациклено, видим вайл-тру, и допускаем, что поток ждёт каких-то событий, которые могут в него прилететь. Запустив поток видим, что, в общем-то, всё, он зациклен, и остановить мы его никак не можем. Но это ситуация подконтрольная, мы сами её только что в тепличных условиях создали. А что если ситуация возникла без нашего ведома и программа по какой-то причине попала в бесконечный цикл? Верно, со стороны будет казаться что программа зависла и не реагирует на внешние раздражители. Нам надо как-то наш поток остановить снаружи, а как это сделать, как отдать команду потоку на завершение? +% 02 + +Хорошо посмотрев документацию или исходники класса тред можно обнаружить, что у треда есть метод стоп, но если смотреть очень внимательно, можно увидеть, что он отмечен аннотацией депрекейтед. причём запрещён этот метод аж со второй версии языка, то есть, практически, сразу. Интересно, почему запретили жёстко завершать потоки? давайте порассуждаем. Допустим, что мы отдали потоку какую-то сложную длинную вычислительную задачу и он эту задачу выполняет, помните картинку с псевдо-параллельной многопоточностью? Так вот, выполняет наш поток какую-то задачу, а мы его, получается, прерываем на полу-слове, и неизвестно, что там происходит, может он там ресурсы какие-то захватил, может он скоро, вот-вот, закончил бы. Это всем очевидно? поток там чем-то занимался, возможно, важным, а мы ему топором по голове НА ТЕБЕ и непонятно в каком состоянии данные, что он сделал, что не сделал, и что теперь делать с неживым потоком. +% 03 + +И придумали потоки завершать мягко. у класса тред внутри есть метод isInterrupted() который возвращает флажок isInterrupted. И вместо while (true) всегда желательно писать while (!isInterrupted()) и где-то снаружи станоится возможным потоку сказать thread.interrupt(); и этот флажок начинает тру возвращать, а следовательно, когда-то цикл прервётся и поток завершит работу. Соответственно задача программиста в том, чтобы сложный метод внутри многопоточной программы так написать, чтобы он иногда проверял этот флажок, и мог мягко завершиться. \\ \hline + +09-12 & Это всё довольно скучно и не очень понятно, что с этим делать, потому что слишком абстрактно и теоретически. Раз уж у нас тут неподалёку есть ЧатКлиент, предлагаю на примере чата что нибудь и сделать. Те, кто был на прошлых семинарах, могут припомнить, что мы делали простое окошко управления сервером, где было две кнопки. У ChatServer, как и у любого другого сервера, есть методы старт и стоп, довольно очевидно, чтобы запускать и останавливать сервер, они-то нам и пригодятся. Создадим какой-нибудь хороший класс, и сделаем в нём что-нибудь умное, чтобы все остальные программисты нам завидовали. Отходя немного в сторону от темы, у любого сервера, рано или поздно, появляется задача - ожидать входящего соединения. Не особенно заморачивайтесь что это вообще такое, пока что нам надо понимать только формулировку. И заниматься этим ожиданием соединений будет класс с очень странным названием ServerSocketThread, который будет наследоваться от Thread и мы его пока что будем использовать для тестов. Внутри этого странно названного класса переопределяем метод run(). И описываем в классе конструктор. Из очевидного -- спросим у создающего имя и порт, который нужно будет слушать, и сразу запустим поток. Получится такая вот заготовка. +% 02 + +Теперь нам очень важно правильно написать работу этого потока. Для начала объяснения пока что в методе run его зациклим. Первый вариант -- это мы его циклим в бесконечность с простым выводом информации о том, что сервер работает. Вот тут будет очень важно понимать что именно делает этот код, поэтому идём обратно в ChatServer и вспоминаем, что мы там написали. Ничего не написали, прямо сейчас -- методы старт и стоп ничего не делает. А если мы предположим, что хотим управлять сервером с графического интерфейса, то когда мы в графическом интерфейсе кликаем по кнопке старт или стоп у нас должны вызываться методы старт и стоп в классе чатСервера. А сам класс ЧатСервер - это у нас пока-что такая картонная дурилка, которая только логирует. Соответственно нам по кнопке Старт надо запустить поток ServerSocketThread а по кнопке Стоп - его остановить. +% 03 + +Нашему классу ChatServer понадобится экземпляр потока сервера. И давайте его по нажатию старта создадим. Дадим ему какое-нибудь имя, типа SeverThread и передадим в него порт. sst = new sst("st", port). Пробуем, и у нас бесконечно выводится сообщение о том, что работает поток. Интерфейс не залип, что говорит нам о том, что сообщение выводится из какого-то ещё потока. Думаю, если вы попробуете сделать тоже самое вживую, а не просто посмотрите на лекцию, будет более наглядно. +% 04 + +Но и остановить мы этот другой поток не можем. Нас это не устраивает, поэтому мы идём использовать второй вариант остановки - помните, вызывать метод стоп нельзя? мы серверный поток зацикливаем не по true, а пока поток не прерван. а по кнопочке стоп мы серверу скажем - прервись, пожалуйста. Вот, собственно, сразу живой пример управления потоком. Пример, который мы будем использовать. Это единственный штатный правильный способ тормознуть поток. Мы его должны как-то изначально правильно написать, чтобы он время от времени проверял флажок, и если флажок сбросился - поток должен как-то сам подумать, как ему перестать работать. \\ \hline + +09-13 & Чтобы на этот пример было приятнее смотреть, для начала разгрузим немного наш поток, чтобы он нам в логи не кидал постоянно сообщение о том, что он работает, вспомним, что у класса потока есть довольно много интересных методов, в том числе статических -- каждый поток можно заставить поспать. Например после вывода сообщения мы можем заставить наш поток спать какое-то время. Просто написав Тред.слип видим, что среда разработки нам наш метод подчеркнула. Метод sleep(3000) может выбрасывать InterruptedException. И вот этот InterruptedException выбрасывается, когда ваш поток спит, а ему кто-то снаружи делает interrupt. Обернём в трай-кэтч, залоггируем этот момент, и посмотрим, как это работает. Видим -- сообщение выводится каждые 3 секунды, всё хорошо, жмём кнопку остановки, сработал interrupt но флаг не сбросился и поток то не прервался. +% 02 + +Помните, я на лекции про исключения говорил, что самое плохое, что можно сделать с исключением -- это его проигнорировать или просто залоггировать? Вот конкретный случай, когда нельзя просто взять и залоггировать исключение, потому что мы не получаем ожидаемого поведения от потока. Нам надо либо самим сбросить флаг, либо ещё что-то сделать. Все прониклись проблемой? Получается, в кэтч поток может сам себе сделать приятно, мягко прервавшись, а в данном случае и это достаточно штатно -- можно брейкнуться из цикла, например, потому что после сна иногда ещё какие-то действия происходят. Для верности всё залоггируем и посмотрим, что при нажатии кнопки старта стартует поток, а при нажатии стопа поток останавливается. Оказалось не так уж и сложно. \\ \hline + +09-14 & Какие у нас проблемы ещё остались? Мы можем сколько-то много раз нажать на старт и насоздавать потоков. Эти потоки будут создаваться, а остановить мы сможем только крайний, потому что ссылка у нас останется только на него. Ещё раз, внимательно, мы стартуем поток, ссылка на него сохраняется в идентификаторе, вызвав старт ещё раз мы не смотрим, существует ли ссылка на поток в идентификаторе, и всегда просто создаём новый поток. Предыдущий созданный поток -- просто продолжает существовать и работать параллельно, но никаких способов его прервать у нас нет. Чтобы быть уверенными, что поток будет создан только один -- мы можем спросить у потока, а вообще, существует и живой он, или нет. Для этого есть прекрасный метод isAlive(). Внимательно смотрим на 7ю строку метода, запускающего поток. +% 02 + +Вроде всё круто, но у нас же изначально ничего в этой переменной не лежит, значит если мы на кнопочку нажмём - отвалимся с NullPointerException. Дописываем вторую проверку на null, и когда нажмём кнопку мы либо просто сообщим, что поток уже запущен, а в противном случае -- стартуем новый сервер, в принципе, больше ничего не нужно делать. +% 03 + +Обратная ситуация -- когда мы не стартовали поток, а просто жмём стоп, допустим, по ошибке -- получим неприятность -- снова отвалимся с NullPointerException. И правильно сделаем, ведь у нас же объект сервера никогда не создавался, но и мы в нашем изучении идём от простого к сложному. Думаем, в каком случае нам не нужно прерывать поток, в случае нажатия кнопки стоп? Если потока не существует, или он уже не живой. + +Получается, что если до старта жмём стоп, видим сообщение о том, что поток не запущен, стартуем, прерываем, и если ещё раз жмём стоп - в переменной не налл, но и поток не живой, так что пишет, что сервер не запущен. Конечно, ничего страшного не будет, если наш метод потыкат палочкой в мёртвый метод, но для порядка всё таки надо такие вещи ограничивать, потому что чаще всего мы хотим контролировать все аспекты существования объектов в системе. \\ \hline + +09-15 & Ну как, увлекательно пока что работать с потоками? Предлагаю проверить, как в целом усваивается. По традиции у нас три вопроса. Многопоточность – это возможность исполнять несколько программ параллельно; инструмент создания высоконагруженных приложений; инструмент создания иллюзии параллельности работы приложения. + +... 30 сек... + +Отчасти верно всё, но самое основное -- это создание иллюзии параллельности работы приложения. Несколько программ -- это многопроцессность, а высоконагруженность -- это то, что можно достигать в том числе многопоточностью, но не в первую очередь ею. + +Для запуска отдельного потока исполнения приложения необходимо создать потомка класса Thread; переопределить метод run() класса Thread; переопределить метод start() класса Thread. + +... 30 сек... + +Переопределение ран задаёт поведение потока, переопределение метода старт не имеет смысла, а вот вызов метода старт делает то, что нужно -- запускает поток. И раз запускать мы поток умеем, то чтобы остановить выполниение потока необходимо для объекта потока выполнить метод Thread\#stop(); переопределить метод Thread\#interrupt(); выполнить метод Thread\#interrupt(); + +... 30 сек... + +Стоп это запрещённый по понятным причинам метод, переопределение интеррапт не приведёт к нужному эффекту, а вот вызов метода интеррапт для объекта потока, предварительно верно спроектированным потоком -- то, что нужно. \\ \hline + +09-16 & Вернёмся к нашему созданному для примеров MyThread. Прерывание потока мы разобрали. Поговорили о старте, о том как можно поспать, осталось поговорить ещё об одном моменте в части базового менеджмента потоков. О том, как можно дождаться завершения потока. На слайде видно, что у нас был некий поток мейн, который в какой-то момент своей жизни запустил на параллельное исполнение ещё четыре потока, создав асинхронность выполнения программы. Можно, конечно сходить в википедию за определением, но я думаю, лучше сказать своими словами. Асинхронность -- это такое явление, которое позволяет говорить о независимости исполнителей, в нашем случае потоков. То есть временная шкала у них едина, но совершенно нет гарантий того, что потоки выполнятся хотя бы приблизительно одновременно. мейн так и вовсе продолжит выполнять свои инструкции сразу, как только отдаст команды четырём параллельным потокам на запуск, на слайде это место отмечено словом «сейчас». Предположим ситуацию, в которой мейн отдал параллельным потокам какую-нибудь сложную задачу. +% 02 + +Вот, предположим, у нас есть какое-то очень-очень большое изображение, и нам надо с ним что-то сделать, скажем, осветлить или наложить виньетку. Если мы будем это делать в одном потоке, значит мы последовательно возъмём каждый пискель и применим к нему соответствующий фильтр с соответствующим коэффициентом, получим довольно сложный цикл со множеством математики. А разбив на потоки, мы одновременно будем обрабатывать 4 картинки размером 5000х5000, получив таким образом такое-же обработанное изображение, но гораздо быстрее. Конечно, не в 4 раза, потому что нужно время на разбиение изображения, на синхронизацию, на создание потока. Какой поток когда будет работать - разруливает операционная система, мы на это не можем ни повлиять, ни отследить, а вот если мы запросим у ОС больше потоков, мы можем быть уверены, что она нам будет давать чуть больше процессорного времени, чем если бы мы не запрашивали потоки. +% 03 + +С точки зрения кода, мы теперь знаем как спать, поэтому можем сделать вид, что наш поток что-то тяжёлое делает. В МайТрэд мы заставляем поток спать столько секунд, сколько передадим в конструктор, но будем предполагать, что это он на самом деле делает что-то жутко сложное, например, накладывает на изображение виньетку. Вот здесь, кстати, пожалуй, единственный случай, где можно исключение проглотить. То есть ничего интеррапт не изменит, нам всё равно надо доспать своё время. Конечно, в этом случае исключение вообще невозможно, потому что мы интеррапт и посылать-то не будем, но вот, редкий слуйчай, когда исключение можно игнорировать. И, для наглядности, после сна залоггируем завершение потока, а перед сном - залоггируем его старт. + +Далее в мэйне запустим несколько, и в конце мэйна залоггируем завершение мэйна. Увидим, что у нас запустился мейн, запустил все остальные потоки и завершился, а побочные потоки продолжают делать свои важные дела ещё секунду, две, три и четыре, соответственно. Но ели бы мейн отдал побочным потокам на обработку части изображения, а потом их забирал обратно -- у нас бы ничего не заработало, потому что мейн то не дождался обработки изображения, завершился и более к активной деятельности не возвращался. +% 04 + +Так вот -- есть способ заставить текущий поток дождаться завершения какого-либо другого потока. делается это при помощи метода join(). Метод не даст завершиться потоку, к которому был присоединён другой поток, то есть, если мы начали что-то делать в параллельных потоках и просим мейн дождаться -- он просто будет спать, пока другие потоки работают. Мейн становится классическим начальником других потоков. +% 05 + +Этот метод тоже умеет генерировать InterruptedException. И генерится он если уже наш, текущий поток прерывают, а мы ждём завершения чьей-то работы. Логично, ведь если мы кого-то ждём, надо не просто прерываться, а что-то сделать с тем, кого мы ждём. Старуем и видим, что метод join действительно не отпускает наш мэйн пока не закончит свою работу второй, третий и четвёртый потоки, гарантируя последовательность событий. Это один из простейших и эффективных способов синхронизации потоков. + +В приведённом примере видно, что я заставляю первый поток сразу джойниться к мейну, без создания дополнительного идентификатора, но строка закомментирована. Такое поведение иногда бывает удобно, но чаще всего приводит к тому, что работа значительно замедляется, потому что мы сначала дождёмся выполнения задачи нулевым потоком, а потом уже будем создавать первый и остальные. С такими конструкциями при работе в многопоточных средах желательно быть внимательнее.\\ \hline + +09-17 & Если внимательно присмотреться, можно заметить, что у класса Thread есть конструктор, который на вход принимает некий странный Runnable(). А ещё сам тред тоже, оказывается, реализует Раннебл, это намекает нам на то, что раннебл -- это интерфейс. Пройдя прямо там, в коде, к документации на Runnable видим, что это и впрямь интерфейс, содержащий подозрительно знакомый метод run(), который мы усердно переопределяем последние несколько минут. Значит, делаем вывод, о том, что именно выполненное в методе run() и будет выполнено в отдельном потоке. Пазл сложился. +% 02 + +С точки зрения вызова, покажу на примере горячо всеми любимых анонимных классов. Я не великий поклонник таких этажерок, однако такие конструкции достаточно популярны и их можно довольно часто встретить в чужом коде. Как видно в выводе терминала в приложении создался новый поток безо всяких наследников майтред и прочих интересных сложностей. Сразу отвечу на невысказанный вопрос, когда использовать раннебл, а когда использовать тред. Если ваш отдельный поток должен хранить какое-то состояние внутри себя -- используйте тред, если вы запускаете какую-то задачу, работающую без хранения состояния -- используйте раннебл. \\ \hline + +09-18 & Перейдём к сложным моментам. Потоки работают совершенно асинхронно. Переключаются в какие-то неизвестные моменты времени, и вообще никак друг от друга не зависят. Пока потоки работают с какими-то своими данными -- никаких проблем нет. Проблемы возникают, когда потоки начинают работать с общими данными. Два самых страшных слова для многопоточного приложения -- это Race Condition и Deadlock. \\ \hline + +09-19 & Не будем далеко ходить. Простой пример, допустим в мэйне есть три переменных и некий метод incrementAll, который будет их как-то очень просто, но достаточно долго увеличивать обычным циклом. И в данной ситуации достаточно очевидно, что эти переменные должны увеличиваться равномерно и синхронно. Даже выведем их на экран по окончании цикла. Пока мы работаем в один поток - всё синхронно и замечательно. По сложившейся у нас неплохой традиции, предлагаю, раз оно так хорошо работает, всё сломать. Создадим вместо простого вызова метода Runnable который будет в своём методе run() делать одну вещь - вызывать наш очень хороший метод. И создадим пару новых потоков на его основе, и запустим их. +% 02 + +В одном потоке, даже если это будет не поток мейн, проблем не будет, мы также досчитаем до миллиона. Усложним задачу и создадим два потока. Тут сложность в том, что потоки начинают работать с одними и теми-же данными по очереди в определяемом операционной системой порядке и количестве времени. Получается полностью асинхронное исполнение. Запускаем, возможно, даже несколько раз и смотрим какие разные будут значения. Мы ни разу не получили значение 1 и 2 миллиона для первого и второго потока. Это называется "состояние гонки". Race Condition. Пожалуй одна из самых страшных вещей в многопоточном программировании. В этом рафинированном примере всё довольно очевидно, а если данных и их изменений поменьше, и потоков штук пять, то мы можем месяцами запускать программу, и она будет отрабатывать как надо, а потом бац и всё развалится, потому что потоки отработают не в том порядке. +% 03 + +Очевидно, что семантически нельзя допускать, чтобы этот метод выполнялся в разных потоках Это называется не потокобезопасный код. То есть никак нельзя двум потокам в этот цикл попадать. вообще никак нельзя. Почему? припоминаем, что класс хранится в куче вместе с его статическими переменными, а вот счётчик в цикле фор -- хранится на стеке, а значит изменяемые в цикле переменные -- общие для всех потоков, а счётчик у каждого потока свой. Но и тут есть подвох -- всё, что статическое и примитивное, на время исполнения и вычислений также помещается на стек, поэтому каждый поток может не успеть сохранить результаты своих вычислений, из-за этого и возникает гонка -- то значения обгоняют счётчик, то счётчик значения. На слайде я постарался показать, что примерно происходит в памяти, но важно помнить, что это очень приблизительный пример, в котором для простоты демонстрации используются равные промежутки времени работы потоков и счёт идёт до малых значений, в то время как в показанном ранее примере в коде мы видели реальный разброс значений. Можем немного внимательнее изучить этот слайд и проследить, когда и какие значения успели сохраниться в кучу, а какие нет. +\\ \hline + +09-20 & Существуют способы синхронизации потоков. Так называемые "критические секции". Вы в коде выделяете критические секции +% 02 + +У каждой секции есть объект монитора. +% 03 + +К нашему методу прилетело несколько потоков, которые хотят его выполнить +% 04 + +Случайный поток, нам сложно предсказать какой, в зависимости от приоритета и ещё кучи факторов захватывается этим монитором +% 05 + +Поток отправляется в критическую секцию в которой существует наш метод и начинает его усердно выполнять. Остальные потоки встают в очередь и тупо ждут. +% 06 + +Как только первый поток выходит из критической секции он отпускает монитор, его захватывает какой-то другой случайный поток, и дело повторяется. Получается блокировка по монитору. Очень простой и очень надёжный механизм. +% 07 + +Естественно, что критических секций может быть много, и в каждой может быть как один метод, так и несколько, тут уж что напрограммируем. +% 08 + +Этот способ часто применяется не только для синхронизации потоков, но и для синхронизации процессов, например какая-то программа пишет лог файл, а другая программа должна читать лог файл, и она такая смотрит "аа, нельзя, только один процесс может файлик этот использовать"\\ \hline + +09-21 & Отсюда возникает абсолютно логичный вопрос: откуда взять этот монитор. А очень просто, монитором может выступать совершенно любой объект. То есть это реализовано на уровне языка. А раз монитором может быть что угодно, осталось понять, как описать критическую секцию. Допустим мы хотим, чтобы в наш цикл внутри метода мог одновременно попасть только один поток, мы пишем ключевое слово synchronized() и в фигурные скобки оборачиваем критическую секцию. А в круглые скобки мы записываем объект, который будет выступать монитором. И всё. Вот так просто. Получился такой пример глючного кода, который мы почти вылечили синхронизацией. Почему почти? В результате запуска хорошо видно, что мы очень близко к нужному результату, но не достигли его. + +Как вы думаете, почему результат именно такой? ... 30 сек... +% 02 + +На самом деле, первый поток завершил работу в критической секции на миллионной итерации цикла, но пока он переходил к следующей инструкции и формировал строку на вывод -- второй поток уже успел захватить монитор и немного изменить переменные, находящиеся в общей памяти, поэтому у первого потока получились неверные данные, а второму потоку уже никто не мешал и у него данные получились хорошие. Исправить ситуацию несложно -- синхронизировать не только цикл, но всё тело метода. Так мы логично подошли к ещё одной популярной конструкции.\\ \hline + +09-22 & На самом деле, можно и не создавать отдельный объект. Для этого достаточно вспомнить, что вообще всё в джаве -- это объекты. даже классы, поэтому, возможно устроить синхронизацию по this. И ничего страшного, если монитором будет выступать текущий экземпляр, даже если мы не создавали его явно, его создаст виртуальная машина для своих служебных нужд или даже не экземпляр приложения, а сам класс программы. В данной ситуации мы не пускаем другие потоки во весь метод целиком, значит мы можем объявить весь метод как synchronized и это будет синхронизацией по this. Важно помнить, что если мы сделаем два синхронизированных по this метода, то у нас поток заберёт монитор одного из методов, и во второй метод тоже никто не попадёт. У них будет один монитор на двоих. Чудеса многопоточности, будьте внимательны. +% 02 + +Допустим, перед нами чужой код. Возможно, даже код стандартной библиотеки языка джава. Как понять, является-ли тот или иной метод потокобезопасным? Очень просто -- почитать документацию. берём любой метод, например, хорошо известный принтлн и читаем, если нигде не написано, что он Thread-safe - считаем его не потокобезопасным. И не можем к нему обращаться из разных потоков. Если вдруг внезапно нет документации под рукой - лезем в исходник и первое что видим - synchronized(this). Засинхронен. Явно потокобезопасный. Но если доков нет, исходников нет, и сами на первый взгляд мы оценить не можем - не имеем права считать метод потокобезопасным. \\ \hline + +09-23 & Снова возникает логичный вопрос, почему бы не сделать вообще всё синхронизированным и потокобезопасным? Если всё объединять под один монитор, очевидно, что все потоки выстроятся в очередь и будет обычное последовательное исполнение программы, а всё объединить разными мониторами нельзя, потому что мы вполне можем попасть в ситуацию Dead Lock когда один поток будет ждать пока мы отпустим монитор одного метода, а в этом методе будет сидеть поток, который будет ждать, пока мы отпустим монитор первого метода. И всё, намертво. Таких ситуаций, конечно-же надо избегать, это результат неправильной архитектуры приложения. Внимательно присмотритесь к коду на слайде и проследите, кто кого ждёт и почему не может дождаться. + +... 30 сек... \\ \hline + +09-24 & \\ \hline +09-25 & \\ \hline +09-26 & \\ \hline +09-27 & \\ \hline +09-28 & \\ \hline +09-29 & \\ \hline \end{longtable} - - - +Ну и на сегодня предлагаю закончить. На мой взгляд многопоточность - это самое сложное, что есть в программировании вообще. С этим надо быть жутко аккуратным. Пока вы программируете в одном потоке - ваши баги будут повторяться при одинаковых условиях, а в многопоточных системах баг может годами не проявляться, а потом вдруг звёзды сошлись, потоки запустились в той последовательности, в которой им запускаться было не надо, случился DeadLock и всё, думай-гадай где он случился. \end{document} -Когда Вы создаёте приложение на любой платформе у вас создаётся процесс, и как минимум создаётся один поток (мэйн). В джаве таких потоков для каждого процесса создаётся несколько - Main, Error, GC, EDT. Поскольку процессоры у нас ещё не очень хорошо работают с параллельной логикой - создаётся псевдо-параллельность, то есть какое-то количество времени исполняется один поток, какое-то другой, потом снова первый и т.д. И поскольку процессоры сейчас с бешеными гигагерцами, то для человека создаётся впечатление, что это происходит параллельно. Технология называется HyperThreading. Так вот, когда завершается процесс? Когда завершается последний поток. А дефолтный обработчик исключений висит не на процессе, а на потоке. Давайте сделаем так: - psvm - thr new RuntimeException("Hello from main"); - Всё очевидно и прямолинейно, случилось исключение в главном потоке, уронили приложение, всё, как и планировалось. А теперь давайте внутри потока EDT попробуем упасть. Всё, что мы начали в этом потоке (конструктор) и всё что внутри конструктора описано - будет в этом потоке. А мэйн будет завершён. Чтобы было показательно - на какую-то кнопку повесим thr new RuntimeException("hello from EDT"). - В потоке EDT исключения внезапно ведут себя по-другому. Оно происходит, выбрасывается, но приложение то не падает. Видите, жмём на кнопку, он выбрасывает исключение, и мы всё равно можем нажать на кнопку снова. - Это всё консоль, среда разработки, понятно, мы всё видим. А как вы думаете, что будет, если мы создадим jar файл, и запустим наше приложение отдельно от среды разработки? Вариантов может быть масса, а вы как думаете? Проще всего взять и проверить. (OpenModuleSettings - Artifacts + JAR + fromDependencies - ServerGUI). Делаем ставки. Я думаю тут два варианта - ничего не произойдёт, или упадёт. Второе - лучше. Надо объяснять почему? (что-то в приложении произошло, а мы не увидели). Прониклись проблемой? + Есть третий способ создания потоков - через ExecutorService. Он позволяет быстро создавать потоки, отдавать им задачи и не терять время на подготовку потока к работе, но об этом на следующем занятии. Если вкратце то Вы сразу создаёте некий пул потоков и они уже где то рядом с программой существуют, ожидая задачи и запуска. + Бывают SingleThreadPool с одним потоком, + FixedThreadPool с фиксированным количеством (если больше то ждём пока освободится), + и CachedThreadPool с заранее подготовленным количеством потоков, но умеющий расширять свой пул по необходимости. Явный его минус в том, что мы не можем ему указать потолок потоков. То есть если в какой то момент у Вас прилетит 20к параллельных запросов то вы создадите 20к потоков, и никакие из них не поставите в очередь. + И соответственно не можете созданные потоки удалить, то есть после всплеска они будут висеть неиспользуемые, а потоки, которые висят в пулах считаются активными потоками программы, даже когда они уже отработали. Но об этом подробно на следующем занятии. +***** ExecutorService + Thread Pool's + Давайте посмотрим в целом обзорно на общую идею, как ими пользоваться. + Используем Executors в качестве фабрики для создания ExecutorService'ов + ExecutorService serv = Executors.newFixedThreadPool(4); + for (int i = 0; i < 10; i++) { + final int w = 1; + serv.execute(new Runnable() { + @Override + public void run() { + System.out.println(w + "begin"); + try { + Thread.sleep(100 + (int) (3000 * Math.random())); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(w + "end"); + } + }); + } + Скажем, что у нас есть только 4 потока, и 10 задач, обратите внимание, что задачи будут выполняться не более, чем по 4 в каждый момент времени. + А теперь вопрос: никто ничего странного не замечает? Приложение не завершилось. Это говорит нам о том, что потоки активны, и ожидают других заказов. То есть если бы потоки завершались и каждый раз стартовали по новой - это ничем не отличалось бы от обычного тред.старт. Поэтому надо всегда делать завершение сервиса + serv.shutdown(); + Это говорит что надо завершить рботу сервиса когда все активности в потоках завершатся. по сути, это запрещает отдавать сервису новые задачи, и закрывает его когда выполнение задач заончится, а потоки будут уничтожены. + serv.awaitTermination(); + ожидание окончания работы всех потоков, то есть по сути это что то вроде джоина для сервиса пулов. +***** Вспомним то что мы знаем + Все ли помнят что такое .join(); + Runnable r = new Runnable() { + @Override + public void run() { + System.out.println("Runnable"); + } + }; + + Thread ex1 = new Thread(r); + Thread ex2 = new Thread(r); + + try { + ex1.join(); + ex2.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Берём код и вопрос: Одновременно-ли выполнятся джоины? (как только первый джоин отпустит мы пойдём во второй). То есть он будет работать в том самом методе, в котором вызван и ждать потока, для которого вызван. +***** Про синхронизацию помним? + Cвоего рода блокировка каждого потока и ожидание пока многопоточное приложение отпустит тот или иной метод. Что будет выведено в результате работы этого кода? + Counter c = new Counter(); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + c.inc(); + try { + Thread.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + Thread t2 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + c.dec(); + try { + Thread.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + try { + t1.start(); + t2.start(); + t1.join(); + t2.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("counter = " + c.getC()); + Верно говорить, мы не знаем, потому что в методы инк и дек мы будем заходить в неконтролируемые многопоточные отрезки времени. Происходит так называемое состояние гонки (race condition) Возникает оно когда два отдельных потока пытаются постучаться в одну и ту же ячейку памяти, а операции инкремента и декремента не атомарные, поэтому потоки сначала выполняют геттер, смотрят какое значение увеличивать или уменьшать, в это время другой поток отнимает доступ и смотрит на значение которое увеличивать или уменьшать ему, ну и они соответственно делают это для собственных значений. Тут и нужна синхронизация, которая не даст двум потокам одновременно залезть в одну ячейку и изменить в ней значения. + Соответственно, добавив в методы синхронизацию мы гарантируем отсутствие таких ситуаций. +***** С помощью чего можно синхронизироваться + Когда мы пишем synchronized метод - что будет являться монитором? (объект у которого вызван метод) Тема мониторов не могла не рассматриваться на втором уровне в районе пятого урока. Какая важная особенность работы мониторов стоит того чтобы о ней упомянуть? то есть монитор пропускает в себя только один тред, поэтому если разные треды будут стучаться в разные методы, контролируемые одним монитором - они будут дожидаться, пока монитор отпустит тред. + Второй вариант - создать некоторый объект, который может быть монитором и внутри метода написать sunchronized(obj){...} что нам это даст? Возможность например создать не синхронизированный участок метода, который будет выполняться с риском получения состояния гонки. Но такой подход нужен если нам надо дать возможность в рамках одного класса дать возможность параллельно исполнять разные методы. + Третий вариант - что выступает в роли монитора, ели мы ставим синхронизацию на статический метод? (сам класс). Соответственно, если есть статический синхронизированный метод и нестатический синхронизированный метод, их вызов в двух потоках будет работать параллельно или последовательно? (параллельно, статик по классу, нестатик по объекту этого класса). +***** Примерчики + У меня тут внезапно оказался примерчик набросаный по тредам и синхронизациям, можете его поразбирать, а если считаете, что он сложный - сотрите и никогда не открывайте. Суть в чём, четыре человека пытаются сеть на два стула, причём первые три пытаются сесть на первый стул, а четвёртый на второй. Увидим, что первые трое никогда не сядут на один стул одновременно, а четвёртый спокойно сядет на второй стул и спокойно с него сойдёт. Не удастся найти момент где два человека сидят на одном стуле. + Смотрим на второй пример IntegerMonitor, и думаем, как выполнятся методы, последовательно, или параллельно? (параллельно) Почему? Проведём эксперимент - закомментим n++. стали работать последовательно. В чём причина? Причина в использовании класса Integer. Его объекты неизменяемы, так что когда мы инкрементируем мы создаём новый объект и кладём его по той же ссылке. Второй поток видит незанятый никем монитор и захватывает его. Получается, несмотря на то, что они все засинхронизировались на одну переменную - они захватили три совершенно разных объекта. А по примитиву мы засинхрониться не можем. +***** Dead Lock + Ситуация при которой два потока начинают ждать друг друга и никогда этого не дождутся. Есть такая интересная история, когда приходит программист устраиваться на работу, и ему задают последний вопрос "мы примем Вас на работу если Вы расскажете нам, что такое Deadlock" на что он отвечает "я расскажу что такое deadlock как только вы возьмёте меня на работу". + Итак на примере мы видим, что первый поток захватывает первый монитор, и через секунду захочет захватить второй. Параллельно с ним второй поток захватывает второй монитор и через секунду захочет захватить первый. Таким образом первый поток будет ждать второй монитор, чтобы отпустить первый, а второй поток будет ждать первый монитор, чтобы отпустить второй. И вот они будут друг друга ждать, и никогда не дождутся. +***** Остановка потока + допустим у нас есть поток, внутри которого есть бесконечный цикл, и мы этот поток стартуем. как его остановить? Что плохого в t.stop(), почему он deprecated? (потому что никто снаружи никогда не знает что там внутри потока происходит и в каком состоянии поток будет когда мы его остановим) + Хороший вариант это interrupt давайте напишем, но почему не прервалось? (потому что кажется надо как то цикл переработать - например поставить в условие !Thread.currentThread().isInterrupted()). так всё равно по очевидной причине не сработает, но флаг нам действительно надо проверять, внутри цикла можем поставить условие (if (Thread.curr.isInterr()) break;) Ключевое отличие от стопа в том, что поток внутри себя знает когда он останавливается. И всё равно поток не останавливается, что же такое, сломали джаву? А дело всё в этом самом InterruptException, он говорит о том, что пока нельзя поток остановить - кто то всё же пытается. Получается ситуация когда нам нельзя просто брать и не задумываясь делать e.printStackTrace. И нам надо постараться в этой обработке написать какую то логику, в которой будет умная остановка потока. + Когда мы будем смотреть на всякие сервисы интересные - там будут два шатдауна - один который просто остановит сервис, а второй который ещё и всем потокам посылает интеррапты. но это посмотрим позже. +***** Демоны + Демон-поток - это поток, который не предотвращает выход JVM, когда программа заканчивается, но поток все еще работает. Примером для потока демона является сбор мусора. + В принципе потоки демоны это такие потоки, которые в принципе не жалко потерять. Например запустил пользователь радио, мы ему отдали тред с трансляцией, ну а когда закрыл - то закрыл, нам ничего не надо с программой при этом делать. + Вопрос такой - вот есть поток Т, внутри себя он запускает ещё поток-демон. Если мы завершим поток Т, но сделаем мэйн бесконечным - демоны остановятся? Демон будет работать пока хотя бы один поток программы будет работать. +***** Wait Notify + Никогда не понятно, как правильно говорить об этих двух механизмах и в каком порядке, но как то надо. Давайте я попробую объяснить, а Вы потом скажете, понятно или не понятно. + Представляем, что у нас есть три потока, засинхроненных по одному монитору. Каждый поток запускает какой нибудь маленький цикл внутри которого печатает свою букву. Вопрос: если их запустить параллельно все три - каковы шансы, что в консоли мы увидим ABC-ABC-ABC-ABC-ABC? (По хорошему это где то в районе тысячной процента). Гораздо более вероятно если напечатается 5A-5B-5C, потому что довольно маленькие задачи и нет смысла дробить их на части многопоточностью. + Ну и идея вэйт-нотифая в следующем - каждый поток пытается засинхронизироваться по монитору, и если он видит, что сейчас не может напечатать свою букву - переходит в режим ожидания. Такая же ситуация со вторым и третьим потоком. Затем, когда мы всё таки находим поток, который напечатает свою букву - мы печатаем букву и будим все остальные потоки. Возникает вопрос - как же три потока смогли зайти в один монитор? мы же вроде бы вошли в монитор и не можем уже другим потоком в этот монитор войти. Так в чём получается смысл всех этих вэйтов? Когда мы вызываем у монитора метод вэйт - мы отправляем поток в сон, и высвобождаем монитор. А нотифайОл будит все потоки, привязанные к этому монитору. Обратите внимание, что тут не иф, а вайл, потому что в случае, если кто то разбудит этот поток снова не в его время - ему опять надо будет отправиться спать, а не печатать свою букву невовремя. + Обычно это используется для проверок и реализации очередей. Допустим у нас есть очередь, которую мы заполняем некоторыми объектами, и очередь будет у нас монитором, с другой стороны этой очереди объекты будут выкидываться с какой то другой скоростью. Это можно реализовать заставив производителя поспать миллисекунду, и проверить, не надо ли положить новый объект в очередь. Это весьма нагрузит процессор. Собственно, чтобы не тратить ресурсы мы заполняем очередь и говорим производителю ждать, а когда с другой стороны объект забрали - говорим нотифай. Стандартная ситуация паттерна producer-consumer. + Одиночный нотифай будит один поток, первый в очереди потоков. Но поскольку мы не знаем какой из потоков в очереди стоит - нельзя однозначно сказать какой проснётся. Поэтому это используется обычно только когда у нас есть два потока и не более. В документации также сказано что надо вэйты делать в цикле, потому что по какой тонепонятной причине поток может быть разбужен системой. + + Сегодня будем продолжать говорить о всяких классах которые помогают нам упростить жизнь, чтобы напрямую не использовать всякие мониторы, а как то удобнее работать со многопоточностью. +***** На прошлом уроке + Мы с вами рассмотрели Single ThreadPool, FixedThreadPool, CachedThreadPool. Давайте подробно разбираться. + Для создания пулов потоков мы используем ExecutorService pool = Executors.newFixedThreadPool(4); + Когда мы создаём фиксированный пул мы указываем что он подготавливает для нас 4 потока. И считается, что они будут активны, пока я не сделаю shutdown. А когда они активируются? Когда я создаю пул, или когда я даю им задачи? Давайте попытаемся ответить на этот вопрос. Обратите внимание, что у класса Executors есть два метода для создания например фиксированных пулов, один просто принимает количество нужных потоков, а второй может принять так называемый ThreadFactory. Когда пулу потоков нужен новый поток он использует некую фабрику, для его создания. Вообще по умолчанию там работает стандартная фабрика, но мы можем подкинуть туда и свою. Сделаем это и увидим, что тут есть в интерфейсе один метод newThread который принимает раннебл, то есть задачу, и отдаёт таки тред. То есть самый простой вариант - это отдать оттуда return new Thread(r); + Давайте попробуем поделать что то более интересное + Thread t = new Thread(r); + t.setDaemon(true); + t.setPriority(8); + return t; + Получится, что все потоки, которые будут созданы в этом пуле будут выполнять свою задачу, станут демонами и будут иметь приоритет 8. Получается своего рода преднастройка. Тут то мы можем и посмотреть, когда же создаётся новый тред, уберём демона и выведем в консоль надпись о создании потока. Запускаем и как видите консоль пустая. +***** Пробуем заглянуть внутрь + pool.execute(() -> System.out.println("3")); + вызываем пять пулов и видим, что создалось 4 новых треда, и пятый вывод произошёл уже без создания. Видим что действительно не создаются потоки сразу, действительно используется некая фабрика, и действительно тредов получается фиксированное количество. + вызов метода shutdown() закроет все неактивные потоки, дождётся завершения активных и их тоже закроет + метод shutdownNow() закроет все неактивные потоки, а у активных вызовет interrupt(); + проверка isTerminated() говорит о том, уничтожен ли пул. То есть является ли он шатдаун и без активных потоков + проверка isShutdown() говорит о том, является ли сервер в состоянии выключения + awaitTermination(). Если Вы хотите подождать чтобы работу закончил какой то один поток - вы делаете join, если хотите дождатьвся всех - вызываем этот метод. + execute даёт пулу задачу. Кроме этого есть вариант вызвать submit() если нам надо получить из потока результат. Раннебл это интерфейс с войд методом ран, а тут возвращается некий дженерик Фьюче. + Интерфейс Callable это дженерик, который указывает какого типа данные будут возвращены в результате работы потока. Теперь вопрос - как же нам этот результат поймать? + Как мы могли заметить метод submit возвращает объект типа Future. Он нужен для хранения того, что вернёт метод. и мы можем у этого самого объекта взять и запросить результат методом result.get(). Ну вот, а что будет, если сделать Thread.sleep(5000). Если задача выполняется 5 секунд, как я могу получить результат уже на следующей строке. На самом деле тут произойдёт что то вроде джоина, то есть мы тут зависнем до того момента как нам не прилетит ответ от треда. ну и что также очень удобно - метод перехватывает все возникшие в процессе исполнения тредом задачи исключения. Соответственно предварительно мы можем проверить, отменён ли поток и завершена ли его задача методами isDone() и isCancelled(). +***** invokeAll/Any + Допустим у Вас есть список каких то задач, и их очень много, вы можете создать коллекцию этих задач, пулл потоков постарается эти задачи раздать своим потокам, и на выходе у пула запросить список результатов выполнения этих задач в виде списка фьючерсов. или, например, есть задачи, они где то собраны и они совершенно равнозначны, поэтому мы можем их выполнять, например в фоне, по одной раз в 10 секунд, отдаём коллекцию тасков методу invokeAny() и он будет их потихоньку разгребать. +***** ScheduledExecutorService + Очень часто складываются ситуации, когда надо опрашивать какой нибудь сайт раз в 10 секунд, и для этого надо написать метод, который будет запускать поток, в котором будет делать опрос, потом спать. Или, например, сам метод должен вызываться раз в 10 секунд, и это уже другой поток с другим поведением, или опрос раз в 5 секунд каждую нечётную минуту, и это третий поток с третьим набором Тред.слип()ов. Вот чтобы упростить такие мелкие но муторные задачи и придумали ScheduledExecutorService. Для его создания также используется класс Executors#newScheduledThreadPool(4). и вот здесь есть несколько вариантов того как мы можем отдать сюда задачу: просто запланированную, запланированную с рейтом и запланированную с задержкой. +***** Методы запуска + Простая задержка. Мы говорим, что у нас есть задача, и мы хотим её выполнить, но не прямо сейчас, а через 50 минут, например, указываем задержку и единицу измерения задержки. + Добавление фиксдРейта - это значит. что задача будет не только сдвинута во времени, но также мы указываем через какие промежутки времени надо её повторять. Если задача будет выполняться дольше, чем установленный промежуток - никакого промежутка времени бездействия не получится, и задачи просто будут выполняться друг за другом, накапливая несостыковку по времени. + По большому счёту - фикстДелэй делает тоже самое но с небольшим отличием, которое позволяет нам не думать о накоплении времени перекрытия задач. + +____.==__.==__.==__.==__ FixedRate + 0 10 20 30 40 + + +____.==_10_.==_10_.==_10_. FixedDelay + 0 10 15 25 30 40 45 + То есть либо через равные независимые промежутки времени, либо сервис смотрит чтобы между окончанием задачи и началом следующей были равные промежутки времени. +***** О коллекциях + Напоминаю, что методы и вообще данные бывают синхронизированные и несинхронизированные. Коллекции - не исключение (забавная получилась игра слов). Если вы попытаетесь взять например ArrayList и попытаетесь его модифицировать то с очень большой долей вероятности вы получите ConcurrentModificationException. Из названия очевидно что это что то связанное с неправильным параллельным изменением коллекции. В случае ArrayList у нас есть такая замена как Vector - это тот же самый ArrayList только он синхронизирован. Понятно, что мы не первые столкнулись с задачей хранения каких то данных в многопоточных приложениях, поэтому фрейморк коллекций предоставляет довольно много синхронизированных аналогов обычных коллекций. давайте на некоторые из них посмотрим. Полный список и интересное чтиво на ночь можно найти в пакете java.util.concurrent.* +****** CopyOnWriteArrayList + Про то что бывают синхронизированные эррэйлисты я сказал, это класс вектор, а ещё есть класс вот с таким сложным называнием которое намекает нам на то что он в глубине души - эррэйлист. Судя по тому, что находится он в пакете concurrent - заточен он под работу в многопоточных приложениях. С одной стороны это хорошо, вроде как избавляет нас от большого количества проблем, а с другой надо быть с этой коллекцией весьма осторожным - смотрим на название - Копировать-При-Записи. То есть все могут свободно читать из этой коолекции, но при любой модификации этого списка - будет сделана его копия. Зачем? Чтобы вообще никак не мешать тем, кто читает. То есть если предполагается большое количество записей - может получиться слегка мусорно. +****** ArrayBlockingQueue + Это естественно дженерик который можно использовать очевидно как очередь. Тут при создании надо указать размерность очереди, динамики как мы в качестве примера это делали на уроках по алгоритмам и структурам данных не будет. Интересная особенность в том, что мы можем класть в эту очередь элементы четырьмя разными методами, и убирать их из очереди также четырьмя разными методами. + add бросает исключение если очередь полная, а мы хотим добавить следующий элемент. + offer вернёт булево - добавилось значение в очередь или нет. + У оффера есть перегрузка с таймаутом, то есть вы можете сказать в течение какого времени метод должен пытаться класть в очередь значение, после чего вернёт своё булево, говорящее об успехе. + put вешает поток и ждёт пока место в очереди не освободится. То есть фактически поток переходит в режим ожидания свободной ячейки в очереди. + Для вывода элемента из очереди есть соответствующие четыре метода + remove - если в очереди нет объектов бросает исключение + poll с перегрузкой указывающей таймаут - либо вернёт объект, либо null. по таймауту аналогично + take фактически вешает поток который спрашивает данные из очереди, и не отпускает его до появления данных. + Если посмотреть внутрь методов, которые переводят потоки в режим ожидания, то там работает механизм вейт-нотифай, который мы рассматривали на прошлом уроке, получается, что эти методы постоянно шлют в очередь оповещалки о том, пора ли класть или забирать объекты из очереди. +****** HashMap + Там внутри есть некоторые корзины, в которые кладутся значения по хешам и ключам, и соответственно, если два потока захотят одновременно что то положить в одну из корзин внутри мэпа - вполне можно получить RaceContition. Что же делать, если я хочу чтобы с моим мэпом работало много-много потоков? мы делаем такую вешь, как ConcurrentHashMap. Тут методы такие же как в обычном ХешМэп, пут и гет, но судя по названию, этот класс приспособлен к работе в многопоточных средах. Там внутри хитрая синхронизация, которая для чтения ничего не блокирует, а для записи блокирует только ту корзину, в которую будет производиться запись (но делает это и для чтения в том числе), то есть при очень удачном стечении обстоятельств, если внутри мэпа 16 бакетов, то мы можем работать с ним в 16 потоков. Бакеты назначаются системой и мы на это особо не влияем. +****** Collections.synchronizedMap() + Есть ещё один способ работы с мэпами - Map m = Collections.synchronizedMap(new HashMap<>()); Что с ним? вроде синхронизация, вроде многопоточность, зачем было придумывать что то ещё? Созданный таким образом мэп совершенно не оптимизирован. то есть если кто то читает - весь мэп полностью заблокирован. Ничего не сломается, но будет очень медленно. +***** Попробуем придумать какую нибудь задачу + ExecutorService serv = Executors.newFixedThreadPool(10); + for (int i = 0; i < 10; i++) { + serv.execute(() -> { + System.out.println("1"); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println("2"); + }); + } + serv.shutdown(); + Что может быть плохого в такой конструкции? давайте представим, что вместо сна происходит доступ к каким то общим данным. Пусть, например, это будут модули, которые пишут очень большие файлы на флешки, одновременно. Один-два-три файла записать параллельно - не сложно, но если это будет сотня-две-три файлов то это может значительно снизить скорость, потому что ОС нужно будет постоянно переключаться с одной задачи на другую и на это будет уходить больше времени, чем мы выиграем от параллеьности. Такое вполне может случиться, потому что ОС требуется время чтобы переключить контексты. Получается у нас есть сервис из десяти потоков, но чтобы не напортачить со скоростью доступа - нам надо сказать, что задачей копирования файлов должны заниматься не больше допустим четырёх потоков. С помощью вэйт-нотифаев это сделать довольно сложно, отому что там надо будет заводить какие то счётчики, как то их сложно проверять, а это тоже драгоценное процессорное время. На помощь приходит такой интересный класс, как Semaphore smp = new Semaphore(4); и в конструкторе мы указываем сколько у нас будет одновременных доступов. Теперь чтобы применить это к нашему сервису - мы в начале раннебла говорим smp.acquire(); и таким образом захватываем семафор, по сути это что то вроде захода в блок синхронизации, только для 4х потоков. Каждый поток захватывает по монитору внутри семафора, и когда их не остаётся - новый поток ждёт пока семафор не освободится. Соответственно надо не забывать его освободить после того как мы поработали smp.release(); который вообще желательно ставить в файналли, потому что кто его знает, что может произойти в трае, мало ли не отпустим семафор +***** Потоков у нас может быт сколько угодно + и разбросаны они могут быть неизвестно где, так что надо как то уметь с ними со всеми справляться. Например, надо их все заджоинить. Та ещё задачка, ведь не факт, что мы их создавали и у нас есть на них ссылки. Чтобы все их поймать нам понадобится так называемая защёлка. CountDownLatch cdl = new CountDownLatch(10); и в этом виде она рассчитана на 10 щелчков. что это значит? запустим 10 хитро придуманных тредов которые просто будут печатать строки и жить они будут разное количество времени. + CountDownLatch cdl = new CountDownLatch(10); + for (int i = 0; i < 10; i++) { + int w = i; + new Thread(() -> { + System.out.println(w + "-1"); + try { + Thread.sleep((int) (Math.random() * 5000)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println(w + "-2"); + }).start(); + } + ссылок на эти потоки у нас нет, поэтому заджоинить их классическим способом не удастся. поэтому мы в конце выполнения потока делаем cdl.countDown(); что сократит количество оставшихся щелчков защёлки на один. А вот где то в мэйне или любом другом классе я могу сказать cdl.await(); что заставит поток дождаться пока все щелчки не закончатся. По сути мы получили джоин для десяти потоков. Внутри каждого потока мы сказали что надо произвести щелчок защёлкой, а мэйну сказали - жди, пока значение в защёлке не станет нулём или не сработает таймаут из перегруженного метода ожидания await(10, TimeUnit.HOURS). Соответсвенно защёлки это одноразовые объекты и для того чтобы перезапустить защёлку - надо её заново пересоздать. +***** Модульность + Бывают ситуации, когда у нас программа состоит из нескольких модулей, и нам надо дождаться, пока все они загрузятся, чтобы продолжать работу программы. Как бы нам так хитро засинхронизировать много разных элементов, если один может грузиться пол секунды, а другой двадцать секунд? Для проверки сделаем что то похожее на предыдущий пример + for (int i = 1; i < 11; i++) { + int w = i; + new Thread(() -> { + System.out.println(w + "-старт"); + try { + Thread.sleep((int) (Math.random() * 5000)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println(w + "-готов"); + System.out.println(w + "-стоп"); + }).start(); + } + Как бы нам сделать так, чтобы все потоки одновременно написали "стоп"? Для таких задач существует так называемый циклический барьер. При создании которого Вы говорите на сколько потоков он рассчитан. И в потоке между готовностью и остановкой говорите cb.await(); Идея в том, что каждый поток делает эвейт и снижает цифру в барьере на один, и переходит в режим ожидания. Когда последний десятый поток переводит барьер в 0 - метод отпускает все остальные потоки и они идут дальше. Получается, что таки образом мы дожидаемся всех. Здесь важно то что класс цикличный, так что если вы дождётесь 10 потоков, а потом каким то 11-м сделаете эвейт, то этот подсчёт ожидания пойдёт по новой. То есть если мы поменяем цикл до 21 то увидим 10 разнородных запусков и одновременных эндов, и потом снова 10 разнородных запусков и одновременных эндов. +***** Блоки synchronized + Не всегда бывают удобны, потому что если какой то поток подошёл к такому блоку, ему некуда деваться, он будет сидеть и ждать пока там монитор отпустит секцию. Как альтернатива предлагается такая сущность, как замОк. + ReentrantLock rl = new ReentrantLock(); + По сути они заменяют блоки синхронизации, допустим у нас есть два потока, в которых мы делаем синхронизацию при помощи замков + new Thread(() -> { + rl.lock(); + System.out.println("1"); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println("2"); + rl.unlock(); + }).start(); + new Thread(() -> { + try { + rl.lock(); + System.out.println("3"); + Thread.sleep(2000); + System.out.println("4"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + rl.unlock(); + } + }).start(); + В этом случае мы по идее никогда не должны увидеть одновременно 1 и 3. как если бы там была обычная синхронизация по монитору. Но тут есть такой сомнительный момент. Если блок синхронизации у вас всегда захватывается потоком, и им же отпускается, то замок вы можете захватить в одном месте, а отпустить вообще неизвестно где. Для того чтобы гарантированно отпускать замки желательно их класть в файналли. Кроме этого сомнительного плюса есть ещё один - метод tryLock(). Это попытка засинхронизироваться. То есть если замок занят - мы ничего не делаем, возвращаем фолс. а если мы туда зашли - то вернули тру и заняли блок синхронизации. Также можно пробовать захватить замок с таймаутом, то есть пытаемся 10 минут, если не удалось - перестаём пытаться. То есть если заменить лок на if(rl.tryLock()) то мы напечатаем только одну пару, а второй поток туда даже не полезет. или же мы говорим что второй поток у нас не так уж и торопится и может пытаться захватить секцию 5 ближайших секунд. if (rl.tryLock(5, TimeUnit.SECONDS)) + То есть это один из механизмов которые могут спасти вас от состояния DeadLock. +***** Ещё один вариант замка + ReentrantReadWriteLock + Внутри него живут несколько замков - это readLock() и writeLock() которые вы можете захватить и освободить независимо друг от друга. И в принципе вы пользуетесь ими не особо вникая как они устроены внутри. Идея в том, что вы можете работать с каким то ресурсом, читать и изменять его. Что может быть плохого в том, что пять тредов будут одновременно читать оттуда данные? (ничего) Соответственно, если кто то захочет что то в этот ресурс записать пока другие из него читают - надо ли запретить такое поведение? (да) И собственно так это и работает - не даст записывать пока кто то захватил замок чтения. и таких захвативших может быть сколько угодно, И обратная ситуация - пока кто то пишет - не даст читать оттуда данные, но писать может только кто то один. + А может ли сложиться ситуация, что мы хотим что то записать, ждём пока ресурс освободят читающие, и пока мы ждём туда прилетят другие читающие, и мы будем бесконечно ждать? + Замок довольно умный и не допустит такой ситуации, потому что поставит всех следующих после писателя читателей в очередь. За счёт такой нехитрой логики мы получаем значительное ускорение параллельного чтения и защиту от битых данных за счёт того что писать может только один. + Вообще внутри там довольно тяжёлая логика, но к счастью у нас есть готовая оболочка которая ведёт себя известным нам образом. + Использование замков естественно по ситуации, обычно в веб-приложениях высоконагруженных сервисах где сотни и тысячи потоков ломятся поработать с одним ресурсом. +***** Атомарные типы данных + Все же помнят как мы на втором уровне брали переменную, и многопоточно её ломали, одновременно что присваивая и читая. Все проблемы рэйс кондишна происходят из-за того что операция изменения она состоит из двух частей - чтения и записи, и мы не знаем в какой момент поток прервётся другим. Атомарные типы данных это те, у которых операция их изменения - неожиданно атомарная. + AtomicInteger ai = new AtomicInteger(100); + System.out.println(ai.decrementAndGet()); + Соответственно мы можем гарантировать что это изменение точно не будет прервано никаким другим потоком. ну и многие другие методы которые в основном дублируют поведение привычное по математическим операторам. и некоторые другие, например compareAndSet() - мы говорим что ожидаем увидеть там значение 100. и еслитам действительно 100 то мы положим туда число из второго аргумента. diff --git a/settings/fancy-listings-preamble.tex b/settings/fancy-listings-preamble.tex index 28b1ee6..0bd21ea 100644 --- a/settings/fancy-listings-preamble.tex +++ b/settings/fancy-listings-preamble.tex @@ -17,9 +17,9 @@ belowskip=3mm, showstringspaces=false, columns=flexible, - basicstyle=\scriptsize\ttfamily, + basicstyle=\larger[-1]\ttfamily, numbers=left, - numberstyle=\tiny\color{gray}, + numberstyle=\larger[-2]\color{gray}, keywordstyle=\color{blue}, commentstyle=\color{dkgreen}, stringstyle=\color{mauve}, diff --git a/settings/main-style-preamble.tex b/settings/main-style-preamble.tex index fae3aa9..267cdd8 100644 --- a/settings/main-style-preamble.tex +++ b/settings/main-style-preamble.tex @@ -22,6 +22,7 @@ \usepackage{spreadtab} \usepackage{svg} \usepackage{afterpage} +\usepackage{relsize} \newcommand{\tabitem}{~~\llap{\textbullet}~~} @@ -48,6 +49,7 @@ \newcommand{\info}{\cellcolor{green!20}{\Huge \faInfoCircle \quad}} \newcommand{\excl}{\cellcolor{red!20}{\Huge \faExclamationTriangle \quad}} +\newcommand{\quot}{\cellcolor{blue!20}{\Huge \faQuoteLeft \quad}} \makeatletter \newcommand{\setword}[2]{%