Обработка ошибок и исключения
Методы обработки ошибок
1. Не обрабатывать.
2. Коды возврата. Основная идея — в случае ошибки возвращать специальное значение, которое не может быть корректным. Например, если в методе есть операция деления, то придется проверять делитель на равенство нулю:
Double f(Double a, Double b) { if ((a == null) || (b == null)) { return null; } //... if (Math.abs(b) < EPS) { return null; } else { return a / b; } }
При вызове метода необходимо проверить возвращаемое значение:
Double d = f(a, b); if (d != null) { //... } else { //... }
Минусом такого подхода является необходимость проверки возвращаемого значения каждый раз при вызове метода. Кроме того, не всегда возможно определить тип ошибки.
3.Использовать флаг ошибки: при возникновении ошибки устанавливать флаг в соответствующее значение:
boolean error; Double f(Double a, Double b) { if ((a == null) || (b == null)) { error = true; } //... if (Math.abs(b) < EPS) { error = true; return b; } else { return a / b; } }
error = false; Double d = f(a, b); if (error) { //... } else { //... }
Минусы такого подхода аналогичны минусам использования кодов возврата.
4.Можно вызвать метод обработки ошибки и возвращать то, что вернет этот метод.
Double f(Double a, Double b) { if ((a == null) || (b == null)) { return nullPointer(); } //... if (Math.abs(b) < EPS) { return divisionByZero(); } else { return a / b; } }
Но в таком случае не всегда возможно проверить корректность результата вызова основного метода.
5.В случае ошибки просто закрыть программу.
if (Math.abs(b) < EPS) { System.exit(0); return this; }
Это приведет к потере данных, также невозможно понять, в каком месте возникла ошибка.
Исключения
В Java возможна обработка ошибок с помощью исключений:
Double f(Double a, Double b) { if ((a == null) || (b == null)) { throw new IllegalArgumentException("arguments of f() are null"); } //... return a / b; }
Проверять b
на равенство нулю уже нет необходимости, так как при делении на ноль метод бросит непроверяемое исключение ArithmeticException
.
Исключения позволяют:
- разделить обработку ошибок и сам алгоритм;
- не загромождать код проверками возвращаемых значений;
- обрабатывать ошибки на верхних уровнях, если на текущем уровне не хватает данных для обработки. Например, при написании универсального метода чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей метод программы.
Каждый раз, когда при выполнении программы происходит ошибка, создается объект-исключение, содержащий информацию об ошибке, включая её тип и состояние программы на момент возникновения ошибки. После создания исключения среда выполнения пытается найти в стеке вызовов метод, который содержит код, обрабатывающий это исключение. Поиск начинается с метода, в котором произошла ошибка, и проходит через стек в обратном порядке вызова методов. Если не было найдено ни одного подходящего обработчика, выполнение программы завершается.
Таким образом, механизм обработки исключений содержит следующие операции:
- Создание объекта-исключения.
- Заполнение stack trace'а этого исключения.
- Stack unwinding (раскрутка стека) в поисках нужного обработчика.
Классификация исключений
Класс Java Throwable
описывает все, что может быть брошено как исключение. Наследеники Throwable
- Ecxeption
и Error
- основные типы исключений. Также RuntimeException
, унаследованный от Exception
, является существенным классом.
Проверяемые исключения
Наследники класса Exception
(кроме наслеников RuntimeException
) являются проверяемыми исключениями(checked exception). Как правило, это ошибки, возникшие по вине внешних обстоятельств или пользователя приложения – неправильно указали имя файла, например. Эти исключения должны обрабатываться в ходе работы программы, поэтому компилятор проверяет наличие обработчика или явного описания тех типов исключений, которые могут быть сгенерированы некоторым методом.
Все исключения, кроме классов Error
и RuntimeException
и их наследников, являются проверяемыми.
Error
Класс Error
и его подклассы предназначены для системных ошибок. Свои собственные классы-наследники для Error
писать (за очень редкими исключениями) не нужно. Как правило, это действительно фатальные ошибки, пытаться обработать которые довольно бессмысленно (например OutOfMemoryError
).
RuntimeException
Эти исключения обычно возникают в результате ошибок программирования, такие как ошибки разработчика или неверное использование интерфейса приложения. Например, в случае выхода за границы массива метод бросит OutOfBoundsException
. Такие ошибки могут быть в любом месте программы, поэтому компилятор не требует указывать runtime исключения в объявлении метода. Теоретически приложение может поймать это исключение, но разумнее исправить ошибку.
Обработка исключений
Чтобы сгенерировать исключение используется ключевое слово throw
. Как и любой объект в Java, исключения создаются с помощью new
.
if (t == null) { throw new NullPointerException("t = null"); }
Есть два конструктора для всех стандартных исключений: первый - конструктор по умолчанию, второй принимает строковый аргумент, поэтому можно поместить подходящую информацию в исключение.
Как и было сказано раньше, определение метода должно содержать список всех проверяемых исключений, которые метод может бросить. Также можно написать более общий класс, среди наследников которого есть эти исключения.
void f() throws InterruptedException, IOException { //...
try-catch-finally
Код, который может бросить исключения оборачивается в try
-блок, после которого идут блоки catch
и finally
.
try { // Код, который может сгенерировать исключение }
Сразу после блока проверки следуют после обработчики исключений, которые объявляются ключевым словом catch
.
try { // Код, который может сгенерировать исключение } catch(Type1 id1) { // Обработка исключения Type1 } catch(Type2 id2) { // Обработка исключения Type2 }
Сatch
-блоки обрабатывают исключения, указанные в качестве аргумента. Тип аргумента должен быть классом, унаследованного от Throwable
, или самим Throwable
. Блок catch
выполняется, если тип брошенного исключения является наследником типа аргумента и если это исключение не было обработано предыдущими блоками.
Код из блока finally
выполнится в любом случае: при нормальном выходе из try
, после обработки исключения или при выходе по команде return
.
NB: Если JVM выйдет во время выполнения кода из try
или catch
, то finally
блок может не выполниться. Также, например, если поток выполняющий try
или catch
код остановлен, то блок finally
может не выполниться, даже если приложение продолжает работать.
Блок finally
удобен для закрытия файлов и освобождения любых других ресурсов. Код в блоке finally
должен быть максимально простым. Если внутри блока finally
будет брошено какое-либо исключение или просто встретится оператор return
, брошенное в блоке try
исключение (если таковое было брошено) будет забыто.
import java.io.IOException; public class ExceptionTest { public static void main(String[] args) { try { try { throw new Exception("a"); } finally { throw new IOException("b"); } } catch (IOException ex) { System.err.println(ex.getMessage()); } catch (Exception ex) { System.err.println(ex.getMessage()); } } }
После того, как было брошено первое исключение - new Exception("a")
- будет выполнен блок finally
, в котором будет брошено исключение new IOException("b")
, именно оно будет поймано и обработано. Результатом его выполнения будет вывод в консоль b
. Исходное исключение теряется.
Информация об исключениях
-
getMessage()
. Этот метод возвращает строку, которая была первым параметром при создании исключения; -
getCause()
возвращает исключение, которое стало причиной текущего исключения; -
printStackTrace()
печатает stack trace, который содержит информацию, с помощью которой можно определить причину исключения и место, где оно было брошено.
Exception in thread "main" java.lang.IllegalStateException: A book has a null property at com.example.myproject.Author.getBookIds(Author.java:38) at com.example.myproject.Bootstrap.main(Bootstrap.java:14) Caused by: java.lang.NullPointerException at com.example.myproject.Book.getId(Book.java:22) at com.example.myproject.Author.getBookIds(Author.java:35)
Все методы выводятся в обратном порядке вызовов. В примере исключение IllegalStateException
было брошено в методе getBookIds
, который был вызван в main
. "Caused by" означает, что исключение NullPointerException
является причиной IllegalStateException
.
Разработка исключений
Чтобы определить собственное проверяемое исключение, необходимо создать наследника класса java.lang.Exception
. Желательно, чтобы у исключения был конструкор, которому можно передать сообщение:
public class FooException extends Exception { public FooException() { super(); } public FooException(String message) { super(message); } public FooException(String message, Throwable cause) { super(message, cause); } public FooException(Throwable cause) { super(cause); } }
Исключения в Java7
- обработка нескольких типов исключений в одном
catch
-блоке:
catch
(IOException | SQLException ex) {...}
В таких случаях параметры неявно являются final
, поэтому нельзя присвоить им другое значение в блоке catch
.
-
Try
с ресурсами позволяет прямо вtry
блоке объявлять необходимые ресурсы, которые по завершению блока будут корректно закрыты (с помощью методаclose()
). Любой объект реализующийjava.lang.AutoCloseable
может быть использован как ресурс.
static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
В приведенном примере в качестве ресурса использутся объект класса BufferedReader
, который будет закрыт вне зависимосити от того, как выполнится try
-блок.
Можно объявлять несколько ресурсов, разделяя их точкой с запятой:
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)) { //Work with Statement and ResultSet } catch (SQLException e) { e.printStackTrace; } }
Во время закрытия ресурсов тоже может быть брошено исключение. В try-with-resources добавленна возможность хранения "подавленных" исключений, и брошенное try
-блоком исключение имеет больший приоритет, чем исключения получившиеся во время закрытия. Получить последние можно вызовом метода getSuppressed()
от исключения брошенного try
-блоком.
Примеры исключений
- любая операция может бросить
VirtualMachineError
. Как правило это происходит в результате системных сбоев. -
OutOfMemoryError
. Приложение может бросить это исключение, если, например, не хватает места в куче, или не хватает памяти для того, чтобы создать стек нового потока. -
IllegalArgumentException
используется для того, чтобы избежать передачи некорректных значений аргументов. Например:
public void f(Object a) { if (a == null) { throw new NullPointerException("a must not be null"); } }
IllegalStateException
возникает в результате некорректного состояния объекта. Например, использование объекта перед тем как он будет инициализирован.