gb-java-devel/jtc4-04a.tex

500 lines
69 KiB
TeX
Raw Normal View History

2022-12-15 14:36:00 +03:00
\documentclass[j-spec.tex]{subfiles}
\begin{document}
\setcounter{section}{3}
2022-12-15 14:36:00 +03:00
\setlength{\columnsep}{22pt}
\pagestyle{plain}
\sloppy
\tableofcontents
\section{Специализация: ООП и исключения}
\subsection{В предыдущем разделе}
Была рассмотрена реализация объектно-ориентированного программирования в Java. Рассмотрели классы и объекты, а также наследование, полиморфизм и инкапсуляцию. Дополнительно был освещён вопрос устройства памяти.
\subsection{В этом разделе}
В дополнение к предыдущему, будут разобраны такие понятия, как внутренние и вложенные классы; процессы создания, использования и расширения перечислений. Более детально будет разобрано понятие исключений и их тесная связь с многопоточностью в Java. Будут рассмотрены исключения с точки зрения ООП, процесс обработки исключений.
\begin{itemize}
\item \nom{Перечисление}{это упоминание объектов, объединённых по какому-либо признаку. Фактически, представляет новый тип данных, поэтому возможно определить переменную данного типа и использовать её.};
\item \nom{Внутренний класс}{нестатический класс, объявленный внутри другого класса.};
\item \nom{Вложенный класс}{статический класс, объявленный внутри другого класса.};
\item \nom{Локальный класс}{класс, объявленный внутри минимального блока кода другого класса, чаще всего, метода.};
\item \nom{Исключение}{это отступление от общего правила, несоответствие обычному порядку вещей.};
\item \nom{Многопоточность}{одновременное выполнение двух или более потоков для максимального использования центрального процессора (CPU -- central processing unit). Каждый поток работает параллельно и имеет свою собственную выделенную стековую память.};
2022-12-15 14:36:00 +03:00
\end{itemize}
\subsection{Перечисления}
Кроме восьми примитивных типов данных и классов в Java есть специальный тип, выведенный на уровень синтаксиса языка -- \code{enum} или перечисление. Перечисления представляют набор логически связанных констант. Объявление перечисления происходит с помощью оператора \code{enum}, после которого идет название перечисления. Затем идет список элементов перечисления через запятую.
2022-12-15 14:36:00 +03:00
\begin{frm} \info Перечисление -- это упоминание объектов, объединённых по какому-либо признаку \end{frm}
Перечисления -- это специальные классы, содержащие внутри себя собственные статические экземпляры.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример перечисления}]
enum Season { WINTER, SPRING, SUMMER, AUTUMN }
2022-12-15 14:36:00 +03:00
\end{lstlisting}
Перечисление, фактически, представляет новый тип данных, поэтому возможно определить переменную данного типа и использовать её. Переменная типа перечисления может хранить любой объект этого исключения.
2022-12-15 14:36:00 +03:00
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Переменная типа перечисления}]
Season current = Season.SPRING;
System.out.println(current);
\end{lstlisting}
2022-12-15 14:36:00 +03:00
Интересно также то, что вывод в терминал и запись в коде у исключений полностью совпадают, поэтому, в результате выполнения этого кода, в терминале будет выведено
2022-12-15 14:36:00 +03:00
\begin{verbatim}
SPRING
\end{verbatim}
2022-12-15 14:36:00 +03:00
Каждое перечисление имеет статический метод \code{values()}, возвращающий массив всех констант перечисления.
2022-12-15 14:36:00 +03:00
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вывод всех элементов перечисления}]
Season[] seasons = Season.values();
for (Season s : seasons) {
System.out.printf("s ", s);
}
\end{lstlisting}
2022-12-15 14:36:00 +03:00
Именно в этом примере используется цикл foreach для прохода по массиву, для лаконичности записи. Данный цикл берёт последовательно каждый элемент перечисления, присваивает ему имя \code{s} точно также, как это сделано в примере выше, делает эту переменную доступной в теле цикла в рамках одной итерации, на следующей итерации будет взят следующий элемент, и так далее.
2022-12-15 14:36:00 +03:00
\begin{verbatim}
WINTER, SPRING, SUMMER, AUTUMN
\end{verbatim}
2022-12-15 14:36:00 +03:00
Также, в перечисления встроен метод \code{ordinal()}, возвращающий порядковый номер определенной константы (нумерация начинается с 0). Обратите внимание на синтаксис, метод можно вызвать только у конкретного экземпляра перечисления, а при попытке вызова у самого класса перечисления, ожидаемо компилятор выдаёт ошибку невозможности вызова нестатического метода из статического контекста.
2022-12-15 14:36:00 +03:00
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Метод \code{ordinal()}}]
System.out.println(current.ordinal());
2022-12-15 14:36:00 +03:00
System.out.println(Seasons.ordinal()); // <@\lh{dkgreen}{ошибка}@>
\end{lstlisting}
2022-12-15 14:36:00 +03:00
Такое поведение возможно, только если номер элемента хранится в самом объекте.
2022-12-15 14:36:00 +03:00
\begin{frm} \info
В перечислениях можно наблюдать очень примечательный пример инкапсуляции -- неизвестно, хранятся ли на самом деле объекты перечисления в виде массива, но можем вызвать метод values() и получить массив всех элементов перечисления. Неизвестно, хранится ли в каждом объекте перечисления его номер, но можем вызвать его метод \code{ordinal()}.
\end{frm}
2022-12-15 14:36:00 +03:00
Раз перечисление -- это класс, возможно определять в нём поля, методы, конструкторы и прочее. Перечисление \code{Color} определяет приватное поле \code{code} для хранения кода цвета, а с помощью метода \code{getCode} он возвращается.
2022-12-15 14:36:00 +03:00
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Расширение объекта перечисления}]
public class Main {
enum Color {
RED("#FF0000"), BLUE("#0000FF"), GREEN("#00FF00");
private String code;
Color(String code) {
this.code = code;
}
2022-12-15 14:36:00 +03:00
public String getCode(){ return code;}
}
2022-12-15 14:36:00 +03:00
public static void main(String[] args) {
System.out.println(Color.RED.getCode());
System.out.println(Color.GREEN.getCode());
}
}
\end{lstlisting}
Через конструктор передается значение пользовательского поля.
\begin{frm} \excl Конструктор по умолчанию имеет модификатор \code{private}. Любой другой модификатор будет считаться ошибкой.
\end{frm}
Cоздать константы перечисления с помощью конструктора возможно только внутри самого перечисления. И что косвенно намекает на то, что объекты перечисления это статические объекты внутри самого класса перечисления. Также важно, что механизм описания конструкторов класса работает по той же логике, что и обычные конструкторы, то есть, при описании собственного конструктора, конструктор по-умолчанию перестаёт создаваться автоматически. Таким образом, с объектами перечисления можно работать точно также, как с обычными объектами.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вывод значений пользовательского поля перечисления}]
for (Color c : Color.values()) {
System.out.printf("s(s)\n", c, c.getCode());
}
\end{lstlisting}
\begin{verbatim}
RED(#FF0000)
BLUE(#0000FF)
GREEN(#00FF00)
\end{verbatim}
2022-12-15 14:36:00 +03:00
\subsubsection{Задания для самопроверки}
\begin{enumerate}
\item Перечисления нужны, чтобы: 3
\begin{enumerate}
\item вести учёт созданных в программе объектов;
\item вести учёт классов в программе;
\item вести учёт схожих по смыслу явлений в программе;
\end{enumerate}
\item Перечисление -- это: 2
2022-12-15 14:36:00 +03:00
\begin{enumerate}
\item массив
\item класс
\item объект
\end{enumerate}
\item каждый объект в перечислении -- это: 3
2022-12-15 14:36:00 +03:00
\begin{enumerate}
\item статическое поле
\item статический метод
\item статический объект
\end{enumerate}
\end{enumerate}
\subsection{Внутренние и вложенные классы}
В Java есть возможность создавать классы внутри других классов, все такие классы разделены на следующие типы:
\begin{enumerate}
\item Non-static nested (inner) classes — нестатические вложенные (внутренние) классы;
\begin{itemize}
\item локальные классы (local classes);
\item анонимные классы (anonymous classes);
\end{itemize}
\item Static nested classes — статические вложенные классы.
\end{enumerate}
Для рассмотрения анонимных классов понадобятся дополнительные знания об интерфейсах, поэтому будут рассмотрены позднее.
\subsubsection{Внутренние классы}
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вывод значений пользовательского поля перечисления}]
public class Orange {
public void squeezeJuice() {
System.out.println("Squeeze juice ...");
}
class Juice {
public void flow() {
System.out.println("Juice dripped ...");
}
}
}
\end{lstlisting}
\textbf{Внутренние классы} создаются внутри другого класса. Рассмотрим на примере апельсина с реализацией, как это предлагает официальная документация Oracle. В основной программе необходимо создать отдельно апельсин, отдельно его сок через интересную форму вызова конструктора, показанную в листинге \hrf{lst:create-orange}, что позволяет работать как с апельсином, так и его соком по отдельности.
\begin{lstlisting}[language=Java,style=JCodeStyle,label={lst:create-orange},caption={Обычный апельсин Oracle}]
Orange orange = new Orange();
Orange.Juice juice = orange.new Juice();
orange.squeezeJuice();
juice.flow();
\end{lstlisting}
Важно помнить, что когда в жизни апельсин сдавливается, из него сам по себе течёт сок, а когда апельсин попадает к нам в программу он сразу снабжается соком.
\begin{lstlisting}[language=Java,style=JCodeStyle,label={lst:create-gb-orange},caption={Необычный апельсин GeekBrains}]
public class Orange {
private Juice juice;
public Orange() {
this.juice = new Juice();
}
public void squeezeJuice() {
System.out.println("Squeeze juice ...");
juice.flow();
}
private class Juice {
public void flow() {
System.out.println("Juice dripped ...");
}
}
}
\end{lstlisting}
Итак, был создан апельсин, при создании объекта апельсина у него сразу появляется сок. Ниже в классе описано потенциальное наличие у апельсина сока, как его части, поэтому внутри класса апельсин создан класс сока. При создании апельсина создали сок, так или иначе -- самостоятельную единицу, обладающую своими свойствами и поведением, отличным от свойств и поведения апельсина, но неразрывно с ним связанную. При попытке выдавить сок у апельсина -- объект сока сообщил о том, что начал течь
\begin{lstlisting}[language=Java,style=JCodeStyle,label={lst:use-gb-orange},caption={Использование апельсина GeekBrains}]
Orange orange = new Orange();
orange.squeezeJuice();
\end{lstlisting}
Таким образом у каждого апельсина будет свой собственный сок, который возможно выжать, сдавив апельсин. В этом смысл внутренних классов не статического типа -- нужные методы вызываются у нужных объектов.
\begin{frm} \info Такая связь объектов и классов называется композицией. Существуют также ассоциация и агрегация.
\end{frm}
Если класс полезен только для одного другого класса, то часто бывает удобно встроить его в этот класс и хранить их вместе. Использование внутренних классов увеличивает инкапсуляцию. Оба примера достаточно отличаются реализацией. Пример не из документации подразумевает «более сильную» инкапсуляцию, так как извне ко внутреннему классу доступ получить нельзя, поэтому создание объекта внутреннего класса происходит в конструкторе основного класса -- в апельсине. С другой стороны, у примера из документации есть доступ извне ко внутреннему классу сока, но всё равно, только через основной класс апельсина, как и создать объект сока можно только через объект апельсина, то есть подчёркивается взаимодействие на уровне объектов.
\textbf{Особенности внутренних классов}:
\begin{itemize}
\item Внутренний объект не существует без внешнего. Это логично -- для этого \code{Juice} был создан внутренним классом, чтобы в программе не появлялись апельсиновые соки из воздуха.
\item Внутренний объект имеет доступ ко всему внешнему. Код внутреннего класса имеет доступ ко всем полям и методам экземпляра (и к статическим членам) окружающего класса, включая все члены, даже объявленные как \code{private}.
\item Внешний объект не имеет доступа ко внутреннему без создания объекта. Это логично, так как экземпляров внутреннего класса может быть создано сколь угодно много, и к какому именно из них обращаться?
\item У внутренних классов есть модификаторы доступа. Это влияет на то, где в программе возможно создавать экземпляры внутреннего класса. Единственное сохраняющееся требование — объект внешнего класса тоже обязательно должен существовать и быть видимым.
\item Внутренний класс не может называться как внешний, однако, это правило не распространяется ни на поля, ни на методы;
\item Во внутреннем классе нельзя иметь не-final статические поля. Статические поля, методы и классы являются конструкциями верхнего уровня, которые не связаны с конкретными объектами, в то время как каждый внутренний класс связан с экземпляром окружающего класса.
\item Объект внутреннего класса нельзя создать в статическом методе «внешнего» класса. Это объясняется особенностями устройства внутренних классов. У внутреннего класса могут быть конструкторы с параметрами или только конструктор по умолчанию. Но независимо от этого, когда создаётся объект внутреннего класса, в него неявно передаётся ссылка на объект внешнего класса.
\item Со внутренними классами работает наследование и полиморфизм.
\end{itemize}
\subsubsection{Задания для самопроверки}
\begin{enumerate}
\item Внутренний класс: 1
\begin{enumerate}
\item реализует композицию;
\item это служебный класс;
\item не требует объекта внешнего класса;
\end{enumerate}
\item Инкапсуляция с использованием внутренних классов: 2
\begin{enumerate}
\item остаётся неизменной
\item увеличивается
\item уменьшается
\end{enumerate}
\item Статические поля внутренних классов: 2
\begin{enumerate}
\item могут существовать
\item могут существовать только константными
\item не могут существовать
\end{enumerate}
\end{enumerate}
\subsubsection{Локальные классы}
Классы -- это новый тип данных для программы, поэтому технически возможно создавать классы, а также описывать их, например, внутри методов. Это довольно редко используется но синтаксически язык позволяет это сделать. \textbf{Локальные классы} — это подвид внутренних классов. Однако, у локальных классов есть ряд важных особенностей и отличий от внутренних классов. Главное заключается в их объявлении.
\begin{frm} \info Локальный класс объявляется только в блоке кода. Чаще всего — внутри какого-то метода внешнего класса.
\end{frm}
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример локального класса}]
public class Animal {
void performBehavior(boolean state) {
class Brain {
void sleep() {
if (state)
System.out.println("Sleeping");
else
System.out.println("Not sleeping");
}
}
Brain brain = new Brain();
brain.sleep();
}
}
\end{lstlisting}
Например, некоторое животное, у которого устанавливается состояние спит оно или нет. Метод \code{performBehavior()} принимает на вход булево значение и определяет, спит ли животное. Мог возникнуть вопрос: зачем? Итоговое решение об архитектуре проекта всегда зависит от структуры, сложности и предназначения программы.
\textbf{Особенности локальных классов}:
\begin{itemize}
\item Локальный класс сохраняет доступ ко всем полям и методам внешнего класса, а также ко всем константам, объявленным в текущем блоке кода, то есть полям и аргументам метода объявленным как \code{final}. Начиная с JDK 1.8 локальный класс может обращаться к любым полям и аргументам метода объявленным в текущем блоке кода, даже если они не объявлены как \code{final}, но только в том случае если их значение не изменяется после инициализации.
\item Локальный класс должен иметь свои внутренние копии всех локальных переменных, которые он использует (эти копии автоматически создаются компилятором). Единственный способ обеспечить идентичность значений локальной переменной и ее копии объявить локальную переменную как \code{final}.
\item Экземпляры локальных классов, как и экземпляры внутренних классов, имеют окружающий экземпляр, ссылка на который неявно передается всем конструкторам локальных классов. То есть, сперва должен быть создан экземпляр внешнего класса, а только затем экземпляр внутреннего класса.
\end{itemize}
\subsubsection{Статические вложенные классы}
При объявлении такого класса используется ключевое слово \code{static}. Для примера в классе котика % lst:cmpcat
и заменим метод \code{voice()} на статический класс.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Статический вложенный класс}]
public class Cat {
private String name;
private String color;
private int age;
public Cat()
public Cat(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
static class Voice {
private final int volume;
public Voice(int volume) {
this.volume = volume;
}
public void sayMur() {
System.out.printf("A cat purrs with volume %d\n", volume);
}
}
}
\end{lstlisting}
То есть, такое мурчание котика может присутствовать без видимости и понимания, что именно за котик присутствует в данный момент. Также, добавлена возможность установить уровень громкости мурчанья.
\begin{frm} \info
Основное отличие статических и нестатических вложенных классов в том, что объект статического класса не хранит ссылку на конкретный экземпляр внешнего класса.
\end{frm}
Без объекта внешнего класса объект внутреннего просто не мог существовать. Для статических вложенных классов это не так. Объект статического вложенного класса может существовать сам по себе. В этом плане статические классы более независимы, чем нестатические. Довольно важный момент заключается в том, что при создании такого объекта нужно указывать название внешнего класса,
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование статического класса}]
Cat.Voice voice = new Cat.Voice(100);
voice.sayMur();
\end{lstlisting}
Статический вложенный класс может обращаться только к статическим полям внешнего класса. При этом неважно, какой модификатор доступа имеет статическая переменная во внешнем классе.
Не следует путать объекты с переменными. Если речь идёт о статических переменных — да, статическая переменная класса существует в единственном экземпляре. Но применительно ко вложенному классу \code{static} означает лишь то, что его объекты не содержат ссылок на объекты внешнего класса.
\subsubsection{Задания для самопроверки}
\begin{enumerate}
\item Вложенный класс: 1
\begin{enumerate}
\item реализует композицию;
\item это локальный класс;
\item всегда публичный;
\end{enumerate}
\item Статический вложенный классо бладает теми же свойствами, что: 2
\begin{enumerate}
\item константный метод
\item внутренний класс
\item статическое поле
\end{enumerate}
\end{enumerate}
\subsection{Исключения}
Язык программирования -- это, в первую очередь, набор инструментов. Например, есть художник. У художника есть набор всевозможных красок, кистей, холстов, карандашей, мольберт, ластик и прочие. Это всё его инструменты. Тоже самое для программиста. У программиста есть язык программирования, который предоставляет ему инструменты: циклы, условия, классы, функции, методы, ООП, фрейморки, библиотеки. Исключения -- это один из инструментов. Исключения всегда следует рассматривать как ещё один инструмент для работы программиста.
\begin{frm} \info Исключение -- это отступление от общего правила, несоответствие обычному порядку вещей
\end{frm}
В общем случае, возникновение исключительной ситуации, это ошибка в программе, но основным вопросом является следующий. Возникшая ошибка -- это:
\begin{itemize}
\item ошибка в коде программы;
\item ошибка в действиях пользователя;
\item ошибка в аппаратной части компьютера?
\end{itemize}
При возникновении ошибок создаётся объект класса «исключение», и в этот объект записывается какое-то максимальное количество информации о том, какая ошибка произошла, чтобы потом прочитать и понять, где проблема. Соответственно эти объекты возможно «ловить и обрабатывать».
\begin{figure}[H]
\fontsize{11}{1}\selectfont
\includesvg[scale=.85]{pics/jc-04-throwable.svg}
\caption{Часть иерархии исключений}
\label{pic:exception-hierarchy}
\end{figure}
Все исключения наследуются от класса \code{Throwable} и могут быть как обязательные к обработке, так и необязательные. Есть ещё подкласс \code{Error}, но он больше относится к аппаратным сбоям или серьёзным алгоритмическим или архитектурным ошибкам, и на данном этапе интереса не представляет, потому что поймав, например, \code{OutOfMemoryError} средствами Java прямо в программе с ним ничего сделать невозможно, такие ошибки необходимо обрабатывать и не допускать в процессе разработки ПО.
Для изучения и примеров, воспользуемся двумя подклассами \code{Throwable} -- \code{Exception} -- \code{RuntimeException} и \code{IOException}.
\begin{frm} \excl Все исключения, кроме наследников \code{RuntimeException} необходимо обрабатывать.
\end{frm}
% лайвкод 04-мартёшка-методов & Давайте рассмотрим примерчики, напишем пару-тройку методов, и сделаем матрёшку,
% из мэйна вызываем метод2, оттуда метод1, оттуда метод0, а в методе0 всё как всегда портим, пишем
% private static int div0() return 1 / 0;
% ArithmeticException является наследником класса RuntimeEcxeption поэтому статический анализатор его не подчеркнул, и ловить его вроде как не обязательно, спасибо большое разработчикам джава, надёжность кода не повысилась, единообразность работы всех исключений нарушилась, всё хорошо.\\ \hline
% лайвкод 04-метод-деления & Выходит, на примере деления на ноль можно всё хорошо сразу и объяснить. допустим у нас есть какой-то метод который возможно мы даже сами написали, который, скажем, целочисленно делит два целых числа. немного его абстрагируем
% private static int div0(int a, int b) return a / b;
% Если посмотреть на этот метод с точки зрения программирования, он написан очень хорошо - алгоритм понятен, метод с единственной ответственностью, всё супер. Однако, из поставленной перед методом задачи очевидно, что он не может работать при всех возможных входных значениях. То есть если у нас вторая переменная равна нулю, то это неправильно. И что же с этим делать? \\ \hline
% лайвкод 04-исключение-1 & Нам нужно как-то запретить пользователю передавать в качестве делителя ноль. Самое простое - ничего не делать, но мы так не можем.
% private static int div0(int a, int b)
% if (b != 0) return a / b; return ???;
% Потому что метод должен что-то вернуть, а что вернуть, неизвестно, ведь от нас ожидают результат деления. Поэтому, допустим можем руками сделать проверку (б == 0ф) и выкинуть пользователю так называемый объект исключения throw new RuntimeException("деление на ноль") а иначе вернём а / б.
% private static int div0(int a, int b)
% if (b == 0) throw new RuntimeException("parameter error");
% return a / b; \\ \hline
% лайвкод 04-исключение-2 & Вызываем метод и пробуем делить 1 на 2. А вот если мы второй параметр передадим 0 то у нас выкинется исключение,
% System.out.println(div0(1,2));
% System.out.println(div0(1,0));
% то есть по сути new... это конструктор, нового объекта какого-то класса, в который мы передаём какой то параметр, в данном конкретном случае это строка с сообщением. Зафиксируем пока что эту мысль. \\ \hline
% отбивка введение в многопоточность & Кажется многовато отступлений, но без этого точно никак нельзя продолжать. \\ \hline
% 04-метод-броска & Итак, что происходит? Ключевое слово throw заставляет созданный объект исключения начать свой путь по родительским методам, пока этот объект не встретится с каким-то обработчиком. в нашем текущем случае - это дефолтный обработчик виртуальной машины, который в специальный поток err выводит так называемый стектрейс, и завершает дальнейшее выполнение метода.\\ \hline
% 04-поток-ерр & Далее по порядку: поток ерр. Все программы в джава всегда многопоточны. Понимаете вы многопоточность или нет, знаете ли вы о её существовании или нет, не важно, многопоточность есть всегда. не будем вдаваться в сложности прикладной многопоточности, у нас ещё будет на это довольно много времени, пока что поговорим в общем. в чём смысл? смысл в том, что на старте программы запускаются так называемые потоки, которые работают псевдопараллельно и предназначены каждый для решения своих собственных задач, например, это основной поток, поток сборки мусора, поток обработчика ошибок, потоки графического интерфейса. Основная задача этих потоков - делать своё дело и иногда обмениваться информацией.\\ \hline
% 04-стектрейс & В упомянутый же парой минут ранее стектрейс кладётся максимальная информация о типе исключения, его сообщении, иерархии методов, вызовы которых привели к исключительной ситуации. Если не научиться читать стектрейс, если честно, можно расходиться по домам и не думать о серьёзном большом программировании. Итак стектрейс. Когда у нас случается исключение - мы видим, что случилось оно в потоке мэйн, и является объектом класса RuntimeException сообщение мы тоже предусмотрительно приложили. Первое что важно понять, что исключение - это объект класса. Далее читаем матрёшку - в каком методе создался этот объект, на какой строке, в каком классе. Далее смотрим кто вызвал этот метод, на какой строке, в каком классе. Это вообще самый простой стектрейс, который может быть. Бывают полотна по несколько десятков строк, клянусь, сам видел. Особенно важно научиться читать стектрейс разработчикам андроид, потому что именно такие стектрейсы будут вам прилетать в отчёт. У пользователя что то упало, он нажал на кнопку отправить отчёт, и вам в консоль разработчика прилетел стектрейс, который будет являться единственной доступной информацией о том, где вы или пользователь накосячили.\\ \hline
% лайвкод 04-простой-пример-исключения & Если мы не напишем никакого исключения, кстати, оно всё равно произойдёт. Это общее поведение исключения. Оно где-то случается, прекращает выполнение текущего метода, и начинает лететь по стеку вызовов вверх. Возможно даже долетит до дефолтного обработчика, как в этом примере.
% int[] arr = {1};
% System.out.println(arr[2])
% Некоторые исключения генерятся нами, некоторые самой джавой, они вполне стандартные, например выход за пределы массива, деление на ноль, и классический нуль-поинтер. \\ \hline
% лайвкод 04-объект-исключения & Посмотрим на исключения под немного другим углом. Создадим какой-нибудь учебный класс, psvm и создадим экземпляр класса исключения
% RuntimeException e = new RuntimeException();
% Если просто сейчас запустить программу, то ничего не произойдёт, нам нужно наше исключение, как бы это сказать, активировать, выкинуть, возбудить сгенерировать. Для этого есть ключевое слово
% throw e;
% Запускаем, видим. Компилятор ошибок не обнаружил и всё пропустил, а интерпретатор наткнулся на класс исключения, и написал нам в консоль следующее, в основном потоке программы возникло вот такое исключение в таком пакете в таком классе на такой строке.
% \\ \hline
% лайвкод 04-рантайм-выводы & Усложним создадим паблик стэтик воид методА, выкинем исключение в нём, и вызовем его из мэйна.
% теперь по вот этому стэктрейсу можем проследить что откуда вызвалось и как мы дошли до исключительной ситуации. Можем в нашем исключении даже написать наше кастомное сообщение и использовать все эти штуки в разработке. Можем унаследоваться от какого-то исключения и создать свой класс исключений, об этом чуть позже, в общем, всё зависит от поставленной задачи. Достаточно гибкая штука эти исключения. Ну и поскольку это рантаймэксепшн то обрабатывать его на этапе написания кода не обязательно, компилятор на него не ругается, всё круто. \\ \hline
% 04-иерархия-исключений & На самом деле на этом интересные особенности обработки исключений наследников Runtime, также называемых анчекд заканчивается далее будет гораздо интереснее рассматривать исключения обязательные для обработки, потому что статический анализатор кода не просто их выделяет, а обязывает их обрабатывать на этапе написания кода. И просто не скомпилирует проект если в коде есть необработанные так или иначе исключения также известные как чекд. Заменим Runtime исключение на обычное обязательное к обработке. ((удалить слово Runtime))\\ \hline
% 04-пробуй-лови & Давайте наше исключение ловить. Первое, и самое важное, что надо понять - это почему что-то упало, поэтому не пытайтесь что то ловить, пока не поймёте что именно произошло, от этого понимания будет зависеть способ ловли. Исключение ловится двухсекционным оператором try-catch, а именно, его первой секцией try. Это секция, в которой предполагается возникновение исключения, и предполагается, что мы можем его поймать. А в секции catch пишем имя класса исключения, которое мы ловим, и имя объекта, в который мы положим экземпляр нашего исключения. Секция catch ловит указанное исключение и всех его наследников. Это важно. Рекомендуется писать максимально узко направленные секции catch, потому что надо стараться досконально знать как работает ваша программа, и какие исключения она может выбрасывать. Ну и ещё потому что разные исключения могут по-разному обрабатываться, конечно-же. Секций catch может быть сколько угодно много. Как только мы обработали объект исключения, он уничтожается, дальше он не поднимается, и в следующие catch не попадает. Мы, конечно, можем его же насильно пульнуть выше, ключевым словом throw. \\ \hline
% 04-варианты ? & Так вот, когда какой-то наш метод выбрасывает исключение у нас есть два основных пути: вы обязаны либо вынести объявление этого исключения в сигнатуру метода, что будет говорить тем, кто его вызывает о том, что в методе может возникнуть исключение, либо мы это исключение должны непосредственно в методе обработать, иначе у вас ничего не скомпилируется. Примером одного из таких исключений служит ИО это сокращение от инпут-аутпут и генерируется, когда, вы не поверите, возникает ошибка ввода-вывода. То есть при выполнении программы что-то пошло не так и она, программа не может произвести ввод-вывод в штатном режиме. На деле много чего может пойти не так, операционка заглючила, флешку выдернули, устройство телепортировалось в микроволновку, и всё, случился ИОЭксепшн, не смогла программа прочитать или написать что то в потоке ввода-вывода. Соответственно, уже от этого ИОЭ возникают какие-то другие, вроде FileNotFoundException, которое мы тоже обязаны обработать. Например, мы хотим чтобы наша программа что то там прочитала из файла, а файла на нужном месте не оказалось, и метод чтения генерирует исключение. \\ \hline
% % +++ Комментатор_Ильнар. Мы вроде нигде про вынос в сигнатуру до этого не говорили, лучше до этих слов сказать что есть второй способ, а то читается так будто мы об этом уже сказали.
% 04-ответственность & В случае, если мы выносим объявление исключения в сигнатуру, вызывающий метод должен обработать это исключение точно таким-же образом - либо в вызове, либо вынести в сигнатуру. Исключением из этого правила является класс RuntimeException. Все наследники от него, включая его самого, обрабатывать не обязательно. Туда входит деление на ноль, индексаутофбаунд экзепшн например. то есть те ошибки, которые компилятор пропускает, а возникают они уже в среде исполнения. Обычно уже по названию понятно что случилось, ну и помимо говорящих названий, там ещё содержится много инфы, хотя-бы даже номер строки, вызвавшей исключительную ситуацию. Общее правило работы с исключениями одно - если исключение штатное - его надо сразу обработать, если нет - надо дождаться, пока программа упадёт. Повторю, все исключения надо обрабатывать, примите это как расплату за использование языка Java.\\ \hline
% лайвкод 04-обработка-вариант-1 & Вернёся к нашему учебному классу и Усложним ещё. в методеА вызовем методБ. а в методеБ выкинем то что нам обязательно надо обработать, например IOE. Вот тут то и начинается веселье. потому что IOE это не наследник рантайм эксепшена, и мы обязаны обрабатывать на этапе компиляции. Тут у нас есть две опции. многим уже известный try-catch, синтаксис его не совсем очевидный, так что тут надо просто сделать усилие и запомнить. пишем
% try { methodB() } catch (имя класса исключения которое хотим поймать и идентификатор экземпляра) { }.
% Если возникло исключение, попадём в кэтч, и тут можем делать что хотим, чтобы программа не упала. Конечно можно написать в кэтч общий класс вроде Throwable или Exception, вспомним про родительские классы, и тогда он поймает вообще все исключения которые могут быть, даже те, которые мы, возможно не ожидаем. Очень часто в процессе разработки нужно сделать, чтобы нам в процессе выполнения что-то конкретное об исключении выводилось на экран, для этого у экземпляра есть метод гетМессадж. Пишем
% sout(e.getMessage()).
% Ещё чаще бывает, что выполнение программы после выбрасывания исключения не имеет смысла и мы хотим чтобы программа упала. Вот тут очень интересный финт придумали. Мы выкидываем новое рантаймэкзепшн, передав в него экземпляр отловленного исключения используя довольно хитрый конструктор копирования. Во как
% catch throw new runtimeexception(e); \\ \hline
% лайвкод 04-обработка-вариант-2 & Второй вариант обработки исключений - мы в сигнатуре метода пишем
% throws IOE,
% и через запятую все остальные возможные исключения этого метода. Всё, у нас с ним проблем нет, но у метода который его вызовет - появились. И так далее наверх. Тут всё достаточно просто. Далее пробуем описать какое-то исключение, которое мы обязаны будем ловить. Предлагаю переименовать наш учебный класс и его методы в некоторую имитацию потока ввода-вывода.
% Класс ТестСтрим
% методА = конструктор
% методБ = инт читать
% Внутри методаБ давайте создадим какой-нибудь FileInputStream который может генерить FileNotFoundException который на самом деле является наследником IOE, который наследуется от Exception. Никаких рантайм, значит обработать мы его обязаны. Два варианта у нас есть, либо мы его укутаем в try-catch, либо вот не можете вы написать обработчик, потому что не знаете как должна обрабатываться данная исключительная ситуация, и обработать её должна сторона, которая вызывает метод чтения, в таком случае пишем, что метод может генерить исключения. Всё, вы теперь свободны от обработки этого исключения в методе чтения. Но теперь подчёркивается метод мейн... и здесь мы встаём перед той-же дилеммой. и так далее по стэку вызовов любой глубины. \\ \hline
% вовочка перед печкой «и так сойдёт» & Важный момент. Задачи бывают разные. Исключения - это инструмент, который нетривиально работает. Важно при написании кода понять, возникающая исключительная ситуация - штатная, или нештатная. В большинстве случаев - ситуации нештатные, поэтому надо уронить приложение и разбираться с тем, что произошло. Допустим для вашего приложения вот стопроцентно какой-то файл должен быть, без него дальше нет смысла продолжать. Что делать, если его нет? Явно не пытаться в него что то писать, правда? Самое плохое, что можно сделать - ничего не делать. Это самое страшное, когда программа повела себя как-то не так, а мы об этом даже не узнали. Допустим мы хотим прочитать файл, вывести в консоль, но мы глотаем исключение, выведя стектрейс куда-то-там какому-то разработчику и наши супер-важные действия не выполнились. Надо ронять. Как ронять? да throw new RuntimeException(e). Крайне редко случаются ситуации, когда исключение надо проглотить. \\ \hline
% лайвкод 04-файналли & Давайте обрабатывать дальше. Все помнят, что потоки надо закрывать? Даже если не знали, теперь знаете. Вот, допустим, у нас в нашем модном потоке открылся файл, что то из него прочиталось, потом метод упал с исключением, а файл остался незакрытым, ресурсы заняты. Давайте даже допишем свой класс TestStream пусть его конструктор выбрасывает IOE, метод read выбрасывает IOE и будет метод close, везде будем логировать успешность в read пока оставляем исключение throw new IOE("reading error") в close "closed". Штатно возвращаем из read единичку и логируем, что всё прочитали в мейне. Как всё будет происходить штатно?
% TestStream stream = new TestStream();
% int a = stream.read()
% stream.close()
% Далее представим что в методе read что то пошло не так, выбрасываем исключение, и что видим в консоли? создали поток, исключение, конец программы. Что же делать? а делать секцию finally. Секция finally будет выполнена в любом случае, будет исключение, не будет исключения, не важно. Но тут тоже большое спасибо разработчикам джавы, мы не видим наш поток, то есть его надо вынести наружу, а ещё он оказывается не инициализирован, значит надо написать что то типа TestStream stream = null. Теперь всё должно отработать. \\ \hline
% лайвкод 04-проблема & Теперь немного неприятностей. Написали мы блок finally, вроде даже избавились от проблемы с закрытием потока. А как быть, если исключение возникло при создании этого потока? Тогда получается, у нас метод закрытия будет пытаться выполниться от ссылки на null. Нехорошо, знаете-ли, получается. Давайте всё сломаем, прям в конструкторе нашего потока выкинем IOE. Получили NPE в блоке finally. Очевидное решение - ставим в секции finally условие, и если поток не равен нулю, закрываем. Вроде клёво. Меняем тактику. Конструктор отрабатывает нормально. Метод чтения всё ещё генерирует исключение, но бац и в методе закрытия что-то пошло не так, и вылетело исключение. Ну вот так не повезло в жизни. Что делаем? Оборачиваем в try-catch. Вроде снова всё классно. Но и тут мы можем наткнуться на неприятность. Допустим, что нам надо в любом случае ронять приложение (в кэтч допишем throw new Runtime...). Тогда если у нас try поймал исключение, и выкинул его, потом finally всё равно выполнится, и второе исключение затрёт первое, мы его не увидим. Ну а поскольку первое для нас важнее, то второе - максимум что мы можем сделать - это залогировать исключение в консольку. Так дела обстояли в седьмой джаве. \\ \hline
% лайвкод 04-с-ресурсами & Что предлагает нам восьмая джава? try-с-ресурсами. Поток - это ресурс, абстрактное понятие. Как с этим работать? Сейчас будет небольшое забегание вперёд, пока что предлагаю просто запомнить магию. Выражаясь строго формально, мы должны реализовать интерфейс Closeable. А что это за интерфейс? (Ctrl+click) там содержится всего один метод close(), который умеет бросать IOE. Напишем везде пока что штатное поведение в нашем тестовом потоке. Залогируем. Далее синтаксис try-с-ресурсами. Все потоки начиная с восьмой джавы реализуют интерфейс Closeable. Стираем все ужасы, которые мы написали, пишем
% try(TestStream stream = new TestStream())
% int a = stream.read();
% catch (IOException e)
% new RuntimeException(e)
% И всё, мы поток не закрываем. За это у нас ответит сама джава. Запустим и проверим. Никаких close() и finally, всё хорошо. Самое классное - если мы ломаем метод read() то трай с ресурсами всё равно наш поток корректно закроет. Иногда вы можете видеть вывод в консоль и стектрейс вразнобой, ничего страшного, просто исключения вылетают в поток error а вывод в консоль в стандартный вывод. Ну и у них иногда случаются асинхронности. А теперь вообще самый смак - ломаем метод закрытия, и джава очень правильно поступит, она выкатит наверх основное исключение, но и выведет "подавленное" исключение, вторичное в стектрейс. По-человечески, красиво, информативно, глаз радуется. Рекомендуется по возможности использовать вот такую конструкцию. Но научиться пользоваться надо и тем и тем, естественно. \\ \hline
% % Комментатор_Ильнар. Смотри, многие тут потеряются походу этой лекции про вложенные классы и исключения. Понятно что про ресурс надо объяснить почему без него костылить многие вещи сложно, но многие в этом могут запутаться. Добавь пожалуйста резюме на пальцах после таких блоков. Мол, если уже успели запутаться и потеряться, то вот вам простая инструкция - исключения либо прокидываем, либо обрабатываем. Прокидываем так, обрабатываем в try catch finaly. Есть для обработки прекрасная штука try с ресурсами. для чего все это нужно - можно переслушать то что я рассказал после того как поиграете со всем этим на семинаре. Переслушивать можно медленно и конспектируя несколько раз пока не дойдет, не стесняйтесь это делать. Такое же резюме можно по вложенным классам сделать, чтобы их немного успокоить
% 03-наследование & Последнее на сегодня про исключения это наследования и Полиморфизм исключения тема не очень большая и в целом не сложная потому что вы уже знаете что такое класса объекты как классы могут наследоваться и что такое Полиморфизм. Особенно застрять внимание на объектно-ориентированном программирования исключениях скорее всего не нужно потому что было неоднократно сказано что исключение это тоже классы и есть какие-то наследники исключений генерируются и выбрасываю объекты исключений единственное что важно упомянуть это то что под система исключений работает немного не тривиально впрочем это вы могли заметить и сами но мы можем создавать собственные исключения с собственными смыслами и сообщениями и точно также их выбрасывать вместо стандартных наследоваться мы можем от любых исключений единственное что важно это то что не рекомендуется наследоваться от классов throwable и error когда описываете исключение механика checked и unchecked исключений сохраняется при наследование поэтому создав наследник RuntimeException вы получаете не проверяемые на этапе написания кода исключение \\ \hline
% На этом уроке & На этой лекции в дополнение к предыдущей, разобрали такие понятия как внутренние и вложенные классы, было непросто, но мы, кажется, справились; процессы создания, использования и расширения перечислений. Детально разобрали уже знакомое вам понятие исключений и их тесную связь с многопоточностью в джава. Всё время смотрели на исключения с точки зрения ООП, обработали немного исключений, а также раз и навсегда разделили понятия штатных и нештатных ситуаций. \\ \hline
2022-12-15 14:36:00 +03:00
\subsection*{Практическое задание}
\begin{enumerate}
\item напишите два наследника класса Exception: ошибка преобразования строки и ошибка преобразования столбца
\item разработайте исключения-наследники так, чтобы они информировали пользователя в формате ожидание/реальность
\item для проверки напишите программу, преобразующую квадратный массив целых чисел 5х5 в сумму чисел в этом массиве, при этом, программа должна выбросить исключение, если строк или столбцов в исходном массиве окажется не 5.
2022-12-15 14:36:00 +03:00
\end{enumerate}
\newpage
\printnomenclature[40mm]
\end{document}