756 lines
69 KiB
TeX
756 lines
69 KiB
TeX
\documentclass[j-spec.tex]{subfiles}
|
||
|
||
\begin{document}
|
||
\setcounter{section}{3}
|
||
\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{Искл. (событие)}{поведение потока исполнения (например, программы), пользователя или аппаратурного окружения, приведшее к исключению. При возникновении исключения создаётся объект исключения и работа потока останавливается.};
|
||
\item \nom{Искл. (объект)}{созданный программным кодом или JRE объект, передаваемый от потока, в котором произошло исключительное событие, обработчику исключений};
|
||
\item \nom{Обработчик искл.}{объект, работающий в потоке error или его наследники, способный ловить объекты исключений и совершать с ними манипуляции, например, выводить информацию об объекте исключения в консоль.};
|
||
\item \nom{\code{throw}}{оператор, активирующий (выбрасывающий) объект исключения.};
|
||
\item \nom{Stacktrace}{часть объекта исключения, содержащая максимальное количество информации об иерархии методов, вызовы которых привели к исключительной ситуации.};
|
||
\item \nom{\code{try...catch}}{двухсекционный оператор языка Java, позволяющий «безопасно» выполнить код, содержащий исключение, поймать и обработать возникшее исключение.};
|
||
\item \nom{\code{throws}}{ключевое слово, определяющее обработку исключения в методе. Фактически, это предупреждение для вызывающего о возможном исключении в методе.};
|
||
\item \nom{\code{finally}}{часть оператора \code{try...catch}, выполняющаяся вне зависимости от того, возникло ли исключение в секции \code{try} и было ли оно обработано в секции \code{catch}.};
|
||
\item \nom{Подавленное искл.}{исключение, возникшее \textit{первым} в ситуации, когда в одном операторе \code{try...catch...finally} выброшены исключения как в \code{try}, так и в \code{finally}.};
|
||
\item \nom{Многопоточность}{одновременное выполнение двух или более потоков для максимального использования центрального процессора (CPU -- central processing unit). Каждый поток работает параллельно и имеет свою собственную выделенную стековую память.};
|
||
\end{itemize}
|
||
|
||
\subsection{Перечисления}
|
||
Кроме восьми примитивных типов данных и классов в Java есть специальный тип, выведенный на уровень синтаксиса языка -- \code{enum} или перечисление. Перечисления представляют набор логически связанных констант. Объявление перечисления происходит с помощью оператора \code{enum}, после которого идет название перечисления. Затем идет список элементов перечисления через запятую.
|
||
|
||
\begin{frm} \info Перечисление -- это упоминание объектов, объединённых по какому-либо признаку \end{frm}
|
||
|
||
Перечисления -- это специальные классы, содержащие внутри себя собственные статические экземпляры.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример перечисления}]
|
||
enum Season { WINTER, SPRING, SUMMER, AUTUMN }
|
||
\end{lstlisting}
|
||
|
||
Перечисление, фактически, представляет новый тип данных, поэтому возможно определить переменную данного типа и использовать её. Переменная типа перечисления может хранить любой объект этого исключения.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Переменная типа перечисления}]
|
||
Season current = Season.SPRING;
|
||
System.out.println(current);
|
||
\end{lstlisting}
|
||
|
||
Интересно также то, что вывод в терминал и запись в коде у исключений полностью совпадают, поэтому, в результате выполнения этого кода, в терминале будет выведено
|
||
|
||
\begin{verbatim}
|
||
SPRING
|
||
\end{verbatim}
|
||
|
||
Каждое перечисление имеет статический метод \code{values()}, возвращающий массив всех констант перечисления.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вывод всех элементов перечисления}]
|
||
Season[] seasons = Season.values();
|
||
for (Season s : seasons) {
|
||
System.out.printf("s ", s);
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Именно в этом примере используется цикл foreach для прохода по массиву, для лаконичности записи. Данный цикл берёт последовательно каждый элемент перечисления, присваивает ему имя \code{s} точно также, как это сделано в примере выше, делает эту переменную доступной в теле цикла в рамках одной итерации, на следующей итерации будет взят следующий элемент, и так далее.
|
||
|
||
\begin{verbatim}
|
||
WINTER, SPRING, SUMMER, AUTUMN
|
||
\end{verbatim}
|
||
|
||
Также, в перечисления встроен метод \code{ordinal()}, возвращающий порядковый номер определенной константы (нумерация начинается с 0). Обратите внимание на синтаксис, метод можно вызвать только у конкретного экземпляра перечисления, а при попытке вызова у самого класса перечисления, ожидаемо компилятор выдаёт ошибку невозможности вызова нестатического метода из статического контекста.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Метод \code{ordinal()}}]
|
||
System.out.println(current.ordinal());
|
||
|
||
System.out.println(Seasons.ordinal()); // <@\lh{dkgreen}{ошибка}@>
|
||
\end{lstlisting}
|
||
|
||
Такое поведение возможно, только если номер элемента хранится в самом объекте.
|
||
|
||
\begin{frm} \info
|
||
В перечислениях можно наблюдать очень примечательный пример инкапсуляции -- неизвестно, хранятся ли на самом деле объекты перечисления в виде массива, но можем вызвать метод values() и получить массив всех элементов перечисления. Неизвестно, хранится ли в каждом объекте перечисления его номер, но можем вызвать его метод \code{ordinal()}.
|
||
\end{frm}
|
||
|
||
Раз перечисление -- это класс, возможно определять в нём поля, методы, конструкторы и прочее. Перечисление \code{Color} определяет приватное поле \code{code} для хранения кода цвета, а с помощью метода \code{getCode} он возвращается.
|
||
|
||
\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;
|
||
}
|
||
|
||
public String getCode(){ return code;}
|
||
}
|
||
|
||
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}
|
||
|
||
\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}
|
||
|
||
\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 Внутренний класс:
|
||
\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{Локальные классы}
|
||
Классы -- это новый тип данных для программы, поэтому технически возможно создавать классы, а также описывать их, например, внутри методов. Это довольно редко используется но синтаксически язык позволяет это сделать. \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 Вложенный класс:
|
||
\begin{enumerate}
|
||
\item реализует композицию;
|
||
\item это локальный класс;
|
||
\item всегда публичный;
|
||
\end{enumerate}
|
||
\item Статический вложенный класс обладает теми же свойствами, что:
|
||
\begin{enumerate}
|
||
\item константный метод
|
||
\item внутренний класс
|
||
\item статическое поле
|
||
\end{enumerate}
|
||
\end{enumerate}
|
||
|
||
\subsection{Исключения}
|
||
\subsubsection{Понятие}
|
||
Язык программирования -- это, в первую очередь, набор инструментов. Например, есть художник. У художника есть набор всевозможных красок, кистей, холстов, карандашей, мольберт, ластик и прочие. Это всё его инструменты. Тоже самое для программиста. У программиста есть язык программирования, который предоставляет ему инструменты: циклы, условия, классы, функции, методы, ООП, фрейморки, библиотеки. Исключения -- это один из инструментов. Исключения всегда следует рассматривать как ещё один инструмент для работы программиста.
|
||
|
||
\begin{frm} \info Исключение -- это отступление от общего правила, несоответствие обычному порядку вещей
|
||
\end{frm}
|
||
|
||
В общем случае, возникновение исключительной ситуации, это ошибка в программе, но основным вопросом является следующий. Возникшая ошибка -- это:
|
||
\begin{itemize}
|
||
\item ошибка в коде программы;
|
||
\item ошибка в действиях пользователя;
|
||
\item ошибка в аппаратной части компьютера?
|
||
\end{itemize}
|
||
|
||
\subsubsection{Общие сведения}
|
||
При возникновении ошибок создаётся объект класса «исключение», и в этот объект записывается какое-то максимальное количество информации о том, какая ошибка произошла, чтобы потом прочитать и понять, где проблема. Соответственно эти объекты возможно «ловить и обрабатывать».
|
||
|
||
\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 Все исключения (\textbf{checked}), кроме наследников \code{RuntimeException} (\textbf{unchecked}), необходимо обрабатывать.
|
||
\end{frm}
|
||
|
||
Опишем на простом примере, один метод вызывает другой, второй вызывает третий и последний всё портит:
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Цепочка методов},label={lst:except-div-1}]
|
||
private static int div0(int a, int b) {
|
||
return a / b;
|
||
}
|
||
|
||
private static int div1(int a, int b) {
|
||
return div0(a, b);
|
||
}
|
||
|
||
private static int div2(int a, int b) {
|
||
return div1(a, b);
|
||
}
|
||
\end{lstlisting}
|
||
|
||
|
||
\begin{figure}[H]
|
||
\centering
|
||
\includegraphics[width=\textwidth]{jc-04-stacktrace-sample.png}
|
||
\caption{Результат запуска цепочки методов (листинг \hrf{lst:except-div-1})}
|
||
\label{pic:result-except-div-1}
|
||
\end{figure}
|
||
|
||
\code{ArithmeticException} является наследником класса \code{RuntimeEcxeption} поэтому статический анализатор кода его не подчеркнул, и «ловить» его не обязательно.
|
||
|
||
При работе с исключениями часто можно встретить слова, похожие на сленг, но это не так. Чаще всего, то, что звучит как сленг -- просто перевод ключевых слов языка, осуществляющих то или иное действие.
|
||
\begin{itemize}
|
||
\item try -- (англ. пробовать) пробовать, пытаться;
|
||
\item catch -- (англ. ловить) ловить, поймать, хватать;
|
||
\item throw -- (англ. бросать) выбрасывать, бросать, кидать;
|
||
\item NullPointerException -- НПЕ, налпоинтер;
|
||
\item и другие...
|
||
\end{itemize}
|
||
|
||
Если посмотреть на метод \code{div0(int a, int b)} с точки зрения программирования, он написан очень хорошо -- алгоритм понятен, метод с единственной ответственностью, однако, из поставленной перед методом задачи очевидно, что он не может работать при всех возможных входных значениях. То есть если вторая переменная равна нулю, то это ошибка. Необходимо запретить пользователю передавать в качестве делителя ноль. Самое простое -- ничего не делать, но в программе на языке Java так нельзя, если мы объявили, что метод имеет возвращающее значение, он обязан что-то вернуть.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ошибка -- нельзя ничего не возвращать},label={lst:except-div-error}]
|
||
private static int div0(int a, int b) {
|
||
if (b != 0)
|
||
return a / b;
|
||
return ???; // <@\lh{dkgreen}{ошибка}@>
|
||
}
|
||
\end{lstlisting}
|
||
|
||
А что вернуть, неизвестно, ведь от метода ожидается результат деления. Поэтому, возможно руками сделать проверку \code{(b == 0f)} и «выбросить» пользователю так называемый \textbf{объект исключения} с текстом ошибки, объясняющим произошедшее, а иначе вернём \code{a / b}.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Цепочка методов},label={lst:except-div-better}]
|
||
private static int div0(int a, int b) {
|
||
if (b == 0f)
|
||
throw new RuntimeException("parameter error");
|
||
return a / b;
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Следовательно, если делитель не равен нулю произойдёт обычное деление, а если равен -- будет «выброшено» исключение.
|
||
\begin{figure}[H]
|
||
\centering
|
||
\includegraphics[width=\textwidth]{jc-04-stacktrace-better.png}
|
||
\caption{Исключение, выброшенное «на наших условиях» (листинг \hrf{lst:except-div-better})}
|
||
\label{pic:result-except-div-better}
|
||
\end{figure}
|
||
|
||
Очевидно, что ключевое слово \code{new} вызывает конструктор, нового объекта какого-то класса, в который передаётся какой-то параметр, в данном конкретном случае это строка с сообщением.
|
||
|
||
\subsubsection{Объект исключения}
|
||
Ключевое слово \code{throw} заставляет созданный объект исключения начать свой путь по родительским методам, пока этот объект не встретится с каким-то обработчиком. В данном конкретном случае -- это обработчик виртуальной машины (по-умолчанию), который в специальный поток error выводит так называемый stacktrace, и завершает дальнейшее выполнение метода (технически, всего потока целиком).
|
||
|
||
\begin{figure}[H]
|
||
\fontsize{12}{1}\selectfont
|
||
\includesvg[scale=1.01]{pics/jc-04-threading.svg}
|
||
\caption{Принципиальная схема работы приложения}
|
||
\label{pic:throw-treading}
|
||
\end{figure}
|
||
|
||
Все программы в Java всегда многопоточны. На старте программы запускаются так называемые потоки, которые работают псевдопараллельно и предназначены каждый для решения своих собственных задач, например, это основной поток, поток сборки мусора, поток обработчика ошибок, потоки графического интерфейса. Основная задача этих потоков -- делать своё дело и иногда обмениваться информацией.
|
||
|
||
В \textbf{stacktrace}, содержащийся в объекте исключения, кладётся максимальное количество информации о типе исключения, его сообщении, иерархии методов, вызовы которых привели к исключительной ситуации.
|
||
|
||
\begin{frm} \info Важно научиться читать stacktrace на как можно более раннем этапе изучения программирования.
|
||
\end{frm}
|
||
|
||
Итак стектрейс. В стектрейсе на рис. \hrf{pic:result-except-div-better} видно, что исключение было создано в потоке main, и является объектом класса \code{RuntimeException}, сообщение также было предусмотрительно приложено автором кода. Важно понять, что исключение -- это объект класса. Далее, можно просто читать последовательно строку за строкой -- в каком методе создался этот объект, на какой строке, в каком классе. Далее видно, какой код вызвал этот метод, на какой строке, в каком классе.
|
||
|
||
Если не написать явного выбрасывания никакого исключения, оно всё равно будет выброшено. Это общее поведение исключения. Оно где-то случается, прекращает выполнение текущего метода, и начинает лететь по стеку вызовов вверх. Возможно даже долетит до обработчика по-умолчанию. Некоторые исключения создаются в коде явно, некоторые самой Java, они вполне стандартные, например выход за пределы массива, деление на ноль, и классический \code{NullPointer}.
|
||
|
||
Создадим экземпляр класса исключения внутри метода, вызываемого из \code{main}:
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Инициализация объекта исключения},label={lst:except-init}]
|
||
RuntimeException e = new RuntimeException();
|
||
\end{lstlisting}
|
||
|
||
Если оставить программу в таком виде и запустить, то ничего не произойдёт, исключение нужно выкинуть (активировать, возбудить, сгенерировать). Для этого есть ключевое слово
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Выбрасывание объекта исключения},label={lst:except-throw}]
|
||
throw e;
|
||
\end{lstlisting}
|
||
|
||
Компилятор ошибок не обнаружил и всё пропустил, а интерпретатор наткнулся на класс исключения, и написал в консоль, что в основном потоке программы возникло исключение в пакете в классе на такой-то строке. По стэктрейсу возможно проследить что откуда вызвалось и как программа дошла до исключительной ситуации. Возможно также наследоваться от какого-то исключения и создать свой класс исключений.
|
||
|
||
\begin{frm} \info Исключения, наследники \code{RuntimeException}, являются \textbf{Unchecked}, то есть не обязательные для обработки на этапе написания кода. \textit{Все остальные} \code{Throwable} -- обязательные для обработки, статический анализатор кода не просто их выделяет, а обязывает их обрабатывать на этапе написания кода. И просто не скомпилирует проект если в коде есть необработанные исключения, также известные как \textbf{Checked}.
|
||
\end{frm}
|
||
|
||
\subsubsection{Обработка}
|
||
\begin{frm} \excl Первое, и самое важное, что нужно понять -- почему что-то пошло (или пойдёт) «не так», поэтому не пытайтесь что-то ловить, пока не поймёте что именно произошло, от этого понимания будет зависеть \textit{способ} ловли.
|
||
\end{frm}
|
||
|
||
Исключение ловится двухсекционным оператором \textbf{\code{try...catch}}, а именно, его первой секцией \code{try}. Это секция, в которой предполагается возникновение исключения, и предполагается, что его возможно обработать. А в секции \code{catch} пишется имя класса исключения, которое будет поймано ловим, и имя объекта (идентификатор), через который внутри секции можно к пойманному объекту обращаться. Секция \code{catch} ловит указанное исключение и \textit{всех его наследников}.
|
||
|
||
\begin{frm} \info Рекомендуется писать максимально узко направленные секции \code{catch}, потому что надо стараться досконально знать как работает программа, и какие исключения она может выбрасывать. Также, потому что разные исключения могут по-разному обрабатываться.
|
||
\end{frm}
|
||
|
||
Секций \code{catch} может быть любое количество. Как только объект исключения обработан, он уничтожается и в следующие \code{catch} не попадает. Однако, объект возможно явно отправить на обработчик «выше», ключевым словом \code{throw} (чаще всего, используется \code{RuntimeException} с конструктором копирования).
|
||
|
||
Когда какой-то метод выбрасывает исключение у разработчика есть два основных пути:
|
||
\begin{itemize}
|
||
\item обязанность вынести объявление этого исключения в сигнатуру метода, что будет говорить тем, кто его вызывает о том, что в методе может возникнуть исключение;
|
||
\item исключение необходимо непосредственно в методе обработать, иначе ничего не скомпилируется.
|
||
\end{itemize}
|
||
|
||
В случае, если объявление исключения выносится в сигнатуру, вызывающий метод должен обработать это исключение точно таким-же образом -- либо в вызове, либо вынести в сигнатуру. Исключением из этого правила является класс \code{RuntimeException}. Все его наследники, включая его самого, обрабатывать не обязательно. Обычно, уже по названию понятно что случилось, и, помимо говорящих названий, там содержится много информации, например, номер строки, вызвавшей исключительную ситуацию.
|
||
|
||
\begin{frm} \info Общее правило работы с исключениями одно -- если исключение штатное -- его надо сразу обработать, если нет -- надо дождаться, пока программа упадёт.
|
||
\end{frm}
|
||
|
||
Общий вид оператора try...catch можно описать следующим образом:
|
||
\begin{verbatim}
|
||
try {
|
||
метод, выбрасывающий исключение
|
||
} catch (имя класса исключения и идентификатор) {
|
||
команды, обрабатывающие исключение
|
||
}
|
||
\end{verbatim}
|
||
|
||
Если произошло исключение, объект исключения попадает в \code{catch}, и управление ходом выполнения программы попадает в эту секцию. Чаще всего, здесь содержится код, помогающей программе не завершиться. Очень часто в процессе разработки нужно сделать так, чтобы в процессе выполнения что-то конкретное об исключении выводилось на экран, для этого у экземпляра есть метод \code{getMessage()}.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Получение сообщения из объекта исключения},label={lst:except-msg}]
|
||
System.out.println(e.getMessage());
|
||
\end{lstlisting}
|
||
|
||
Ещё чаще бывает, что выполнение программы после выбрасывания исключения не имеет смысла и нужно, чтобы программа завершилась. В этом случае принято выбрасывать новое RuntimeException, передав в него экземпляр пойманного исключения, используя конструктор копирования.
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={«Проброс» исключения на основе пойманного},label={lst:except-rethrow}]
|
||
try {
|
||
// ...
|
||
} catch(Exception e) {
|
||
throw new runtimeexception(e);
|
||
}
|
||
\end{lstlisting}
|
||
|
||
\textbf{Второй вариант обработки исключений} -- в сигнатуре метода пишется
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработка исключений в сигнатуре},label={lst:except-throws}]
|
||
throws IOException,
|
||
\end{lstlisting}
|
||
|
||
и, через запятую, все остальные возможные исключения этого метода. После этого, с ним не будет проблем исполнения, но у метода который его вызовет -- появилась необходимость обработать все checked исключения вызываемого. И так далее наверх.
|
||
|
||
\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{Пример}
|
||
Для примера обработки исключений, возникающих на разных этапах работы приложения (жизненного цикла объекта) предлагается описать класс (листинг \hrf{lst:test-stream}), бизнес логика которого подразумевает создание, чтение некоторой информации, например, как если бы нужно было прочитать байт из файла, и закрытие потока чтения, то есть возврат файла обратно под управление ОС.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Экспериментальный класс},label={lst:test-stream}]
|
||
public class TestStream {
|
||
TestStream() {
|
||
System.out.println("constructor");
|
||
}
|
||
int read() {
|
||
System.out.println("read");
|
||
return 1;
|
||
}
|
||
public void close() {
|
||
System.out.println("close");
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
То есть, способ работы с объектом данного класса (полностью без ошибок и других нештатных ситуаций) будет иметь следующий вид
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Работа в штатном режиме},label={lst:test-stream-main}]
|
||
TestStream stream = new TestStream();
|
||
int a = stream.read()
|
||
stream.close()
|
||
\end{lstlisting}
|
||
|
||
Для примера, внутри метода чтения создаётся \code{FileInputStream} который может генерировать обязательный к проверке на этапе написания кода \code{FileNotFoundException}, который является наследником \code{IOException}, который, в свою очередь, наследуется от \code{Exception}.
|
||
|
||
Возникает два варианта: либо обернуть в \code{try...catch}, либо совершенно непонятно, как должна обрабатываться данная исключительная ситуация, и обработать её должна сторона, которая вызывает метод чтения, в таком случае пишется, что метод может выбрасывать исключения. И тогда \code{TestStream} компилируется без проблем, а вот \code{main} скомпилироваться уже не может. В нём нужно оборачивать в \code{try...catch}.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Метод чтения}]
|
||
int read() {
|
||
FileInputStream s = new FileInputStream("file.txt");
|
||
System.out.println("read");
|
||
return 1;
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработка исключения}]
|
||
try {
|
||
TestStream stream = new TestStream();
|
||
int a = stream.read()
|
||
stream.close()
|
||
} catch (FileNotFoundException e) {
|
||
e.printStackTrace();
|
||
}
|
||
\end{lstlisting}
|
||
|
||
\begin{frm} \excl Важный момент. Задачи бывают разные. Исключения -- это инструмент, который нетривиально работает. Важно при написании кода понять, возникающая исключительная ситуация -- штатная, или нештатная. В большинстве случаев -- ситуации нештатные, поэтому надо «уронить» приложение и разбираться с тем, что именно произошло. Допустим, для вашего приложения обязательно какой-то файл должен быть, без него дальше нет смысла продолжать. Что делать, если его нет? Ситуация явно нештатная. Самое плохое, что можно сделать -- ничего не делать. Это самое страшное, когда программа повела себя как-то не так, а ни мы, разработчики, ни пользователь об этом даже не узнали. Допустим, мы хотим прочитать файл, вывести в консоль, но мы в обработчике исключения просто выводим стектрейс куда-то, какому-то разработчику в среду разработки, и наши действительно важные действия не выполнились. Надо завершать работу приложения. Как завершать? \code{throw new RuntimeException(e)}. Крайне редко случаются ситуации, когда у исключения достаточно распечатать стектрейс.
|
||
\end{frm}
|
||
|
||
Потоки ввода-вывода всегда нужно закрывать. Предположим, что в тестовом потоке открылся файл, из него что-то прочитано, потом метод завершился с исключением, а файл остался незакрытым, ресурсы заняты. Дописав класс \code{TestStream} при работе с ним, возвращаем из \code{read} единицу и логируем, что всё прочитали в \code{main}.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Экспериментальный класс}]
|
||
public class TestStream {
|
||
TestStream() {
|
||
System.out.println("constructor");
|
||
}
|
||
int read() throws IOException {
|
||
throw new IOException("read except");
|
||
System.out.println("read");
|
||
return 1;
|
||
}
|
||
public void close() {
|
||
System.out.println("close");
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Далее представим, что в методе \code{read} что-то пошло не так, выбрасываем исключение, и видим в консоли, что поток создан, произошло исключение, конец программы. Очевидно, поток не закрылся. Что делать?
|
||
|
||
Делать секцию \code{finally}. Секция \code{finally} будет выполнена в любом случае, не важно, будет ли поймано секциями \code{catch} какое-то исключение, или нет. Возникает небольшая проблема видимости, объявление идентификатора тестового потока необходимо вынести за пределы секции \code{try}.
|
||
|
||
Теперь немного неприятностей. Написанный блок \code{finally}, вроде решает проблему с закрытием потока. А как быть, если исключение возникло при создании этого потока, в конструкторе?
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проблема в конструкторе}]
|
||
public class TestStream {
|
||
TestStream() throws IOException {
|
||
throw new IOException("construct except");
|
||
System.out.println("constructor");
|
||
}
|
||
int read() throws IOException {
|
||
throw new IOException("read except");
|
||
System.out.println("read");
|
||
return 1;
|
||
}
|
||
public void close() {
|
||
System.out.println("close");
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Метод закрытия будет пытаться выполниться от ссылки на \code{null}. Недопустимо.
|
||
|
||
\begin{frm} \info При возникновении в конструкторе потока \code{IOException} - получим \code{NullPointerException} в блоке \code{finally}.
|
||
\end{frm}
|
||
|
||
Очевидное решение -- поставить в секции \code{finally} условие, и если поток не равен \code{null}, закрывать. Это точно сработает. Меняем тактику.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проблема при закрытии}]
|
||
public class TestStream {
|
||
TestStream() throws IOException {
|
||
throw new IOException("construct except");
|
||
System.out.println("constructor");
|
||
}
|
||
int read() throws IOException {
|
||
throw new IOException("read except");
|
||
System.out.println("read");
|
||
return 1;
|
||
}
|
||
public void close() throws IOException {
|
||
throw new IOException("close except");
|
||
System.out.println("close");
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Конструктор отрабатывает нормально. Метод чтения всё ещё генерирует исключение, но и в методе закрытия что-то пошло не так, и вылетело исключение. Нужно оборачивать в \code{try...catch}. Итоговый код, работающий с классом будет иметь следующий вид.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработка исключений, насколько это возможно}]
|
||
TestStream stream = null;
|
||
try {
|
||
stream = new TestStream();
|
||
int a = stream.read()
|
||
stream.close()
|
||
} catch (FileNotFoundException e) {
|
||
e.printStackTrace();
|
||
} catch (IOException e) {
|
||
e.printStackTrace();
|
||
} finally {
|
||
try {
|
||
stream.close();
|
||
} catch (NullPointerException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Но и тут возможно наткнуться на неприятность. Допустим, что необходимо в любом случае ронять приложение.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проблемы подавленных исключений}]
|
||
// ...
|
||
catch (IOException e) {
|
||
throw new RuntimeException(e);
|
||
} finally {
|
||
try {
|
||
stream.close();
|
||
} catch (NullPointerException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Тогда если \code{try} поймал исключение, и выкинул его, потом \code{finally} всё равно выполнится, и второе исключение перекроет (подавит) первое, никто его не увидит. Хотя по логике, первое для работы важнее. Так было до Java 1.8.
|
||
|
||
\subsubsection{try-with-resources block}
|
||
Начиная с версии Java 1.8 разработчику предоставляется механизм \textbf{\code{try}-с-ресурсами}. Поток -- это ресурс, абстрактное понятие. Выражаясь строго формально, разработчик должен \textit{реализовать интерфейс} \code{Closeable}. В этом интерфейсе содержится всего один метод \code{close()}, который умеет бросать \code{IOException}. В классе тестового потока нужно всего лишь переопределить этот метод данного интерфейса.
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Реализация интерфейса закрытия потока}]
|
||
public class TestStream implements Closeable {
|
||
// ...
|
||
|
||
@Override
|
||
public void close() throws IOException {
|
||
throw new IOException("close except");
|
||
System.out.println("close");
|
||
}
|
||
}
|
||
\end{lstlisting}
|
||
|
||
Все потоки начиная с Java 1.8 реализуют интерфейс \code{Closeable}. Работа с такими классами имеет лаконичный вид
|
||
|
||
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Реализация блока try-with-resources}]
|
||
try (TestStream stream = new TestStream()) {
|
||
int a = stream.read();
|
||
} catch (IOException e) {
|
||
throw new RuntimeException(e)
|
||
}
|
||
\end{lstlisting}
|
||
|
||
В данном коде не нужно закрывать поток явно, это будет сделано автоматически в следствие реализации интерфейса. Если ломается метод \code{read()}, то try-с-ресурсами всё равно корректно закроет поток. При сломанном методе закрытия и сломанном методе чтения одновременно, JVM запишет наверх основное исключение, но и выведет «подавленное» исключение, вторичное в стектрейс. Рекомендуется по возможности всегда использовать try-с-ресурсами.
|
||
|
||
\subsubsection{Наследование и полиморфизм исключений}
|
||
Наследование и полиморфизм для исключений -- тема не очень большая и не сложная, потому что ничего нового в информацию про классы и объекты исключения не привносят. Застрять внимание на объектно-ориентированном программировании в исключениях не целесообразно, потому что исключения это \textit{тоже классы} и те исключения, которые используются в программе -- уже какие-то наследники других исключений.
|
||
|
||
Генерируются и выбрасываются \textit{объекты исключений}, единственное, что важно упомянуть это то, что подсистема исключений работает не тривиально. Но разработчик может создавать собственные исключения с собственными смыслами и сообщениями и точно также их выбрасывать вместо стандартных. Наследоваться возможно от любых исключений, единственное что важно, это то, что не рекомендуется наследоваться от классов \code{Throwable} и \code{Error}, когда описываете исключение.
|
||
|
||
Механика checked и unchecked исключений сохраняется при наследовании, поэтому создав наследник \code{RuntimeException} вы получаете не проверяемые на этапе написания кода исключения.
|
||
|
||
\subsection*{Практическое задание}
|
||
\begin{enumerate}
|
||
\item напишите два наследника класса Exception: ошибка преобразования строки и ошибка преобразования столбца
|
||
\item разработайте исключения-наследники так, чтобы они информировали пользователя в формате ожидание/реальность
|
||
\item для проверки напишите программу, преобразующую квадратный массив целых чисел 5х5 в сумму чисел в этом массиве, при этом, программа должна выбросить исключение, если строк или столбцов в исходном массиве окажется не 5.
|
||
\end{enumerate}
|
||
|
||
\newpage
|
||
\printnomenclature[40mm]
|
||
|
||
\end{document}
|