#База знаний
- 24 фев 2021
-
13
Разбираемся, что такое исключения, зачем они нужны и как с ними работать.
vlada_maestro / shutterstock
Хлебом не корми — дай кому-нибудь про Java рассказать.
Из этой статьи вы узнаете:
- что такое исключения (Exceptions);
- как они возникают и чем отличаются от ошибок (Errors);
- зачем нужна конструкция try-catch;
- как разобраться в полученном исключении
- и как вызвать исключение самому.
Код вашей программы исправно компилируется и запускается, только вот вместо желанного результата вы видите непонятный текст. Строчки его будто кричат на вас, аж побагровели.
За примером далеко ходить не надо: сделаем то, что нам запрещали ещё в школе, — поделим на ноль.
public static void main(String[] args) {
hereWillBeTrouble(42, 0);
}
public static void hereWillBeTrouble(int a, int b) {
int oops = a / b;
System.out.println(oops);
}
А получим вот что:
Это и есть исключение.
«Исключение» — сокращение от слов «исключительный случай». Это ситуация, в которой программа не может продолжить работу или её работа становится бессмысленной. Причём речь не только о нештатных ситуациях — исключения бывают и намеренными, такие разработчик вызывает сам.
Это интересно. Исключения в Java появились уже в первой версии языка. А вот в языках, где их нет, вместо них возвращают коды ошибок.
У всех классов исключений есть общий класс-предок Throwable, от него наследуются классы Error и Exception, базовые для всех прочих.
Error — это критические условия, в которых работа программы должна быть завершена. Например, когда при выполнении программы закончилась память, произошёл сбой в системе или виртуальной машине. Не будем задерживаться на этой ветке, поскольку документация Java говорит:
Error is the superclass of all the exceptions from which ordinary programs are not ordinarily expected to recover.
Что в переводе означает: ошибки (Error) — это такие исключительные ситуации, в которых восстанавливать работу программы не предполагается.
То есть это проблемы, которые нельзя (недопустимо) исправлять на ходу. Всё, что нам остаётся, — извиниться перед пользователем и впредь писать программы, где возникнет меньше подобных ситуаций. Например, не допускать такой глубокой рекурсии, как в коде ниже:
static void notGood() {
System.out.println("Только не снова!");
notGood();
}
При работе этого метода у нас возникнет ошибка: Exception in thread «main» java.lang.StackOverflowError — стек вызовов переполнился, так как мы не указали условие выхода из рекурсии.
А теперь об Exception. Эти исключительные ситуации возникают, если разработчик допустил невыполнимую операцию, не предусмотрел особые случаи в бизнес-логике программы (или сообщает о них с помощью исключений).
1. Невыполнимая операция
Мир не рухнул, как в случае с Error, просто Java не знает, что делать дальше. Как раз из этого разряда деление на ноль в начале статьи: и правда, какое значение тогда присвоить переменной oops?
Убедитесь сами, что исключение класса ArithmeticException наследуется как раз от Exception.
Стоит запомнить. В IntelliJ IDEA, чтобы увидеть положение класса в иерархии, выберите его и нажмите Ctrl + H (или на пункт Type Hierarchy в меню Navigate).
Другая частая ситуация — обращение к несуществующему элементу массива. Например, у нас в нём десять элементов, а мы пытаемся обратиться к одиннадцатому.
2. Особый случай в бизнес-логике программы
Классика. Программируем задачу о перевозке волка, козы и капусты через реку: в лодке может быть только два пассажира, но волка с козой и козу с капустой нельзя оставлять на берегу вместе. Это и есть особый случай в бизнес-логике, который нельзя нарушать.
Или пользователь вводит дату начала некоторого периода и дату его окончания. Вторая дата не может быть раньше первой.
Или, допустим, у нас есть метод, который читает файл. Сам метод написан верно. Пользователь передал в него корректный путь. Только вот у этого работника нет права читать этот файл (его роль и права обусловлены предметной областью). Что же тогда методу возвращать? Вернуть-то нечего, ведь метод не отработал. Самое очевидное решение — выдать исключение.
В дерево исключений мы ещё углубимся, а сейчас посмотрим, что и как с ними делают.
Простейший вариант — ничего; возникает исключение — программа просто прекращает работать.
Чтобы убедиться в этом, выполним код:
public static void main(String[] args) {
hereWillBeTrouble(42, 0);
}
public static void hereWillBeTrouble(int a, int b) {
System.out.println("Всё, что было до...");
int oops = a / b;
System.out.println(oops);
System.out.println("Всё, что будет после...");
}
Так и есть: до деления на ноль код выполнялся, а после — нет.
Это интересно: когда возникает исключение, программисты выдают что-то вроде «код [вы]бросил исключение» или «код кинул исключение». А глагол таков потому, что все исключения — наследники класса Throwable, что значит «бросаемый» / «который можно бросить».
Второе, что можно делать с исключениями, — это их обрабатывать.
Для этого нужно заключить кусок кода, который может вызвать исключение, в конструкцию try-catch.
Как это работает: если в блоке try возникает исключение, которое указано в блоке catch, то исполнение блока try прервётся и выполнится код из блока catch.
Например:
public static void main(String[] args) {
hereWillBeTrouble();
}
private static void hereWillBeTrouble(int a, int b) {
int oops;
try {
System.out.println("Всё, что было до...");
oops = a / b;
System.out.println(oops);
System.out.println("Всё, что будет после...");
} catch (ArithmeticException e) {
System.out.println("Говорили же не делить на ноль!");
oops = 0;
}
System.out.println("Метод отработал");
}
Разберём этот код.
Если блок try кинет исключение ArithmeticException, то управление перехватит блок catch, который выведет строку «Говорили же не делить на ноль!», а значение oops станет равным 0.
После этого программа продолжит работать как ни в чём не бывало: выполнится код после блока try-catch, который сообщит: «Метод отработал».
Проверьте сами: запустите код выше. Вызовите метод hereWillBeTrouble с любыми значениями аргументов кроме нулевого b. Если в блоке try не возникнет исключений, то его код выполнится целиком, а в блок catch мы даже не попадём.
Есть ещё и третий вариант — пробросить исключение наверх. Но об этом в следующей статье.
Вернёмся к первой картинке. Посмотрим, что нам сказала Java, когда произошло исключение:
Начинаем разбирать сверху вниз:
— это указание на поток, в котором произошло исключение. В нашей простой однопоточной программе это поток main.
— какое исключение брошено. У нас это ArithmeticException. А java.lang.ArithmeticException — полное название класса вместе с пакетом, в котором он размещается.
— весточка, которую принесло исключение. Дело в том, что одно и то же исключение нередко возникает по разным причинам. И тут мы видим стандартное пояснение «/ by zero» — из-за деления на ноль.
— это самое интересное: стектрейс.
Стектрейс (Stack trace) — это упорядоченный список методов, сквозь которые исключение пронырнуло.
У нас оно возникло в методе hereWillBeTrouble на 8-й строке в классе Main (номер строки и класс указаны в скобках синим). А этот метод, в свою очередь, вызван методом main на 3-й строке класса Main.
Стектрейсы могут быть довольно длинными — из десятков методов, которые вызывают друг друга по цепочке. И они здорово помогают расследовать неожиданно кинутое исключение.
Советую закреплять теорию на практике. Поэтому вернитесь в блок про Error и вызовите метод notGood — увидите любопытный стектрейс.
Всё это время мы имели дело с исключением, которое бросает Java-машина — при делении на ноль. Но как вызвать исключение самим?
Раз исключение — это объект класса, то программисту всего-то и нужно, что создать объект с нужным классом исключения и бросить его с помощью оператора throw.
public static void main(String[] args) {
hereWillBeTrouble(42, 0);
}
private static void hereWillBeTrouble(int a, int b) {
if (b == 0) {
throw new ArithmeticException("ты опять делишь на ноль?");
}
int oops = a / b;
System.out.println(oops);
}
При создании большинства исключений первым параметром в конструктор можно передать сообщение — мы как раз сделали так выше.
А получим мы то же самое, что и в самом первом примере, только вместо стандартной фразы «/by zero» теперь выдаётся наш вопрос-пояснение «ты опять делишь на ноль?»:
В следующей статье мы углубимся в иерархию исключений Java, узнаем про их разделение на checked и unchecked, а также о том, что ещё интересного можно с ними делать.
Научитесь: Профессия Java-developer PRO
Узнать больше
Глава 9
Обработка исключений
Основные навыки и понятия
- Представление об иерархии исключений
- Использование ключевых слов try и catch
- Последствия неперехвата исключений
- Применение нескольких операторов catch
- Перехват исключений, генерируемых подклассами
- Вложенные блоки try
- Генерирование исключений
- Представление о членах класса Throwable
- Использование ключевого слова finally
- Использование ключевого слова throws
- Представление о исключениях, встроенные в Java
- Создание специальных классов исключений
В этой главе речь пойдет об обработке исключительный ситуаций, или просто исключений. Исключение — это ошибка, возникающая в процессе выполнения программы. Используя подсистему обработки исключений Java, можно контролировать реакцию программы на появление ошибок в ходе ее выполнения. Средства обработки исключений в том или ином виде присутствуют практически во всех современных языках программирования. Можно смело утверждать, что в Java подобные инструментальные средства отличаются большей гибкостью, более понятны и удобны в употреблении по сравнению с большинством других языков программирования.
Преимущество обработки исключений заключается в том, что она автоматически предусматривает реакцию на многие ошибки, избавляя от необходимости писать вручную соответствующий код. Например, в некоторых старых языках программирования предусматривается возврат специального кода при возникновении ошибки в ходе выполнения метода. Этот код приходится проверять вручную при каждом вызове метода. Такой подход к обработке ошибок вручную трудоемок и чреват погрешностями. Обработка исключений упрощает этот процесс, давая возможность определять в программе блок кода, называемый обработчиком исключения и автоматически выполняющийся при возникновении ошибки. Это избавляет от необходимости проверять вручную, насколько удачно или неудачно была выполнена та или иная операция или вызов метода. Если возникнет ошибка, все необходимые действия по ее обработке выполнит обработчик исключений.
В Java определены стандартные исключения для наиболее часто встречающихся программных ошибок, в том числе деления на нуль или попытки открыть несуществующий файл. Для того чтобы обеспечить требуемую реакцию на конкретную ошибку, в программу следует включить соответствующий обработчик событий. Исключения широко применяются в библиотеке Java API.
Иерархия исключений
В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable имеются два непосредственных подкласса: Exception и Error. Исключения типа Error относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в прикладной программе, как правило, не предусматривается. В связи с этим исключения данного типа не будут описываться в этой книге.
Ошибки, связанные с выполнением действий в программе, представлены отдельными подклассами, производными от класса Exception. К этой категории, в частности, относятся ошибки деления на нуль, выхода за границы массива и обращения к файлам. Подобные ошибки следует обрабатывать в самой программе. Важным подклассом, производным от Exception, является класс RuntimeException, который служит для представления различных видов ошибок, часто встречающихся при выполнении программ.
Общее представление об обработке исключений
Для обработки исключений в Java предусмотрены пять ключевых слов: try, catch, throw, throws и finally. Они образуют единую подсистему, в которой использование одного ключевого слова почти всегда автоматически влечет за собой употребление другого. Каждое из упомянутых выше ключевых слов будет подробно рассмотрено далее в этой главе. Но прежде следует дать общее представление об их роли в процессе обработки исключений. Поэтому ниже поясняется вкратце, каким образом они действуют.
Операторы, в которых требуется отслеживать появление исключений, помещаются в блок try. Если в блоке try будет сгенерировано исключение, его можно перехватить и обработать нужным образом. Системные исключения генерируются автоматически. А для того чтобы сгенерировать исключение вручную, следует воспользоваться ключевым словом throw. Иногда возникает потребность обрабатывать исключения за пределами метода, в котором они возникают, и для этой цели служит ключевое слово throws. Если же некоторый фрагмент кода должен быть выполнен обязательно и независимо от того, возникнет исключение или нет, его следует поместить в блок finally.
Использование ключевых слов try и catch
Основными языковыми средствами обработки исключений являются ключевые слова try и catch. Они используются совместно. Это означает, что нельзя указать ключевое слово catch в коде, не указав ключевое слово try. Ниже приведена общая форма записи блоков try/catch, предназначенных для обработки исключений.
try {
// Блок кода, в котором должны отслеживаться ошибки
}
catch (тип_исключения_1 объект_исключения) {
// Обработчик исключения тип_исключения_1
}
catch (тип_исключения_2 объект_исключения) {
// Обработчик исключения тип_исключения_2
}
В скобках, следующих за ключевым словом catch, указываются тип исключения и переменная, ссылающаяся на объект данного типа. Когда возникает исключение, оно перехватывается соответствующим оператором catch, обрабатывающим это исключение. Как следует из приведенной выше общей формы записи, с одним блоком try может быть связано несколько операторов catch. Тип исключения определяет, какой именно оператор catch будет выполняться. Так, если тип исключения соответствует типу оператора catch, то именно он и будет выполнен, а остальные операторы catch — пропущены. При перехвате исключения переменной, указанной в скобках после ключевого слова catch, присваивается ссылка на объект_исключения.
Следует иметь в виду, что если исключение не генерируется, блок try завершается обычным образом и ни один из его операторов catch не выполняется. Выполнение программы продолжается с первого оператора, следующего за последним оператором catch. Таким образом, операторы catch выполняются только при появлении исключения.
На заметку.
В версии JDK 7 внедрена новая форма оператора try, поддерживающая автоматическое управления ресурсами и называемая оператором try с ресурсами. Более подробно она описывается в главе 10 при рассмотрении потоков ввода-вывода, в том числе и тех, что связаны с файлами, поскольку потоки ввода-вывода относятся к числу ресурсов, наиболее употребительных в прикладных программах.
Простой пример обработки исключений
Рассмотрим простой пример, демонстрирующий перехват и обработку исключения. Как известно, попытка обратиться за границы массива приводит к ошибке, и виртуальная машина Java генерирует соответствующее исключение ArraylndexOutOf BoundsException. Ниже приведен код программы, в которой намеренно создаются условия для появления данного исключения, которое затем перехватывается.
// Демонстрация обработки исключений,
class ExcDemol {
public static void main (String args[]) {
int nums[] = new int[4];
// Создание блока try.
try {
System.out.println("Before exception is generated.");
// Попытка обратиться за границы массива.
nums[7] = 10;
System.out.println("this won't be displayed");
}
// Перехват исключения в связи с обращением за границы массива.
catch (ArraylndexOutOfBoundsException exc) {
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
Результат выполнения данной программы выглядит следующим образом:
Before exception is generated.
Index out-of-bounds!
After catch statement.
Несмотря на всю простоту данного примера программы, он наглядно демонстрирует несколько важных особенностей обработки исключений. Во-первых, код, подлежащий проверке на наличие ошибок, помещается в блок try. И во-вторых, когда возникает исключение (в данном случае это происходит при попытке обратиться за границы массива), выполнение блока try прерывается и управление получает блок catch. Следовательно, явного обращения к блоку catch не происходит, но переход к нему осуществляется лишь при определенном условии, возникающем в ходе выполнения программы. Так, оператор вызова метода println(), следующий за выражением, в котором происходит обращение к несуществующему элементу массива, вообще не выполняется. По завершении блока catch выполнение программы продолжается с оператора, следующего за этим блоком. Таким образом, обработчик исключений предназначен для устранения программных ошибок, приводящих к исключительным ситуациям, а также для обеспечения нормального продолжения исполняемой программы.
Как упоминалось выше, если в блоке try не возникнут исключения, операторы в блоке catch не получат управление и выполнение программы продолжится после блока catch. Для того чтобы убедиться в этом, измените в предыдущей программе строку кода
на следующую строку кода:
Теперь исключение не возникнет и блок catch не выполнится.
Важно понимать, что исключения отслеживаются во всем коде в блоке try. К их числу относятся исключения, которые могут быть сгенерированы методом, вызываемым из блока try. Исключения, возникающие в вызываемом методе, перехватываются операторами в блоке catch, связанном с блоком try. Правда, это произойдет лишь в том случае, если метод не обрабатывает исключения самостоятельно. Рассмотрим в качестве примера следующую программу:
/* Исключение может быть сгенерировано одним методом,
а перехвачено другим. */
class ExcTest {
// сгенерировать исключение
static void genException() {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// Здесь генерируется исключение в связи с
// обращением за границы массива.
nums[7] = 10;
System.out.println("this won't be displayed");
}
}
class ExcDemo2 {
public static void main(String args[]) {
try {
ExcTest.genException() ;
}
//А здесь исключение перехватывается.
catch (ArraylndexOutOfBoundsException exc) {
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
Выполнение этой версии программы дает такой же результат, как и при выполнении ее предыдущей версии. Этот результат приведен ниже.
Before exception is generated.
Index out-of-bounds!
After catch statement.
Метод genException() вызывается из блока try, и поэтому генерируемое, но не перехватываемое в нем исключение перехватывается далее в блоке catch в методе main(). Если бы метод genException() сам перехватывал исключение, оно вообще не дошло бы до метода main().
Последствия неперехвата исключений
Перехват стандартного исключения Java, продемонстрированный в предыдущем примере, позволяет предотвратить завершение программы вследствие ошибки. Генерируемое исключение должно быть перехвачено и обработано. Если исключение не обрабатывается в программе, оно будет обработано виртуальной машиной Java. Но дело в том, что по умолчанию виртуальная машина Java аварийно завершает программу, выводя сообщение об ошибке и трассировку стека исключений. Допустим, в предыдущем примере попытка обращения за границы массива не отслеживается и исключение не перехватывается, как показано ниже.
// Обработка ошибки средствами виртуальной машины Java,
class NotHandled {
public static void main(String args[]) {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// Попытка обращения за границы массива,
nums[7] = 10;
}
}
При появлении ошибки, связанной с обращением за границы массива, выполнение программы прекращается и выводится следующее сообщение:
Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: 7 at NotHandled.main(NotHandled.java:9)
Оно полезно на этапе отладки, но пользователям программы эта информация вряд ли нужна. Именно поэтому очень важно, чтобы программы обрабатывали исключения самостоятельно и не поручали эту задачу виртуальной машине Java.
Как упоминалось выше, тип исключения должен соответствовать типу, указанному в операторе catch. В противном случае исключение не будет перехвачено. Так, в приведенном ниже примере программы делается попытка перехватить исключение, связанное с обращением за границы массива, с помощью оператора catch, в котором указан тип ArithmeticException еще одного встроенного в Java исключения. При неправильном обращении к массиву будет сгенерировано исключение ArraylndexOutOfBoundsException, не соответствующее типу, указанному в операторе catch. В результате программа будет завершена аварийно.
// Эта программа не будет работать нормально!
class ExcTypeMismatch {
public static void main(String args[]) {
int nums[] = new int[4];
try {
System.out.println("Before exception is generated.");
// При выполнении следующего оператора возникает
// исключение ArraylndexOutOfBoundsException
nums[7] = 10;
System.out.println("this won’t be displayed");
}
/* Исключение, связанное с обращением за границы массива,
нельзя обработать с помощью оператора catch, в котором
указан тип исключения ArithmeticException. */
catch (ArithmeticException exc) {
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
Ниже приведен результат выполнения данной программы.
Before exception is generated.
Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: 7
at ExcTypeMismatch.main(ExcTypeMismatch.java:10)
Нетрудно заметить, что оператор catch, в котором указан тип исключения ArithmeticException, не может перехватить исключение ArraylndexOutOfBoundsException.
Обработка исключений — изящный способ устранения программных ошибок
Одно из главных преимуществ обработки исключений заключается в том, что она позволяет вовремя отреагировать на ошибку в программе и затем продолжить ее выполнение. В качестве примера рассмотрим еще одну программу, в которой элементы одного массива делятся на элементы другого. Если при этом происходит деление на нуль, то генерируется исключение ArithmeticException. Обработка подобного исключения заключается в том, что программа уведомляет об ошибке и затем продолжает свое выполнение. Таким образом, попытка деления на нуль не приведет к аварийному завершению программы из-за ошибки при ее выполнении. Вместо этого ошибка обрабатывается изящно, не прерывая выполнение программы.
// Изящная обработка исключения и продолжение выполнения программы,
class ExcDemo3 {
public static void main(String args[]) {
int numer[] = { 4, 8, 16, 32, 64, 128 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can't divide by Zero!");
}
}
}
}
Результат выполнения данной программы выглядит следующим образом:
4 / 2 is 2
Can't divide by Zero!
16/ 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
Данный пример демонстрирует еще одну важную особенность: обработанное исключение удаляется из системы. Иными словами, на каждом шаге цикла блок try выполняется в программе сызнова, а все возникшие ранее исключения считаются обработанными. Благодаря этому в программе могут обрабатываться повторяющиеся ошибки.
Применение нескольких операторов catch
Как пояснялось ранее, с блоком try можно связать несколько операторов catch. Обычно разработчики так и поступают на практике. Каждый из операторов catch должен перехватывать отдельный тип исключений. Например, в приведенной ниже программе обрабатываются как исключения, связанные с обращением за границы массива, так и ошибки деления на нуль.
// Применение нескольких операторов catch. '
class ExcDemo4 {
public static void main(String args[]) {
// Здесь массив numer длиннее массива denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
// За блоком try следует несколько блоков catch подряд,
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can't divide by Zero!");
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
}
}
}
}
Выполнение этой программы дает следующий результат:
4 / 2 is 2
Can't divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
No matching element found.
No matching element found.
Как подтверждает приведенный выше результат выполнения программы, в каждом блоке оператора catch обрабатывается свой тип исключения.
Вообще говоря, выражения с операторами catch проверяются в том порядке, в котором они встречаются в программе. И выполняется лишь тот из них, который соответствует типу возникшего исключения. А остальные блоки операторов catch просто игнорируются.
Перехват исключений, генерируемых подклассами
В отношении подклассов следует отметить еще одну интересную особенность применения нескольких операторов catch: условие перехвата исключений для суперкласса будет справедливо и для любых его подклассов. Например, класс Throwable является суперклассом для всех исключений, поэтому для перехвата всех возможных исключений в операторах catch следует указывать тип Throwable. Если же требуется перехватывать исключения типа суперкласса и типа подкласса, то в блоке операторов первым должен быть указан тип исключения, генерируемого подклассом. В противном случае вместе с исключением типа суперкласса будут перехвачены и все исключения производных от него классов. Это правило соблюдается автоматически, и если указать первым тип исключения, генерируемого суперклассом, то будет создан недостижимый код, поскольку условие перехвата исключения, генерируемого подклассом, никогда не будет выполнено. А ведь недостижимый код в Java считается ошибкой.
Рассмотрим в качестве примера следующую программу
//В операторах catch исключения типа подкласса должны
// предшествовать исключениям типа суперкласса,
class ExcDemo5 {
public static void main(String args[]) {
// Здесь массив numer длиннее массива denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
// Перехват исключения от подкласса.
catch (ArraylndexOutOfBoundsException exc) {
System.out.println("No matching element found.");
}
// Перехват исключения от суперкласса.
catch (Throwable exc) {
System.out.println("Some exception occurred.");
}
}
}
}
Ниже приведен результат выполнения данной программы.
4 / 2 is 2
Some exception occurred.
16 / 4 is 4
32 / 4 is 8
Some exception occurred.
128 / 8 is 16
No matching element found.
No matching element found.
В данном случае оператор catch (Throwable) перехватывает все исключения, кроме ArraylndexOutOfBoundsException. Соблюдение правильного порядка следования операторов catch приобретает особое значение в том случае, когда исключения генерируются в самой программе.
Вложенные блоки try
Блоки try могут быть вложенными друг в друга. Исключение, возникшее во внутреннем блоке try и не перехваченное связанным с ним блоком catch, распростра¬няется далее во внешний блок try и обрабатывается связанным с ним блоком catch. Такой порядок обработки исключений демонстрируется в приведенном ниже примере программы, где исключение ArraylndexOutOfBoundsException не перехватывается во внутреннем блоке catch, но обрабатывается во внешнем.
// Применение вложенных блоков try.
class NestTrys {
public static void main(String args[]) {
// Массив numer длиннее, чем массив denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
// Вложенные блоки try.
try { // Внешний блок try.
for(int i=0; i<numer.length; i++) {
try { // Внутренний блок try.
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]) ;
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can't divide by Zero!");
}
}
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
System.out.println("Fatal error - program terminated.");
}
}
}
Выполнение этой программы может дать, например, следующий результат:
4 / 2 is 2
Can't divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can't divide by Zero!
128 / 8 is 16
No matching element found.
Fatal error - program terminated.
В данном примере исключение, которое может быть обработано во внутреннем блоке try (в данном случае ошибка деления на нуль), не мешает дальнейшему выполнению программы. А вот ошибка превышения границ массива перехватывается во внешнем блоке try, что приводит к аварийному завершению программы.
Ситуация, продемонстрированная в предыдущем примере, является не единственной причиной для применения вложенных блоков try, хотя она встречается очень часто. В этом случае вложенные блоки try помогают по-разному обрабатывать разные типы ошибок. Одни ошибки невозможно устранить, а для других достаточно предусмотреть сравнительно простые действия. Внешний блок try чаще всего используется для перехвата критических ошибок, а менее серьезные ошибки обрабатываются во внутреннем блоке try.
Генерирование исключений
В предыдущих примерах программ обрабатывались исключения, автоматически генерируемые виртуальной машиной Java. Но генерировать исключения можно и вручную, используя для этого оператор throw. Ниже приведена общая форма этого оператора.
где объект_исключения должен быть объектом класса, производного от класса Throwable.
Ниже приведен пример программы, демонстрирующий применение оператора throw. В этой программе исключение ArithmeticException генерируется вручную.
// Генерирование исключения вручную,
class ThrowDemo {
public static void main(String args[]) {
try {
System.out.println("Before throw.");
// Генерирование исключения.
throw new ArithmeticException() ;
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Exception caught.");
}
System.out.println("After try/catch block.");
}
}
Выполнение этой программы дает следующий результат:
Before throw.
Exception caught.
After try/catch block.
Обратите внимание на то, что исключение ArithmeticException генерируется с помощью ключевого слова new в операторе throw. Дело в том, что оператор throw генерирует исключение в виде объекта. Поэтому после ключевого слова throw недостаточно указать только тип исключения, нужно еще создать объект для этой цели.
Повторное генерирование исключений
Исключение, перехваченное блоком catch, может быть повторно сгенерировано для обработки другим аналогичным блоком. Чаще всего повторное генерирование исключений применяется с целью предоставить разным обработчикам доступ к исключению. Так, например, повторное генерирование имеет смысл в том случае, если один обработчик оперирует одним свойством исключения, а другой обработчик ориентирован на другое его свойство. Повторно сгенерированное исключение не может быть перехвачено тем же самым блоком catch. Оно распространяется в другие блоки catch.
Ниже приведен пример программы, демонстрирующий повторное генерирование исключений.
//•Повторное генерирование исключений,
class Rethrow {
public static void genException() {
// Массив numer длиннее маесивв denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can11 divide by Zero!");
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
throw exc; // Повторное генерирование исключения.
}
}
}
}
class RethrowDemo {
public static void main(String args[]) {
try {
Rethrow.genException();
}
catch(ArraylndexOutOfBoundsException exc) {
// Перехват повторно сгенерированного включения.
System.out.println("Fatal error - " +
"program terminated.");
}
}
}
В данной программе ошибка деления на нуль обрабатывается локально в методе genException(), а при попытке обращения за границы массива исключение генерируется повторно. На этот раз оно перехватывается в методе main().
Подробнее о классе Throwable
В приведенных до сих примерах программ только перехватывались исключения, но не выполнялось никаких действий над представляющими их объектами. В выражении оператора catch указываются тип исключения и параметр, принимающий объект исключения. А поскольку все исключения представлены подклассами, производными от класса Throwable, то они поддерживают методы, определенные в этом классе. Некоторые наиболее употребительные методы из класса Throwable приведены в табл. 9.1.
Таблица 9.1. Наиболее употребительные методы из класса Throwable
Метод | Описание |
---|---|
Throwable filllnStackTrace() |
Возвращает объект типа Throwable, содержащий полную трассировку стека исключений. Этот объект пригоден для повторного генерирования исключений |
String getLocalizedMessage() |
Возвращает описание исключения, локализованное по региональным стандартам |
String getMessage() |
Возвращает описание исключения |
void printStackTrace() |
Выводит трассировку стека исключений |
void printStackTrace(PrintStream stream) |
Выводит трассировку стека исключений в указанный поток |
void printStackTrace(PrintWriter stream) |
Направляет трассировку стека исключений в указанный поток |
String toString() |
Возвращает объект типа String, содержащий полное описание исключения. Этот метод вызывается из метода println() при выводе объекта типа Throwable |
Среди методов, определенных в классе Throwable, наибольший интерес представляют методы pr intStackTrace() и toString(). С помощью метода printStackTrace() можно вывести стандартное сообщение об ошибке и запись последовательности вызовов методов, которые привели к возникновению исключения, А метод toString() позволяет получить стандартное сообщение об ошибке. Этот метод также вызывается в том случае, когда объект исключения передается в качестве параметра методу println(). Применение этих методов демонстрируется в следующем примере программы:
// Применение методов из класса Throwable.
class ExcTest {
static void genException() {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// сгенерировать исключение в связи с попыткой
// обращения за границы массива
nums[7] = 10;
System.out.println("this won't be displayed");
}
}
class UseThrowableMethods {
public static void main(String args[]) {
try {
ExcTest.genException() ;
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("Standard message is: ");
System.out.println(exc) ;
System.out.println("nStack trace: ");
exc.printStackTrace();
}
System.out.println("After catch statement.");
}
}
Результат выполнения данной программы выглядит следующим образом:
Before exception is generated.
Standard message is:
java.lang.ArraylndexOutOfBoundsException: 7
Stack trace:
java.lang.ArraylndexOutOfBoundsException: 7
at ExcTest.genException(UseThrowableMethods.java:10)
at UseThrowableMethods.main(UseThrowableMethods.java:19)
After catch statement.
Использование ключевого слова finally
Иногда требуется определить кодовый блок, который должен выполняться по завершении блока try/catch. Допустим, в процессе работы программы возникло исключение, требующее ее преждевременного завершения. Но в программе открыт файл или установлено сетевое соединение, а следовательно, файл нужно закрыть, а соединение разорвать. Для выполнения подобных операций нормального завершения программы удобно воспользоваться ключевым словом finally.
Для того чтобы определить код, который должен выполняться по завершении блока try/catch, нужно указать блок finally в конце последовательности операторов try/catch. Ниже приведена общая форма записи блока try/catch вместе с блоком finally.
try {
// Блок кода, в котором отслеживаются ошибки.
}
catch (тип_исключения_1 объект_исключения) {
// Обработчик исключения тип_исключения_1
}
catch (тип_исключения_2 объект_исключения) {
// Обработчик исключения тип_исключения_2
}
//. . .
finally {
// Код блока finally
}
Блок finally выполняется всегда по завершении блока try/catch независимо от того, какое именно условие к этому привело. Следовательно, блок finally получит управление как при нормальной работе программы, так и при возникновении ошибки. Более того, он будет вызван даже в том случае, если в блоке try или в одном из блоков catch будет присутствовать оператор return для немедленного возврата из метода.
Ниже приведен краткий пример программы, демонстрирующий применение блока finally.
// Применение блока finally,
class UseFinally {
public static void genException(int what) {
int t;
int nums[] = new int[2];
System.out.println("Receiving " + what);
try {
switch(what) {
case 0:
t = 10 / what; // сгенерировать ошибку деления на нуль
break;
case 1:
nums[4] = 4; // сгенерировать ошибку обращения к массиву
break;
case 2:
return; // возвратиться из блока try
}
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can1t divide by Zero!");
return; // возвратиться из блока catch
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
}
// Этот блок выполняется независимо от того, каким
// образом завершается блок try/catch.
finally {
System.out.println("Leaving try.");
}
}
}
class FinallyDemo {
public static void main(String args[]) {
for(int i=0; i < 3; i++) {
UseFinally.genException(i);
System.out.println() ;
}
}
}
В результате выполнения данной программы получается следующий результат:
Receiving О
Can't divide by Zero!
Leaving try.
Receiving 1
No matching element found.
Leaving try.
Receiving 2
Leaving try.
Нетрудно заметить, что блок finally выполняется независимо от того, каким об¬
разом завершается блок try/catch.
Использование ключевого слова throws
Иногда исключения нецелесообразно обрабатывать в том методе, в котором они возникают. В таком случае их следует указывать с помощью ключевого слова throws. Ниже приведена общая форма объявления метода, в котором присутствует ключевое слово throws.
возвращаемый_тип имя_метода(список_параметров) throws список_исключений {
// Тело метода
}
В списке исключений через запятую указываются исключения, которые может генерировать метод.
Возможно, вам покажется странным, что в ряде предыдущих примеров ключевое слово throws не указывалось при генерировании исключений за пределами методов. Дело в том, что исключения, генерируемые подклассом Error или RuntimeException, можно и не указывать в списке оператора throws. Исполняющая система Java по умолчанию предполагает, что метод может их генерировать. А исключения всех остальных типов следует непременно объявить с помощью ключевого слова throws. Если этого не сделать, возникнет ошибка при компиляции.
Пример применения оператора throws уже был представлен ранее в этой книге.
Напомним, что при организации ввода с клавиатуры в метод main() потребовалось
включить следующее выражение:
throws java.io.IOException
Теперь вы знаете, зачем это было нужно. При вводе данных может возникнуть исключение IOException, а на тот момент вы еще не знали, как оно обрабатывается. Поэтому мы и указали, что исключение должно обрабатываться за пределами метода main(). Теперь, ознакомившись с исключениями, вы сможете без труда обработать исключение IOException самостоятельно.
Рассмотрим пример, в котором осуществляется обработка исключения IOException. В методе prompt() отображается сообщение, а затем выполняется ввод символов с клавиатуры. Такой ввод данных может привести к возникновению исключения IOException. Но это исключение не обрабатывается в методе prompt(). Вместо этого в объявлении метода указан оператор throws, т.е. обязанности по обработке данного исключению поручаются вызывающему методу. В данном случае вызывающим является метод main(), в котором и перехватывается исключение.
// Применение ключевого слова throws,
class ThrowsDemo {
// Обратите внимание на оператор throws в объявлении метода.
public static char prompt(String str)
throws java.io.IOException {
System.out.print(str + ": ");
return (char) System.in.read() ;
}
public static void main(String args[]) {
char ch;
try {
// В методе prompt() может быть сгенерировано исключение,
// поэтому данный метод следует вызывать в блоке try.
ch = prompt("Enter a letter");
}
catch(java.io.IOException exc) {
System.out.println("I/O exception occurred.");
ch = 'X';
}
System.out.println("You pressed " + ch);
}
}
Обратите внимание на одну особенность приведенного выше примера. Класс IOException относится к пакету java. io. Как будет разъяснено в главе 10, в этом пакете содержатся многие языковые средства Java для организации ввода-вывода. Следовательно, пакет java.io можно импортировать, а в программе указать только имя класса IOException.
Новые средства обработки исключений, внедренные в версии JDK 7
С появлением версии JDK 7 механизм обработки исключений в Java был значительно усовершенствован благодаря внедрению трех новых средств. Первое из них поддерживает автоматическое управление ресурсами, позволяющее автоматизировать процесс освобождения таких ресурсов, как файлы, когда они больше не нужны. В основу этого средства положена расширенная форма оператора try, называемая оператором try с ресурсами и описываемая в главе 10 при рассмотрении файлов. Второе новое средство называется многократным перехватом, а третье — окончательным или более точным повторным генерированием исключений. Два последних средства рассматриваются ниже.
Многократный перехват позволяет перехватывать два или более исключения одним оператором catch. Как пояснялось ранее, после оператора try можно (и даже принято) указывать два или более оператора catch. И хотя каждый блок оператора catch, как правило, содержит свою особую кодовую последовательность, нередко в двух или более блоках оператора catch выполняется одна и та же кодовая последовательность, несмотря на то, что в них перехватываются разные исключения. Вместо того чтобы перехватывать каждый тип исключения в отдельности, теперь можно воспользоваться единым блоком оператора catch для обработки исключений, не дублируя код.
Для организации многократного перехвата следует указать список исключений в одном операторе catch, разделив их типы оператором поразрядного ИЛИ. Каждый параметр многократного перехвата неявно указывается как final. (По желанию модификатор доступа final можно указать и явным образом, но это совсем не обязательно.) А поскольку каждый параметр многократного перехвата неявно указывается как final, то ему нельзя присвоить новое значение.
В приведенной ниже строке кода показывается, каким образом многократный перехват исключений ArithmeticException и ArraylndexOutOfBoundsException указывается в одном операторе catch.
catch(final ArithmeticException | ArraylndexOutOfBoundsException e) {
Ниже приведен краткий пример программы, демонстрирующий применение многократного перехвата исключений.
// Применение средства многократного перехвата исключений.
// Примечание: для компиляции этого кода требуется JDK 7
// или более поздняя версия данного комплекта,
class MultiCatch {
public static void main(String args[]) {
int a=88, b=0;
int result;
char chrs[] = { 'А', 'В', 'C' };
for(int i=0; i < 2; i++) {
try {
if (i == 0)
// сгенерировать исключение ArithmeticException
result = а / b;
else
// сгенерировать исключение ArraylndexOutOfBoundsException
chrs[5] = 'X';
}
// В этом операторе catch организуется перехват обоих исключений,
catch(ArithmeticException | ArraylndexOutOfBoundsException е) {
System.out.println("Exception caught: " + e);
}
}
System.out.println("After multi-catch.");
}
}
В данном примере программы исключение ArithmeticException генерируется при попытке деления на нуль, а исключение ArraylndexOutOfBoundsException — при попытке обращения за границы массива chrs. Оба исключения перехватываются одним оператором catch.
Средство более точного повторного генерирования исключений ограничивает этот процесс лишь теми проверяемыми типами исключений, которые генерируются в соответствующем блоке try и не обрабатываются в предыдущем блоке оператора catch, а также относятся к подтипу или супертипу указываемого параметра. И хотя такая возможность требуется нечасто, ничто не мешает теперь воспользоваться ею в полной мере. А для организации окончательного повторного генерирования исключений параметр оператора catch должен быть, по существу, указан как final. Это означает, что ему нельзя присвоить новое значение в блоке catch. Он может быть указан как final явным образом, хотя это и не обязательно.
Встроенные в Java исключения
В стандартном пакете java. lang определены некоторые классы, представляющие стандартные исключения Java. Часть из них использовалась в предыдущих примерах программ. Наиболее часто встречаются исключения из подклассов стандартного класса RuntimeException. А поскольку пакет java. lang импортируется по умолчанию во все программы на Java, то исключения, производные от класса RuntimeException, становятся доступными автоматически. Их даже обязательно включать в список оператора throws. В терминологии языка Java такие исключения называют непроверяемыми, поскольку компилятор не проверяет, обрабатываются или генерируются подобные исключения в методе. Непроверяемые исключения, определенные в пакете java.lang, приведены в табл. 9.2, тогда как в табл. 9.3 — те исключения из пакета j ava. lang, которые следует непременно включать в список оператора throws при объявлении метода, если, конечно, в методе содержатся операторы, способные генерировать эти исключения, а их обработка не предусмотрена в теле метода. Такие исключения принято называть проверяемыми. В Java предусмотрен также ряд других исключений, определения которых содержатся в различных библиотеках классов. К их числу можно отнести упоминавшееся ранее исключение IOException.
Таблица 9.2. Непроверяемые исключения, определенные в пакете java.lang
Исключение | Описание |
---|---|
ArithmeticException | Арифметическая ошибка, например попытка деления на нуль |
ArraylndexOutOfBoundsException | Попытка обращения за границы массива |
ArrayStoreException | Попытка ввести в массив элемент, несовместимый с ним по типу |
ClassCastException | Недопустимое приведение типов |
EnumConstNotPresentException | Попытка использования нумерованного значения, которое не было определено ранее |
IllegalArgumentException | Недопустимый параметр при вызове метода |
IllegalMonitorStateException | Недопустимая операция контроля, например, ожидание разблокировки потока |
IllegalStateException | Недопустимое состояние среды выполнения или приложения |
IllegalThreadStateException | Запрашиваемая операция несовместима с текущим состоянием потока |
IndexOutOfBoundsException | Недопустимое значение индекса |
NegativeArraySizeException | Создание массива отрицательного размера |
NullPointerException | Недопустимое использование пустой ссылки |
NumberFormatException | Неверное преобразование символьной строки в число |
SecurityException | Попытка нарушить систему защиты |
StringlndexOutOfBounds | Попытка обращения к символьной строке за ее границами |
TypeNotPresentException | Неизвестный тип |
UnsupportedOperationException | Неподдерживаемая операция |
Таблица 9.3. Проверяемые исключения, определенные в пакете java.lang
Исключение | Описание |
---|---|
ClassNotFoundException | Класс не найден |
CloneNotSupportedException | Попытка клонирования объекта, не реализующего интерфейс Cloneable |
IllegalAccessException | Доступ к классу запрещен |
InstantiationException | Попытка создания объекта абстрактного класса или интер¬фейса |
InterruptedException | Прерывание одного потока другим |
NoSuchFieldException | Требуемое поле не существует |
NoSuchMethodException | Требуемый метод не существует |
ReflectiveOperationException | Суперкласс исключений, связанных с рефлексией (добавлен в версии JDK 7) |
Создание подклассов, производных от класса Exception
Несмотря на то что встроенные в Java исключения позволяют обрабатывать большинство ошибок, механизм обработки исключений не ограничивается только этими ошибками. В частности, можно создавать исключения для обработки потенциальных ошибок в прикладной программе. Создать исключение несложно. Для этого достаточно определить подкласс, производный от класса Exception, который, в свою очередь, является подклассом, порожденным классом Throwable. В создаваемый подкласс не обязательно включать реализацию каких-то методов. Сам факт существования такого подкласса позволяет использовать его в качестве исключения.
В классе Exception не определены новые методы. Он лишь наследует методы, предоставляемые классом Throwable. Таким образом, все исключения, включая и создаваемые вами, содержат методы класса Throwable. Конечно же, вы вольны переопределить в создаваемом вами классе один или несколько методов.
Ниже приведен пример, в котором создается исключение NonlntResultException. Оно генерируется в том случае, если результатом деления двух целых чисел является дробное число. В классе NonlntResultException содержатся два поля, предназначенные для хранения целых чисел, а также конструктор. В нем также переопределен метод toString(), что дает возможность выводить описание исключения с помощью метода println().
// Применение специально создаваемого исключения.
// создать исключение
class NonlntResultException extends Exception {
int n;
int d;
NonlntResultException(int i, int j) {
n = i;
d = j;
}
public String toString() {
return "Result of " + n + " / " + d +
" is non-integer.";
}
}
class CustomExceptDemo {
public static void main(String args[]) {
// В массиве numer содержатся нечетные числа,
int numer[] = { 4, 8, 15, 32, 64, 127, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
if((numer[i]%2) != 0)
throw new
NonlntResultException(numer[i], denom[i]);
System.out.println(numer[i] + " / " +
denom[i] + 11 is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// перехватить исключение
System.out.println("Can11 divide by Zero!");
}
catch (ArraylndexOutOfBoundsException exc) {
// перехватить исключение
System.out.println("No matching element found.");
}
catch (NonlntResultException exc) {
System.out.println(exc) ;
}
}
}
}
Результат выполнения данной программы выглядит следующим образом:
4 / 2 is 2
Can't divide by Zero!
Result of 15 / 4 is non-integer.
32 / 4 is 8
Can't divide by Zero!
Result of 127 / 8 is non-integer.
No matching element found.
No matching element found.
Пример для опробования 9.1.
Добавление исключений в класс очереди
В этом проекте предстоит создать два класса исключении, которые будут использоваться классом очереди, разработанным в примере для опробования 8.1. Эти исключения должны указывать на переполнение и опустошение очереди, а генерировать их будут методы put() и get() соответственно. Ради простоты эти исключения добавляются в класс FixedQueue, но вы можете без труда внедрить их в любые другие классы очереди, разработанные в примере для опробования 8.1.
Последовательность действий
- Создайте файл QExcDemo.java.
- Определите следующие исключения в файле QExcDemo.java:
/* Пример для опробования 9.1. Добавление обработчиков исключений в класс очереди. */ // Исключение, указывающее на переполнение очереди, class QueueFullException extends Exception { int size; QueueFullException(int s) { size = s; } public String toString() { return "nQueue is full. Maximum size is " + size; } } // Исключение, указывающее на опустошение очереди, class QueueEmptyException extends Exception { public String toString() { return "nQueue is empty."; } }
Исключение QueueFullException генерируется при попытке поместить элемент в уже заполненную очередь, а исключение QueueEmptyException — в ответ на попытку извлечь элемент из пустой очереди.
- Измените класс FixedQueue таким образом, чтобы при возникновении ошибки он генерировал исключение. Соответствующий код приведен ниже. Введите этот код в файл QExcDemo.java.
// Класс, реализующий очередь фиксированного размера // для хранения символов. class FixedQueue implements ICharQ { private char q[]; // Массив для хранения элементов очереди, private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди. // создать пустую очередь заданного размера public FixedQueue(int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0; } // поместить символ в очередь public void put(char ch) throws QueueFullException { if(putloc==q.length-1) throw new QueueFullException(q.length-1); putloc++; q[putloc] = ch; } // извлечь символ из очереди public char get() throws QueueEmptyException { if(getloc == putloc) throw new QueueEmptyException(); getloc++; return q[getloc]; } }
Добавление исключений в класс FixedQueue выполняется в два этапа. Сначала в определении методов get() и put() указывается оператор throws с типом генерируемого исключения. А затем в этих методах организуется генерирование исключений при возникновении ошибок. Используя исключения, можно организовать обработку ошибок в вызывающей части программы наиболее рациональным способом. Как вы помните, в предыдущих версиях рассматриваемой здесь программы выводились только сообщения об ошибках. А генерирование исключений является более профессиональным подходом к разработке данной программы.
- Для опробования усовершенствованного класса FixedQueue введите в файл QExcDemo.java приведенный ниже исходный код класса QExcDemo.
// Демонстрация исключений при обращении с очередью, class QExcDemo { public static void main(String args[]) { FixedQueue q = new FixedQueue(10); char ch; int i; try { // Переполнение очереди. for(i=0; i < 11; i++) { System.out.print("Attempting to store : " + (char) ('A' + i)); q.put((char) (fA' + i)); System.out.println(" - OK"); } System.out.println(); } catch (QueueFullException exc) { System.out.println(exc); } System.out.println(); try { // Попытка извлечь символ из пустой очереди. for(i=0; i < 11; i++) { System.out.print("Getting next char: "); ch = q.get(); System.out.println(ch); } } catch (QueueEmptyException exc) { System.out.println(exc); } } }
- Класс FixedQueue реализует интерфейс ICharQ, в котором определены методы get() и put(), и поэтому интерфейс ICharQ необходимо изменить таким образом, чтобы в нем отражалось наличие операторов throws. Ниже приведен видоизмененный соответственно код интерфейса ICharQ. Не забывайте о том, что он должен храниться в файле ICharQjava.
// Интерфейс очереди для хранения символов с генерированием исключений, public interface ICharQ { // поместить символ в очередь void put(char ch) throws QueueFullException; // извлечь символ из очереди char get() throws QueueEmptyException; }
- Скомпилируйте сначала новую версию исходного файла IQChar. j ava, а затем исходный файл QExcDemo. java и запустите программу QExcDemo на выполнение. В итоге вы получите следующий результат ее выполнения:
Attempting to store A - OK Attempting to store В - OK Attempting to store С - OK Attempting to store D - OK Attempting to store E - OK Attempting to store F - OK Attempting to store G - OK Attempting to store H - OK Attempting to store I - OK Attempting to store J - OK Attempting to store К Queue is full. Maximum size is 10 Getting next char: A Getting next char: В Getting next char: С Getting next char: D Getting next char: E Getting next char: F Getting next char: G Getting next char: H Getting next char: I Getting next char: J Getting next char: Queue is empty.
Упражнение для самопроверки по материалу главы 9
- Какой класс находится на вершине иерархии исключений?
- Объясните вкратце, как пользоваться ключевыми словами try и catch?
- Какая ошибка допущена в приведенном ниже фрагменте кода?
// ... vals[18] = 10; catch (ArraylndexOutOfBoundsException exc) { // обработать ошибку }
- Что произойдет, если исключение не будет перехвачено?
- Какая ошибка допущена в приведенном ниже фрагменте кода?
class A extends Exception { ... class В extends А { ... // ... try { // ... } catch (A exc) { ... } catch (В exc) { ... }
- Может ли внутренний блок catch повторно генерировать исключение, которое будет обработано во внешнем блоке catch?
- Блок finally — последний фрагмент кода, выполняемый перед завершением программы. Верно или неверно? Обоснуйте свой ответ.
- Исключения какого типа необходимо явно объявлять с помощью оператора throws, включаемого в объявление метода?
- Какая ошибка допущена в приведенном ниже фрагменте кода?
class MyClass { // ... } // ... throw new MyClass();
- Отвечая на вопрос 3 упражнения для самопроверки по материалу главы 6, вы создали класс Stack. Добавьте в него специальные исключения для реагирования на попытку поместить элемент в переполненный стек и извлечь элемент из пустого стека.
- Какими тремя способами можно сгенерировать исключение?
- Назовите два подкласса, производных непосредственно от класса Throwable.
- Что такое многократный перехват?
- Следует ли перехватывать в программе исключения типа Error?
Это первая часть статьи, посвященной такому языковому механизму Java как исключения (вторая (checked/unchecked) вот). Она имеет вводный характер и рассчитана на начинающих разработчиков или тех, кто только приступает к изучению языка.
Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).
1. Ключевые слова: try, catch, finally, throw, throws
2. Почему используем System.err, а не System.out
3. Компилятор требует вернуть результат (или требует молчать)
4. Нелокальная передача управления (nonlocal control transfer)
5. try + catch (catch — полиморфен)
6. try + catch + catch + …
7. try + finally
8. try + catch + finally
9. Вложенные try + catch + finally
1. Ключевые слова: try, catch, finally, throw, throws
Механизм исключительных ситуаций в Java поддерживается пятью ключевыми словами
- try
- catch
- finally
- throw
- throws
«Магия» (т.е. некоторое поведение никак не отраженное в исходном коде и потому неповторяемое пользователем) исключений #1 заключается в том, что catch, throw, throws можно использовать исключительно с java.lang.Throwable или его потомками.
throws:
Годится
public class App {
public static void main(String[] args) throws Throwable {}
}
Не годится
public class App {
public static void main(String[] args) throws String {}
}
>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'
catch:
Годится
public class App {
public static void main(String[] args) {
try {
} catch (Throwable t) {}
}
}
Не годится
public class App {
public static void main(String[] args) {
try {
} catch (String s) {}
}
}
>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'
throw:
Годится
public class App {
public static void main(String[] args) {
// Error - потомок Throwable
throw new Error();
}
}
Не годится
public class App {
public static void main(String[] args) {
throw new String("Hello!");
}
}
>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'
Кроме того, throw требуется не-null аргумент, иначе NullPointerException в момент выполнения
public class App {
public static void main(String[] args) {
throw null;
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.NullPointerException
throw и new — это две независимых операции. В следующем коде мы независимо создаем объект исключения и «бросаем» его
public class App {
public static void main(String[] args) {
Error ref = new Error(); // создаем экземпляр
throw ref; // "бросаем" его
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
Однако, попробуйте проанализировать вот это
public class App {
public static void main(String[] args) {
f(null);
}
public static void f(NullPointerException e) {
try {
throw e;
} catch (NullPointerException npe) {
f(npe);
}
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.StackOverflowError
2. Почему используем System.err, а не System.out
System.out — buffered-поток вывода, а System.err — нет. Таким образом вывод может быть как таким
public class App {
public static void main(String[] args) {
System.out.println("sout");
throw new Error();
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
>> sout
Так и вот таким (err обогнало out при выводе в консоль)
public class App {
public static void main(String[] args) {
System.out.println("sout");
throw new Error();
}
}
>> sout
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
Давайте это нарисуем
буфер сообщений
+----------------+
+->| msg2 msg1 msg0 | --> out
/ +----------------+
/ +-> +--------+
ВАШЕ ПРИЛОЖЕНИЕ | КОНСОЛЬ|
+-> +--------+
/
+------------------------> err
нет буфера, сразу печатаем
когда Вы пишете в System.err — ваше сообщение тут же выводится на консоль, но когда пишете в System.out, то оно может на какое-то время быть буферизированно. Stacktrace необработанного исключение выводится через System.err, что позволяет им обгонять «обычные» сообщения.
3. Компилятор требует вернуть результат (или требует молчать)
Если в объявлении метода сказано, что он возвращает НЕ void, то компилятор зорко следит, что бы мы вернули экземпляр требуемого типа или экземпляр типа, который можно неявно привести к требуемому
public class App {
public double sqr(double arg) { // надо double
return arg * arg; // double * double - это double
}
}
public class App {
public double sqr(double arg) { // надо double
int k = 1; // есть int
return k; // можно неявно преобразовать int в double
}
}
// на самом деле, компилятор сгенерирует байт-код для следующих исходников
public class App {
public double sqr(double arg) { // надо double
int k = 1; // есть int
return (double) k; // явное преобразование int в double
}
}
вот так не пройдет (другой тип)
public class App {
public static double sqr(double arg) {
return "hello!";
}
}
>> COMPILATION ERROR: Incompatible types. Required: double. Found: java.lang.String
Вот так не выйдет — нет возврата
public class App {
public static double sqr(double arg) {
}
}
>> COMPILATION ERROR: Missing return statement
и вот так не пройдет (компилятор не может удостовериться, что возврат будет)
public class App {
public static double sqr(double arg) {
if (System.currentTimeMillis() % 2 == 0) {
return arg * arg; // если currentTimeMillis() - четное число, то все ОК
}
// а если нечетное, что нам возвращать?
}
}
>> COMPILATION ERROR: Missing return statement
Компилятор отслеживает, что бы мы что-то вернули, так как иначе непонятно, что должна была бы напечатать данная программа
public class App {
public static void main(String[] args) {
double d = sqr(10.0); // ну, и чему равно d?
System.out.println(d);
}
public static double sqr(double arg) {
// nothing
}
}
>> COMPILATION ERROR: Missing return statement
Из-забавного, можно ничего не возвращать, а «повесить метод»
public class App {
public static double sqr(double arg) {
while (true); // Удивительно, но КОМПИЛИРУЕТСЯ!
}
}
Тут в d никогда ничего не будет присвоено, так как метод sqr повисает
public class App {
public static void main(String[] args) {
double d = sqr(10.0); // sqr - навсегда "повиснет", и
System.out.println(d); // d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
}
public static double sqr(double arg) {
while (true); // Вот тут мы на века "повисли"
}
}
Компилятор пропустит «вилку» (таки берем в квадрат ИЛИ висим)
public class App {
public static double sqr(double arg) {
if (System.currentTimeMillis() % 2 == 0) {
return arg * arg; // ну ладно, вот твой double
} else {
while (true); // а тут "виснем" навсегда
}
}
}
Но механизм исключений позволяет НИЧЕГО НЕ ВОЗВРАЩАТЬ!
public class App {
public static double sqr(double arg) {
throw new RuntimeException();
}
}
Итак, у нас есть ТРИ варианта для компилятора
public class App {
public static double sqr(double arg) {// согласно объявлению метода ты должен вернуть double
long time = System.currentTimeMillis();
if (time % 2 == 0) {
return arg * arg; // ок, вот твой double
} else if (time % 2 == 1) { {
while (true); // не, я решил "повиснуть"
} else {
throw new RuntimeException(); // или бросить исключение
}
}
}
Но КАКОЙ ЖЕ double вернет функция, бросающая RuntimeException?
А НИКАКОЙ!
public class App {
public static void main(String[] args) {
// sqr - "сломается" (из него "выскочит" исключение),
double d = sqr(10.0); // выполнение метода main() прервется в этой строчке и
// d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
System.out.println(d); // и печатать нам ничего не придется!
}
public static double sqr(double arg) {
throw new RuntimeException(); // "бросаем" исключение
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException
Подытожим: бросаемое исключение — это дополнительный возвращаемый тип. Если ваш метод объявил, что возвращает double, но у вас нет double — можете бросить исключение. Если ваш метод объявил, что ничего не возвращает (void), но у вам таки есть что сказать — можете бросить исключение.
Давайте рассмотрим некоторый пример из практики.
Задача: реализовать функцию, вычисляющую площадь прямоугольника
public static int area(int width, int height) {...}
важно, что задание звучит именно так, в терминах предметной области — «вычислить площадь прямоугольника», а не в терминах решения «перемножить два числа»:
public static int area(int width, int height) {
return width * height; // тут просто перемножаем
}
Вопрос: что делать, если мы обнаружили, что хотя бы один из аргументов — отрицательное число?
Если просто умножить, то мы пропустили ошибочные данные дальше. Что еще хуже, возможно, мы «исправили ситуацию» — сказали что площадь прямоугольника с двумя отрицательными сторонами -10 и -20 = 200.
Мы не можем ничего не вернуть
public static int area(int width, int height) {
if (width < 0 || height < 0) {
// у вас плохие аргументы, извините
} else {
return width * height;
}
}
>> COMPILATION ERROR: Missing return statement
Можно, конечно, отписаться в консоль, но кто ее будет читать и как определить где была поломка. При чем, вычисление то продолжится с неправильными данными
public static int area(int width, int height) {
if (width < 0 || height < 0) {
System.out.println("Bad ...");
}
return width * height;
}
Можно вернуть специальное значение, показывающее, что что-то не так (error code), но кто гарантирует, что его прочитают, а не просто воспользуются им?
public static int area(int width, int height) {
if (width < 0 || height < 0) {
return -1; // специальное "неправильное" значение площади
}
return width * height;
}
Можем, конечно, целиком остановить виртуальную машину
public static int area(int width, int height) {
if (width < 0 || height < 0) {
System.exit(0);
}
return width * height;
}
Но «правильный путь» таков: если обнаружили возможное некорректное поведение, то
1. Вычисления остановить, сгенерировать сообщение-поломку, которое трудно игнорировать, предоставить пользователю информацию о причине, предоставить пользователю возможность все починить (загрузить белье назад и повторно нажать кнопку старт)
public static int area(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Negative sizes: w = " + width + ", h = " + height);
}
return width * height;
}
4. Нелокальная передача управления (nonlocal control transfer)
Механизм исключительных ситуация (исключений) — это механизм НЕЛОКАЛЬНОЙ ПЕРЕДАЧИ УПРАВЛЕНИЯ.
Что под этим имеется в виду?
Программа, в ходе своего выполнения (точнее исполнения инструкций в рамках отдельного потока), оперирует стеком («стопкой») фреймов. Передача управления осуществляется либо в рамках одного фрейма
public class App {
public static void main(String[] args) {
// Пример: ОПЕРАТОР ПОСЛЕДОВАТЕЛЬНОСТИ
int x = 42; // первый шаг
int y = x * x; // второй шаг
x = x * y; // третий шаг
...
}
}
public class App {
public static void main(String[] args) {
// Пример: ОПЕРАТОР ВЕТВЛЕНИЯ
if (args.length > 2) { первый шаг
// второй шаг или тут
...
} else {
// или тут
...
}
// третий шаг
...
}
}
public class App {
public static void main(String[] args) {
// Пример: ОПЕРАТОР ЦИКЛА do..while
int x = 1;
do {
...
} while (x++ < 10);
...
}
}
и другие операторы.
Либо передача управления происходит в «стопке» фреймов между СОСЕДНИМИ фреймами
- вызов метода: создаем новый фрейм, помещаем его на верхушку стека и переходим в него
- выход из метода: возвращаемся к предыдущему фрейму (через return или просто кончились инструкции в методе)
return — выходим из ОДНОГО фрейма (из фрейма #4(метод h()))
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // вернулись
} // выходим из текущего фрейма, кончились инструкции
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); //вернулись
} // выходим из текущего фрейма, кончились инструкции
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // вернулись
} // выходим из текущего фрейма, кончились инструкции
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.RETURN");
return; // выходим из текущего фрейма по 'return'
}
System.err.println(". . . #4.out"); // ПРОПУСКАЕМ
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.RETURN
>> . . #3.out
>> . #2.out
>> #1.out
throw — выходим из ВСЕХ фреймов
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // ПРОПУСТИЛИ!
}
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); // ПРОПУСТИЛИ!
}
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // ПРОПУСТИЛИ!
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
При помощи catch мы можем остановить летящее исключение (причина, по которой мы автоматически покидаем фреймы).
Останавливаем через 3 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g()) + фрейм #2(метод f())
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
try {
f(); // создаем фрейм, помещаем в стек, передаем в него управление
} catch (Error e) { // "перехватили" "летящее" исключение
System.err.println("#1.CATCH"); // и работаем
}
System.err.println("#1.out"); // работаем дальше
}
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); // ПРОПУСТИЛИ!
}
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // ПРОПУСТИЛИ!
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> #1.CATCH
>> #1.out
Обратите внимание, стандартный сценарий работы был восстановлен в методе main() (фрейм #1)
Останавливаем через 2 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g())
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // вернулись и работаем
}
public static void f() {
System.err.println(". #2.in");
try {
g(); // создаем фрейм, помещаем в стек, передаем в него управление
} catch (Error e) { // "перехватили" "летящее" исключение
System.err.println(". #2.CATCH"); // и работаем
}
System.err.println(". #2.out"); // работаем дальше
}
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // ПРОПУСТИЛИ!
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> . #2.CATCH
>> . #2.out
>> #1.out
Останавливаем через 1 фрейм (фактически аналог return, просто покинули фрейм «другим образом»)
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // вернулись и работаем
}
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); // вернулись и работаем
}
public static void g() {
System.err.println(". . #3.in");
try {
h(); // создаем фрейм, помещаем в стек, передаем в него управление
} catch (Error e) { // "перехватили" "летящее" исключение
System.err.println(". . #3.CATCH"); // и работаем
}
System.err.println(". . #3.out"); // работаем дальше
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> . . #3.CATCH
>> . . #3.out
>> . #2.out
>> #1.out
Итак, давайте сведем все на одну картинку
// ---Используем RETURN--- // ---Используем THROW---
// Выход из 1-го фрейма // Выход из ВСЕХ (из 4) фреймов
#1.in #1.in
. #2.in . #2.in
. . #3.in . . #3.in
. . . #4.in . . . #4.in
. . . #4.RETURN . . . #4.THROW
. . #3.out RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
. #2.out
#1.out
// ---Используем THROW+CATCH---
// Выход из 3-х фреймов // Выход из 2-х фреймов // Выход из 1-го фрейма
#1.in #1.in #1.in
. #2.in . #2.in . #2.in
. . #3.in . . #3.in . . #3.in
. . . #4.in . . . #4.in . . . #4.in
. . . #4.THROW . . . #4.THROW . . . #4.THROW
#1.CATCH . #2.CATCH . . #3.CATCH
#1.out . #2.out . . #3.out
#1.out . #2.out
#1.out
5. try + catch (catch — полиморфен)
Напомним иерархию исключений
Object
|
Throwable
/
Error Exception
|
RuntimeException
То, что исключения являются объектами важно для нас в двух моментах
1. Они образуют иерархию с корнем java.lang.Throwable (java.lang.Object — предок java.lang.Throwable, но Object — уже не исключение)
2. Они могут иметь поля и методы (в этой статье это не будем использовать)
По первому пункту: catch — полиморфная конструкция, т.е. catch по типу Parent перехватывает летящие экземпляры любого типа, который является Parent-ом (т.е. экземпляры непосредственно Parent-а или любого потомка Parent-а)
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (Exception e) { // catch по Exception ПЕРЕХВАТЫВАЕТ RuntimeException
System.err.print(" 2");
}
System.err.println(" 3");
}
}
>> 0 2 3
Даже так: в блоке catch мы будем иметь ссылку типа Exception на объект типа RuntimeException
public class App {
public static void main(String[] args) {
try {
throw new RuntimeException();
} catch (Exception e) {
if (e instanceof RuntimeException) {
RuntimeException re = (RuntimeException) e;
System.err.print("Это RuntimeException на самом деле!!!");
} else {
System.err.print("В каком смысле не RuntimeException???");
}
}
}
}
>> Это RuntimeException на самом деле!!!
catch по потомку не может поймать предка
public class App {
public static void main(String[] args) throws Exception { // пока игнорируйте 'throws'
try {
System.err.print(" 0");
if (true) {throw new Exception();}
System.err.print(" 1");
} catch (RuntimeException e) {
System.err.print(" 2");
}
System.err.print(" 3");
}
}
>> 0
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Exception
catch по одному «брату» не может поймать другого «брата» (Error и Exception не находятся в отношении предок-потомок, они из параллельных веток наследования от Throwable)
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new Error();}
System.err.print(" 1");
} catch (Exception e) {
System.err.print(" 2");
}
System.err.print(" 3");
}
}
>> 0
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
По предыдущим примерам — надеюсь вы обратили внимание, что если исключение перехвачено, то JVM выполняет операторы идущие ПОСЛЕ последних скобок try+catch.
Но если не перехвачено, то мы
1. не заходим в блок catch
2. покидаем фрейм метода с летящим исключением
А что будет, если мы зашли в catch, и потом бросили исключение ИЗ catch?
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2");
if (true) {throw new Error();} // но бросили Error
}
System.err.println(" 3"); // пропускаем - уже летит Error
}
}
>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
В таком случае выполнение метода тоже прерывается (не печатаем «3»). Новое исключение не имеет никакого отношения к try-catch
Мы можем даже кинуть тот объект, что у нас есть «на руках»
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2");
if (true) {throw e;} // и бросили ВТОРОЙ раз ЕГО ЖЕ
}
System.err.println(" 3"); // пропускаем - опять летит RuntimeException
}
}
>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.RuntimeException
И мы не попадем в другие секции catch, если они есть
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2");
if (true) {throw new Error();} // и бросили новый Error
} catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
System.err.print(" 3");
}
System.err.println(" 4");
}
}
>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
Обратите внимание, мы не напечатали «3», хотя у нас летит Error а «ниже» расположен catch по Error. Но важный момент в том, что catch имеет отношение исключительно к try-секции, но не к другим catch-секциям.
Как покажем ниже — можно строить вложенные конструкции, но вот пример, «исправляющий» эту ситуацию
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2.1");
try {
System.err.print(" 2.2");
if (true) {throw new Error();} // и бросили новый Error
System.err.print(" 2.3");
} catch (Throwable t) { // перехватили Error
System.err.print(" 2.4");
}
System.err.print(" 2.5");
} catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
System.err.print(" 3");
}
System.err.println(" 4");
}
}
>> 0 2.1 2.2 2.4 2.5 4
6. try + catch + catch + …
Как вы видели, мы можем расположить несколько catch после одного try.
Но есть такое правило — нельзя ставить потомка после предка! (RuntimeException после Exception)
public class App {
public static void main(String[] args) {
try {
} catch (Exception e) {
} catch (RuntimeException e) {
}
}
}
>> COMPILATION ERROR: Exception 'java.lang.RuntimeException' has alredy been caught
Ставить брата после брата — можно (RuntimeException после Error)
public class App {
public static void main(String[] args) {
try {
} catch (Error e) {
} catch (RuntimeException e) {
}
}
}
Как происходит выбор «правильного» catch? Да очень просто — JVM идет сверху-вниз до тех пор, пока не найдет такой catch что в нем указано ваше исключение или его предок — туда и заходит. Ниже — не идет.
public class App {
public static void main(String[] args) {
try {
throw new Exception();
} catch (RuntimeException e) {
System.err.println("catch RuntimeException");
} catch (Exception e) {
System.err.println("catch Exception");
} catch (Throwable e) {
System.err.println("catch Throwable");
}
System.err.println("next statement");
}
}
>> catch Exception
>> next statement
Выбор catch осуществляется в runtime (а не в compile-time), значит учитывается не тип ССЫЛКИ (Throwable), а тип ССЫЛАЕМОГО (Exception)
public class App {
public static void main(String[] args) {
try {
Throwable t = new Exception(); // ссылка типа Throwable указывает на объект типа Exception
throw t;
} catch (RuntimeException e) {
System.err.println("catch RuntimeException");
} catch (Exception e) {
System.err.println("catch Exception");
} catch (Throwable e) {
System.err.println("catch Throwable");
}
System.err.println("next statement");
}
}
>> catch Exception
>> next statement
7. try + finally
finally-секция получает управление, если try-блок завершился успешно
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
} finally {
System.err.println("finally");
}
}
}
>> try
>> finally
finally-секция получает управление, даже если try-блок завершился исключением
public class App {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
System.err.println("finally");
}
}
}
>> finally
>> Exception in thread "main" java.lang.RuntimeException
finally-секция получает управление, даже если try-блок завершился директивой выхода из метода
public class App {
public static void main(String[] args) {
try {
return;
} finally {
System.err.println("finally");
}
}
}
>> finally
finally-секция НЕ вызывается только если мы «прибили» JVM
public class App {
public static void main(String[] args) {
try {
System.exit(42);
} finally {
System.err.println("finally");
}
}
}
>> Process finished with exit code 42
System.exit(42) и Runtime.getRuntime().exit(42) — это синонимы
public class App {
public static void main(String[] args) {
try {
Runtime.getRuntime().exit(42);
} finally {
System.err.println("finally");
}
}
}
>> Process finished with exit code 42
И при Runtime.getRuntime().halt(42) — тоже не успевает зайти в finally
public class App {
public static void main(String[] args) {
try {
Runtime.getRuntime().halt(42);
} finally {
System.err.println("finally");
}
}
}
>> Process finished with exit code 42
exit() vs halt()
javadoc: java.lang.Runtime#halt(int status)
… Unlike the exit method, this method does not cause shutdown hooks to be started and does not run uninvoked finalizers if finalization-on-exit has been enabled. If the shutdown sequence has already been initiated then this method does not wait for any running shutdown hooks or finalizers to finish their work.
Однако finally-секция не может «починить» try-блок завершившийся исключение (заметьте, «more» — не выводится в консоль)
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
if (true) {throw new RuntimeException();}
} finally {
System.err.println("finally");
}
System.err.println("more");
}
}
>> try
>> finally
>> Exception in thread "main" java.lang.RuntimeException
Трюк с «if (true) {…}» требуется, так как иначе компилятор обнаруживает недостижимый код (последняя строка) и отказывается его компилировать
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
throw new RuntimeException();
} finally {
System.err.println("finally");
}
System.err.println("more");
}
}
>> COMPILER ERROR: Unrechable statement
И finally-секция не может «предотвратить» выход из метода, если try-блок вызвал return («more» — не выводится в консоль)
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
if (true) {return;}
} finally {
System.err.println("finally");
}
System.err.println("more");
}
}
>> try
>> finally
Однако finally-секция может «перебить» throw/return при помощи другого throw/return
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
return 0;
} finally {
return 1;
}
}
}
>> 1
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
throw new RuntimeException();
} finally {
return 1;
}
}
}
>> 1
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
return 0;
} finally {
throw new RuntimeException();
}
}
}
>> Exception in thread "main" java.lang.RuntimeException
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
throw new Error();
} finally {
throw new RuntimeException();
}
}
}
>> Exception in thread "main" java.lang.RuntimeException
finally-секция может быть использована для завершающего действия, которое гарантированно будет вызвано (даже если было брошено исключение или автор использовал return) по окончании работы
// open some resource
try {
// use resource
} finally {
// close resource
}
Например для освобождения захваченной блокировки
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// some code
} finally {
lock.unlock();
}
Или для закрытия открытого файлового потока
InputStream input = new FileInputStream("...");
try {
// some code
} finally {
input.close();
}
Специально для этих целей в Java 7 появилась конструкция try-with-resources, ее мы изучим позже.
Вообще говоря, в finally-секция нельзя стандартно узнать было ли исключение.
Конечно, можно постараться написать свой «велосипед»
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
long rnd = System.currenttimeMillis();
boolean finished = false;
try {
if (rnd % 3 == 0) {
throw new Error();
} else if (rnd % 3 == 1) {
throw new RuntimeException();
} else {
// nothing
}
finished = true;
} finally {
if (finished) {
// не было исключений
} else {
// было исключение, но какое?
}
}
}
}
Не рекомендуемые практики
— return из finally-секции (можем затереть исключение из try-блока)
— действия в finally-секции, которые могут бросить исключение (можем затереть исключение из try-блока)
8. try + catch + finally
Нет исключения
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
// nothing
System.err.print(" 1");
} catch(Error e) {
System.err.print(" 2");
} finally {
System.err.print(" 3");
}
System.err.print(" 4");
}
}
>> 0 1 3 4
Не заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение и есть подходящий catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new Error();}
System.err.print(" 1");
} catch(Error e) {
System.err.print(" 2");
} finally {
System.err.print(" 3");
}
System.err.print(" 4");
}
}
>> 0 2 3 4
Заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение но нет подходящего catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch(Error e) {
System.err.print(" 2");
} finally {
System.err.print(" 3");
}
System.err.print(" 4");
}
}
>> 0 3
>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException
Не заходим в catch, заходим в finally, не продолжаем после оператора — вылетаем с неперехваченным исключением
9. Вложенные try + catch + finally
Операторы обычно допускают неограниченное вложение.
Пример с if
public class App {
public static void main(String[] args) {
if (args.length > 1) {
if (args.length > 2) {
if (args.length > 3) {
...
}
}
}
}
}
Пример с for
public class App {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; i++) {
for (int k = 0; k < 10; k++) {
...
}
}
}
}
}
Суть в том, что try-cacth-finally тоже допускает неограниченное вложение.
Например вот так
public class App {
public static void main(String[] args) {
try {
try {
try {
...
} catch (Exception e) {
} finally {}
} catch (Exception e) {
} finally {}
} catch (Exception e) {
} finally {}
}
}
Или даже вот так
public class App {
public static void main(String[] args) {
try {
try {
...
} catch (Exception e) {
...
} finally {
...
}
} catch (Exception e) {
try {
...
} catch (Exception e) {
...
} finally {
...
}
} finally {
try {
...
} catch (Exception e) {
...
} finally {
...
}
}
}
}
Ну что же, давайте исследуем как это работает.
Вложенный try-catch-finally без исключения
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
// НИЧЕГО
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // НЕ заходим - нет исключения
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // заходим - выполнение в норме
} catch (Exception e) {
System.err.print(" 6"); // НЕ заходим - нет исключения
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // заходим - выполнение в норме
}
}
>> 0 1 2 4 5 7 8
Мы НЕ заходим в обе catch-секции (нет исключения), заходим в обе finally-секции и выполняем обе строки ПОСЛЕ finally.
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНУТРЕННИЙ catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
if (true) {throw new RuntimeException();}
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // ЗАХОДИМ - есть исключение
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // заходим - выполнение УЖЕ в норме
} catch (Exception e) {
System.err.print(" 6"); // не заходим - нет исключения, УЖЕ перехвачено
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // заходим - выполнение УЖЕ в норме
}
}
>> 0 1 3 4 5 7 8
Мы заходим в ПЕРВУЮ catch-секцию (печатаем «3»), но НЕ заходим во ВТОРУЮ catch-секцию (НЕ печатаем «6», так как исключение УЖЕ перехвачено первым catch), заходим в обе finally-секции (печатаем «4» и «7»), в обоих случаях выполняем код после finally (печатаем «5»и «8», так как исключение остановлено еще первым catch).
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНЕШНИЙ catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
if (true) {throw new Exception();}
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // не заходим - выполнение НЕ в норме
} catch (Exception e) {
System.err.print(" 6"); // ЗАХОДИМ - есть подходящее исключение
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // заходим - выполнение УЖЕ в норме
}
}
>> 0 1 4 6 7 8
Мы НЕ заходим в ПЕРВУЮ catch-секцию (не печатаем «3»), но заходим в ВТОРУЮ catch-секцию (печатаем «6»), заходим в обе finally-секции (печатаем «4» и «7»), в ПЕРВОМ случае НЕ выполняем код ПОСЛЕ finally (не печатаем «5», так как исключение НЕ остановлено), во ВТОРОМ случае выполняем код после finally (печатаем «8», так как исключение остановлено).
Вложенный try-catch-finally с исключением, которое НИКТО НЕ ПЕРЕХВАТИТ
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
if (true) {throw new Error();}
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // НЕ заходим - выполнение НЕ в норме
} catch (Exception e) {
System.err.print(" 6"); // не заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // не заходим - выполнение НЕ в норме
}
}
>> 0 1 4 7
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
Мы НЕ заходим в ОБЕ catch-секции (не печатаем «3» и «6»), заходим в обе finally-секции (печатаем «4» и «7») и в обоих случаях НЕ выполняем код ПОСЛЕ finally (не печатаем «5» и «8», так как исключение НЕ остановлено), выполнение метода прерывается по исключению.
Контакты
Я занимаюсь онлайн обучением Java (вот курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно, видео канала лучше систематизировано в этой статье.
Мой метод обучения состоит в том, что я
- показываю различные варианты применения
- строю усложняющуюся последовательность примеров по каждому варианту
- объясняю логику двигавшую авторами (по мере возможности)
- даю большое количество тестов (50-100) всесторонне проверяющее понимание и демонстрирующих различные комбинации
- даю лабораторные для самостоятельной работы
Данная статье следует пунктам #1 (различные варианты) и #2(последовательность примеров по каждому варианту).
skype: GolovachCourses
email: GolovachCourses@gmail.com
➜
Catching and Handling Exceptions
➜
Catching and Handling Exceptions
This section describes how to use the three exception handler components — the try
, catch
, and finally
blocks — to write an exception handler. Then, the try-with-resources statement, introduced in Java SE 7, is explained. The try-with-resources statement is particularly suited to situations that use Closeable
resources, such as streams.
The last part of this section walks through an example and analyzes what occurs during various scenarios.
The following example defines and implements a class named ListOfNumbers
. When constructed, ListOfNumbers
creates an ArrayList
that contains 10 Integer
elements with sequential values 0 through 9. The ListOfNumbers
class also defines a method named writeList()
, which writes the list of numbers into a text file called OutFile.txt
. This example uses output classes defined in java.io
, which are covered in the Basic I/O section.
// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;
public class ListOfNumbers {
private List<Integer> list;
private static final int SIZE = 10;
public ListOfNumbers () {
list = new ArrayList<>(SIZE);
for (int i = 0; i < SIZE; i++) {
list.add(i);
}
}
public void writeList() {
// The FileWriter constructor throws IOException, which must be caught.
PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
// The get(int) method throws IndexOutOfBoundsException, which must be caught.
out.println("Value at: " + i + " = " + list.get(i));
}
out.close();
}
}
The first line in boldface is a call to a constructor. The constructor initializes an output stream on a file. If the file cannot be opened, the constructor throws an IOException
. The second boldface line is a call to the ArrayList
class’s get method, which throws an IndexOutOfBoundsException
if the value of its argument is too small (less than 0) or too large (more than the number of elements currently contained by the ArrayList
.
If you try to compile the ListOfNumbers
class, the compiler prints an error message about the exception thrown by the FileWriter
constructor. However, it does not display an error message about the exception thrown by get()
. The reason is that the exception thrown by the constructor, IOException
, is a checked exception, and the one thrown by the get()
method, IndexOutOfBoundsException
, is an unchecked exception.
Now that you’re familiar with the ListOfNumbers
class and where the exceptions can be thrown within it, you’re ready to write exception handlers to catch and handle those exceptions.
The Try Block
The first step in constructing an exception handler is to enclose the code that might throw an exception within a try
block. In general, a try
block looks like the following:
try {
code
}
catch and finally blocks . . .
The segment in the example labeled code contains one or more legal lines of code that could throw an exception. (The catch
and finally
blocks are explained in the next two subsections.)
To construct an exception handler for the writeList()
method from the ListOfNumbers
class, enclose the exception-throwing statements of the writeList()
method within a try
block. There is more than one way to do this. You can put each line of code that might throw an exception within its own try block and provide separate exception handlers for each. Or, you can put all the writeList()
code within a single try
block and associate multiple handlers with it. The following listing uses one try
block for the entire method because the code in question is very short.
private List<Integer> list;
private static final int SIZE = 10;
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entered try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
}
catch and finally blocks . . .
}
If an exception occurs within the try
block, that exception is handled by an exception handler associated with it. To associate an exception handler with a try
block, you must put a catch
block after it; the next section, The catch Blocks, shows you how.
The Catch Blocks
You associate exception handlers with a try
block by providing one or more catch blocks directly after the try
block. No code can be between the end of the try
block and the beginning of the first catch
block.
try {
} catch (ExceptionType name) {
} catch (ExceptionType name) {
}
Each catch
block is an exception handler that handles the type of exception indicated by its argument. The argument type, ExceptionType
, declares the type of exception that the handler can handle and must be the name of a class that inherits from the Throwable
class. The handler can refer to the exception with name.
The catch
block contains code that is executed if and when the exception handler is invoked. The runtime system invokes the exception handler when the handler is the first one in the call stack whose ExceptionType
matches the type of the exception thrown. The system considers it a match if the thrown object can legally be assigned to the exception handler’s argument.
The following are two exception handlers for the writeList()
method:
try {
} catch (IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
}
Exception handlers can do more than just print error messages or halt the program. They can do error recovery, prompt the user to make a decision, or propagate the error up to a higher-level handler using chained exceptions, as described in the Chained Exceptions section.
Multi-Catching Exceptions
You can catch more than one type of exception with one exception handler, with the multi-catch pattern.
In Java SE 7 and later, a single catch
block can handle more than one type of exception. This feature can reduce code duplication and lessen the temptation to catch an overly broad exception.
In the catch
clause, specify the types of exceptions that block can handle, and separate each exception type with a vertical bar (|
):
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}
Note: If a catch block handles more than one exception type, then the catch
parameter is implicitly final
. In this example, the catch
parameter ex
is final
and therefore you cannot assign any values to it within the catch
block.
The Finally Block
The finally block always executes when the try
block exits. This ensures that the finally
block is executed even if an unexpected exception occurs. But finally
is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return
, continue
, or break
. Putting cleanup code in a finally
block is always a good practice, even when no exceptions are anticipated.
Note: If the JVM exits while the
try
orcatch
code is being executed, then thefinally
block may not execute.
The try
block of the writeList()
method that you’ve been working with here opens a PrintWriter
. The program should close that stream before exiting the writeList()
method. This poses a somewhat complicated problem because writeList()
‘s try
block can exit in one of three ways.
- The new
FileWriter
statement fails and throws anIOException
. - The
list.get(i)
statement fails and throws anIndexOutOfBoundsException
. - Everything succeeds and the
try
block exits normally.
The runtime system always executes the statements within the finally
block regardless of what happens within the try
block. So it’s the perfect place to perform cleanup.
The following finally
block for the writeList()
method cleans up and then closes the PrintWriter
.
finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
} else {
System.out.println("PrintWriter not open");
}
}
Important: The
finally
block is a key tool for preventing resource leaks. When closing a file or otherwise recovering resources, place the code in afinally
block to ensure that resource is always recovered.Consider using the try-with-resources statement in these situations, which automatically releases system resources when no longer needed. The try-with-resources Statement section has more information.
The Try-with-resources Statement
The try-with-resources statement is a try
statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable
, which includes all objects which implement java.io.Closeable
, can be used as a resource.
The following example reads the first line from a file. It uses an instance of BufferedReader
to read data from the file. BufferedReader
is a resource that must be closed after the program is finished with it:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
In this example, the resource declared in the try-with-resources statement is a BufferedReader
. The declaration statement appears within parentheses immediately after the try
keyword. The class BufferedReader
, in Java SE 7 and later, implements the interface java.lang.AutoCloseable
. Because the BufferedReader
instance is declared in a try-with-resource statement, it will be closed regardless of whether the try
statement completes normally or abruptly (as a result of the method BufferedReader.readLine()
throwing an IOException
.
Prior to Java SE 7, you can use a finally
block to ensure that a resource is closed regardless of whether the try
statement completes normally or abruptly. The following example uses a finally
block instead of a try-with-resources statement:
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
However, in this example, if the methods readLine()
and close both throw exceptions, then the method readFirstLineFromFileWithFinallyBlock()
throws the exception thrown from the finally
block; the exception thrown from the try
block is suppressed. In contrast, in the example readFirstLineFromFile()
, if exceptions are thrown from both the try
block and the try-with-resources statement, then the method readFirstLineFromFile()
throws the exception thrown from the try
block; the exception thrown from the try-with-resources block is suppressed. In Java SE 7 and later, you can retrieve suppressed exceptions; see the section Suppressed Exceptions for more information.
You may declare one or more resources in a try-with-resources statement. The following example retrieves the names of the files packaged in the zip file zipFileName
and creates a text file that contains the names of these files:
public static void writeToFileZipFileContents(String zipFileName,
String outputFileName)
throws java.io.IOException {
java.nio.charset.Charset charset =
java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath =
java.nio.file.Paths.get(outputFileName);
// Open zip file and create output file with
// try-with-resources statement
try (
java.util.zip.ZipFile zf =
new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries =
zf.entries(); entries.hasMoreElements();) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName =
((java.util.zip.ZipEntry)entries.nextElement()).getName() +
newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
In this example, the try-with-resources statement contains two declarations that are separated by a semicolon: ZipFile
and BufferedWriter
. When the block of code that directly follows it terminates, either normally or because of an exception, the close()
methods of the BufferedWriter
and ZipFile
objects are automatically called in this order. Note that the close methods of resources are called in the opposite order of their creation.
The following example uses a try-with-resources statement to automatically close a java.sql.Statement
object:
public static void viewTable(Connection con) throws SQLException {
String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
int supplierID = rs.getInt("SUP_ID");
float price = rs.getFloat("PRICE");
int sales = rs.getInt("SALES");
int total = rs.getInt("TOTAL");
System.out.println(coffeeName + ", " + supplierID + ", " +
price + ", " + sales + ", " + total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
The resource java.sql.Statement
used in this example is part of the JDBC 4.1 and later API.
Note: A try-with-resources statement can have catch
and finally
blocks just like an ordinary try
statement. In a try-with-resources statement, any catch
or finally
block is run after the resources declared have been closed.
Suppressed Exceptions
An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents()
, an exception can be thrown from the try
block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile
and BufferedWriter
objects. If an exception is thrown from the try
block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents()
method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed()
method from the exception thrown by the try
block.
Classes That Implement the AutoCloseable or Closeable Interface
See the Javadoc of the AutoCloseable
and Closeable
interfaces for a list of classes that implement either of these interfaces. The Closeable
interface extends the AutoCloseable
interface. The close()
method of the Closeable
interface throws exceptions of type IOException
while the close()
method of the AutoCloseable
interface throws exceptions of type Exception
. Consequently, subclasses of the AutoCloseable
interface can override this behavior of the close()
method to throw specialized exceptions, such as IOException
, or no exception at all.
Putting It All Together
The previous sections described how to construct the try
, catch
, and finally
code blocks for the writeList()
method in the ListOfNumbers
class. Now, let’s walk through the code and investigate what can happen.
When all the components are put together, the writeList()
method looks like the following.
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering" + " try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
As mentioned previously, this method’s try
block has three different exit possibilities; here are two of them.
- Code in the
try
statement fails and throws an exception. This could be anIOException
caused by the newFileWriter
statement or anIndexOutOfBoundsException
caused by a wrong index value in thefor
loop. - Everything succeeds and the
try
statement exits normally.
Let’s look at what happens in the writeList()
method during these two exit possibilities.
Scenario 1: An Exception Occurs
The statement that creates a FileWriter
can fail for a number of reasons. For example, the constructor for the FileWriter
throws an IOException
if the program cannot create or write to the file indicated.
When FileWriter
throws an IOException
, the runtime system immediately stops executing the try
block; method calls being executed are not completed. The runtime system then starts searching at the top of the method call stack for an appropriate exception handler. In this example, when the IOException
occurs, the FileWriter
constructor is at the top of the call stack. However, the FileWriter
constructor doesn’t have an appropriate exception handler, so the runtime system checks the next method — the writeList()
method — in the method call stack. The writeList()
method has two exception handlers: one for IOException
and one for IndexOutOfBoundsException
.
The runtime system checks writeList()
‘s handlers in the order in which they appear after the try
statement. The argument to the first exception handler is IndexOutOfBoundsException
. This does not match the type of exception thrown, so the runtime system checks the next exception handler — IOException
. This matches the type of exception that was thrown, so the runtime system ends its search for an appropriate exception handler. Now that the runtime has found an appropriate handler, the code in that catch
block is executed.
After the exception handler executes, the runtime system passes control to the finally
block. Code in the finally
block executes regardless of the exception caught above it. In this scenario, the FileWriter
was never opened and doesn’t need to be closed. After the finally
block finishes executing, the program continues with the first statement after the finally
block.
Here’s the complete output from the ListOfNumbers
program that appears when an IOException
is thrown.
Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open
Scenario 2: The try Block Exits Normally
In this scenario, all the statements within the scope of the try
block execute successfully and throw no exceptions. Execution falls off the end of the try
block, and the runtime system passes control to the finally
block. Because everything was successful, the PrintWriter
is open when control reaches the finally
block, which closes the PrintWriter
. Again, after the finally
block finishes executing, the program continues with the first statement after the finally
block.
Here is the output from the ListOfNumbers
program when no exceptions are thrown.
Entering try statement
Closing PrintWriter
➜
Catching and Handling Exceptions
➜
В нашей жизни нередко возникают ситуации, которые мы не планировали. К примеру, пошли вы утром умываться и с досадой обнаружили, что отключили воду. Вышли на улицу, сели в машину, а она не заводится. Позвонили другу, а он недоступен. И так далее и тому подобное… В большинстве случаев человек без труда справится с подобными проблемами. А вот как с непредвиденными ситуациями справляется Java, мы сейчас и поговорим.
Что называют исключением. Исключения в мире программирования
В программировании исключением называют возникновение ошибки (ошибок) и различных непредвиденных ситуаций в процессе выполнения программы. Исключения могут появляться как в итоге неправильных действий юзера, так и из-за потери сетевого соединения с сервером, отсутствии нужного ресурса на диске и т. п. Также среди причин исключений — ошибки программирования либо неверное использование API.
При этом в отличие от «человеческого мира», программное приложение должно чётко понимать, как поступать в подобной ситуации. И вот как раз для этого в Java и существует механизм исключений (exception).
Используемые ключевые слова
При обработке исключений в Java применяются следующие ключевые слова:
— try – служит для определения блока кода, в котором может произойти исключение;
— catch – необходим для определения блока кода, где происходит обработка исключения;
— finally – применяется для определения блока кода, являющегося необязательным, однако при его наличии он выполняется в любом случае вне зависимости от результата выполнения блока try.
Вышеперечисленные ключевые слова необходимы для создания в коде ряда специальных обрабатывающих конструкций: try{}finally{}, try{}catch, try{}catch{}finally.
Кроме того:
1. Для возбуждения исключения используем throw.
2. Для предупреждения в сигнатуре методов о том, что метод может выбросить исключение, применяем throws.
Давайте на примере посмотрим, как используются ключевые слова в Java-программе:
//метод считывает строку с клавиатуры public String input() throws MyException {//предупреждаем с помощью throws, // что метод может выбросить исключение MyException BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String s = null; //в блок try заключаем код, в котором может произойти исключение, в данном // случае компилятор нам подсказывает, что метод readLine() класса // BufferedReader может выбросить исключение ввода/вывода try { s = reader.readLine(); // в блок catch заключаем код по обработке исключения IOException } catch (IOException e) { System.out.println(e.getMessage()); // в блоке finally закрываем поток чтения } finally { // при закрытии потока тоже возможно исключение, например, если он не был открыт, поэтому “оборачиваем” код в блок try try { reader.close(); // пишем обработку исключения при закрытии потока чтения } catch (IOException e) { System.out.println(e.getMessage()); } } if (s.equals("")) { // мы решили, что пустая строка может нарушить в дальнейшем работу нашей программы, например, на результате этого метода нам надо вызывать метод substring(1,2), поэтому мы вынуждены прервать выполнение программы с генерацией своего типа исключения MyException с помощью throw throw new MyException("String can not be empty!"); } return s; }Зачем нам механизм исключений?
Для понимания опять приведём пример из обычного мира. Представьте, что на какой-нибудь автодороге имеется участок с аварийным мостом, на котором ограничена грузоподъёмность. И если по такому мосту проедет грузовик со слишком большой массой, мост разрушится, а в момент этого ЧП ситуация для шофёра станет, мягко говоря, исключительной. И вот, дабы такого не произошло, дорожные службы заранее устанавливают на дороге соответствующие предупреждающие знаки. И тогда водитель, посмотрев на знак, сравнит массу своего авто со значением разрешённой грузоподъёмности и примет соответствующее решение, например, поедет по другой дороге.
То есть мы видим, что из-за правильных действий дорожной службы шоферы крупногабаритных транспортных средств:
1) получили возможность заранее изменить свой путь;
2) были предупреждены об опасности;
3) были предупреждены о невозможности проезжать по мосту при определённых условиях.Вот как наш жизненный пример соотносится с применением исключения на Java:
Исходя из вышесказанного, мы можем назвать одну из причин применения исключений в Java. Заключается она в возможности предупреждения исключительной ситуации для её последующего разрешения и продолжения работы программы. То есть механизм исключений позволит защитить написанный код от неверного применения пользователем путём валидации входящих данных.
Что же, давайте ещё раз побудем дорожной службой. Чтобы установить знак, мы ведь должны знать места, где водителей ТС могут ждать различные неприятности. Это первое. Далее, нам ведь надо заготовить и установить знаки. Это второе. И, наконец, надо предусмотреть маршруты объезда, позволяющие избежать опасности.
В общем, механизм исключений в Java работает схожим образом. На стадии разработки программы мы выполняем «ограждение» опасных участков кода в отношении наших исключений, используя блок try{}. Чтобы предусмотреть запасные пути, применяем блок catch{}. Код, выполняемый в программе при любом исходе, пишем в блоке finally{}.
Иногда бывает, что мы не можем предусмотреть «запасной аэродром» либо специально желаем предоставить право его выбора юзеру. Но всё равно мы должны как минимум предупредить пользователя об опасности. Иначе он превратится в разъярённого шофёра, который ехал долго, не встретил ни одного предупреждающего знака и в итоге добрался до аварийного моста, проехать по которому не представляется возможным.
Что касается программирования на Java, то мы, когда пишем свои классы и методы, далеко не всегда можем предвидеть контекст их применения другими программистами в своих программах, а значит, не можем со стопроцентной вероятностью предвидеть правильный путь для разрешения исключительных ситуаций. Но предупредить коллег о возможной исключительной ситуации мы всё-таки должны, и это не что иное, как правило хорошего тона.
Выполнить это правило в Java нам как раз и помогает механизм исключений с помощью throws. Выбрасывая исключение, мы, по сути, объявляем общее поведение нашего метода и предоставляем пользователю метода право написания кода по обработке исключения.
Предупреждаем о неприятностях
Если мы не планируем обрабатывать исключение в собственном методе, но желаем предупредить пользователей метода о возможной исключительной ситуации, мы используем, как это уже было упомянуто, ключевое слово throws. В сигнатуре метода оно означает, что при некоторых обстоятельствах метод может выбросить исключение. Это предупреждение становится частью интерфейса метода и даёт право пользователю на создание своего варианта реализации обработчика исключения.
После упоминания ключевого слова throws мы указываем тип исключения. Как правило, речь идёт о наследниках класса Exception Java. Но так как Java — это объектно-ориентированный язык программирования, все исключения представляют собой объекты.
Иерархия исключений в Java
Когда возникают ошибки при выполнении программы, исполняющая среда Java Virtual Machine обеспечивает создание объекта нужного типа, используя иерархию исключений Java — речь идёт о множестве возможных исключительных ситуаций, которые унаследованы от класса Throwable — общего предка. При этом исключительные ситуации, которые возникают в программе, делят на 2 группы:
1. Ситуации, при которых восстановление нормальной дальнейшей работы невозможно.
2. Ситуации с возможностью восстановления.К первой группе можно отнести случаи, при которых возникают исключения, которые унаследованы из класса Error. Это ошибки, возникающие во время выполнения программы при сбое работы Java Virtual Machine, переполнении памяти либо сбое системы. Как правило, такие ошибки говорят о серьёзных проблемах, устранение которых программными средствами невозможно. Данный вид исключений в Java относят к неконтролируемым исключениям на стадии компиляции (unchecked). К этой же группе относятся и исключения-наследники класса Exception, генерируемые Java Virtual Machine в процессе выполнения программы — RuntimeException. Данные исключения тоже считаются unchecked на стадии компиляции, а значит, написание кода по их обработке необязательно.
Что касается второй группы, то к ней относят ситуации, которые можно предвидеть ещё на стадии написания приложения, поэтому для них код обработки должен быть написан. Это контролируемые исключения (checked). И в большинстве случаев Java-разработчики работают именно с этими исключениями, выполняя их обработку.
Создание исключения
В процессе исполнения программы исключение генерируется Java Virtual Machine либо вручную посредством оператора throw. В таком случае в памяти происходит создание объекта исключения, выполнение основного кода прерывается, а встроенный в JVM обработчик исключений пробует найти способ обработать это самое исключение.
Обработка исключения
Обработка исключений в Java подразумевает создание блоков кода и производится в программе посредством конструкций try{}finally{}, try{}catch, try{}catch{}finally.
В процессе возбуждения исключения в try обработчик исключения ищется в блоке catch, который следует за try. При этом если в catch присутствует обработчик данного вида исключения, происходит передача управления ему. Если же нет, JVM осуществляет поиск обработчика данного типа исключения, используя для этого цепочку вызова методов. И так происходит до тех пор, пока не находится подходящий catch. После того, как блок catch выполнится, управление переходит в необязательный блок finally. Если подходящий блок catch найден не будет, Java Virtual Machine остановит выполнение программы, выведя стек вызовов методов под названием stack trace. Причём перед этим выполнится код блока finally при наличии такового.
Рассмотрим практический пример обработки исключений:
public class Print { void print(String s) { if (s == null) { throw new NullPointerException("Exception: s is null!"); } System.out.println("Inside method print: " + s); } public static void main(String[] args) { Print print = new Print(); List list= Arrays.asList("first step", null, "second step"); for (String s:list) { try { print.print(s); } catch (NullPointerException e) { System.out.println(e.getMessage()); System.out.println("Exception was processed. Program continues"); } finally { System.out.println("Inside bloсk finally"); } System.out.println("Go program...."); System.out.println("-----------------"); } } }А теперь глянем на результаты работы метода main:
Inside method print: first step Inside bloсk finally Go program.... ----------------- Exception: s is null! Exception was processed. Program continues Inside bloсk finally Go program.... ----------------- Inside method print: second step Inside bloсk finally Go program.... -----------------Блок finally чаще всего используют, чтобы закрыть открытые в try потоки либо освободить ресурсы. Но при написании программы уследить за закрытием всех ресурсов возможно не всегда. Чтобы облегчить жизнь разработчикам Java, была предложена конструкция try-with-resources, автоматически закрывающая ресурсы, открытые в try. Используя try-with-resources, мы можем переписать наш первый пример следующим образом:
public String input() throws MyException { String s = null; try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))){ s = reader.readLine(); } catch (IOException e) { System.out.println(e.getMessage()); } if (s.equals("")){ throw new MyException ("String can not be empty!"); } return s; }А благодаря появившимся возможностям Java начиная с седьмой версии, мы можем ещё и объединять в одном блоке перехват разнотипных исключений, делая код компактнее и читабельнее:
public String input() { String s = null; try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { s = reader.readLine(); if (s.equals("")) { throw new MyException("String can not be empty!"); } } catch (IOException | MyException e) { System.out.println(e.getMessage()); } return s; }Итоги
Итак, применение исключений в Java повышает отказоустойчивость программы благодаря использованию запасных путей. Кроме того, появляется возможность отделить код обработки исключительных ситуаций от логики основного кода за счёт блоков catch и переложить обработку исключений на пользователя кода посредством throws.
Основные вопросы об исключениях в Java
1.Что такое проверяемые и непроверяемые исключения?
Если говорить коротко, то первые должны быть явно пойманы в теле метода либо объявлены в секции throws метода. Вторые вызываются проблемами, которые не могут быть решены. Например, это нулевой указатель или деление на ноль. Проверяемые исключения очень важны, ведь от других программистов, использующих ваш API, вы ожидаете, что они знают, как обращаться с исключениями. К примеру, наиболее часто встречаемое проверяемое исключение — IOException, непроверяемое — RuntimeException.
2.Почему переменные, определённые в try, нельзя использовать в catch либо finally?
Давайте посмотрим на нижеследующий код. Обратите внимание, что строку s, которая объявлена в блоке try, нельзя применять в блоке catch. То есть данный код не скомпилируется.try { File file = new File("path"); FileInputStream fis = new FileInputStream(file); String s = "inside"; } catch (FileNotFoundException e) { e.printStackTrace(); System.out.println(s); }А всё потому, что неизвестно, где конкретно в try могло быть вызвано исключение. Вполне вероятно, что оно было вызвано до объявления объекта.
3.Почему Integer.parseInt(null) и Double.parseDouble(null) вызывают разные исключения?
Это проблема JDK. Так как они были разработаны разными людьми, то заморачиваться вам над этим не стоит:Integer.parseInt(null); // вызывает java.lang.NumberFormatException: null Double.parseDouble(null); // вызывает java.lang.NullPointerException4.Каковы основные runtime exceptions в Java?
Вот лишь некоторые из них:IllegalArgumentException ArrayIndexOutOfBoundsExceptionИх можно задействовать в операторе if, если условие не выполняется:
if (obj == null) { throw new IllegalArgumentException("obj не может быть равно null");5.Возможно ли поймать в одном блоке catch несколько исключений?
Вполне. Пока классы данных исключений можно отследить вверх по иерархии наследования классов до одного и того же суперкласса, возможно применение только этого суперкласса.
6.Способен ли конструктор вызывать исключения?
Способен, ведь конструктор — это лишь особый вид метода.class FileReader{ public FileInputStream fis = null; public FileReader() throws IOException{ File dir = new File(".");//get current directory File fin = new File(dir.getCanonicalPath() + File.separator + "not-existing-file.txt"); fis = new FileInputStream(fin); } }7.Возможен ли вызов исключений в final?
В принципе, можете сделать таким образом:public static void main(String[] args) { File file1 = new File("path1"); File file2 = new File("path2"); try { FileInputStream fis = new FileInputStream(file1); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { FileInputStream fis = new FileInputStream(file2); } catch (FileNotFoundException e) { e.printStackTrace(); } } }Но если желаете сохранить читабельность, объявите вложенный блок try-catch в качестве нового метода и вставьте вызов данного метода в блок finally.
finally. public static void main(String[] args) { File file1 = new File("path1"); File file2 = new File("path2"); try { FileInputStream fis = new FileInputStream(file1); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { methodThrowException(); } }