Исключения (Exceptions)
Введение
До сих пор при написании программ мы считали, что все данные 'правильные', и не проверяли никакие получаемые параметры. Теперь представим, что мы вдруг захотим сложить две матрицы размерами 2×2 и 3×3:
public class Main {
public static void main(String[] args) {
SquareMatrix m1 = new SquareMatrix(3);
SquareMatrix m2 = new SquareMatrix(2);
m1.sum(m2);
}
}
Что же случится, если мы запустим эту программу?
example1 $ java Main
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
at Matrix.add(Matrix.java:76)
at SquareMatrix.sum(SquareMatrix.java:43)
at Main.main(Main.java:5)
Java-машина напечатала сообщение об ошибке и аварийно завершила свою работу. Что же содержится в этом сообщении?
- Сообщение о том, что произошла ошибка (исключение: слово
Exception). - Тип ошибки:
ArrayIndexOutOfBoundsExceptionи причина ошибки — индекс 2. - список вызванных методов (в обратном порядке), имена файлов и номера строк.
Теперь понятно, что программа сломалась при попытке обратиться к элементу массива с несуществующим индексом, причём сломавшаяся часть находится в файле Matrix.java на строке 76.
Если мы посмотрим, что написано в файле Matrix.java около строке 76, вот что мы увидим:
protected void add(Matrix m2) {
for (int i = 0; i < myValues.length; i++)
for (int j = 0; j < myValues[i].length; j++)
myValues[i][j] += m2.myValues[i][j]; // ← строка 76
}
И действительно, когда мы попытались к матрице размером 2×2 прибавить матрицу размером 3×3, программа попыталась обратиться к несуществующей (третьей, т.е. строке с индексом 2) строке матрицы.
Другой понятный пример ошибки — это написать что-то в этом духе:
int i, j, k;
i = 10;
j = 0;
k = i/j; // эта строчке вызовет ArithmeticException: / by zero
Java — объектно-ориентированный язык. Поэтому в момент ошибки создается и, как говорят, бросается (throw) объект специального класса — Exception или его класса-наследника. В частности, один из его наследников называется ArrayIndexOutOfBoundsException. Потом исполнение программы постепенно прерывается.
Отступление о стеке вызовов
Рассмотрим такую программу на неком языке:
int i = 10;
bar(int a)
{
// какой-то код
...
}
foo(int x, int y, int z)
{
// какой-то код
...
bar(x);
// ещё код
...
}
main()
{
i = i + 1;
foo(2, 3, 4);
// код
...
foo(5, 6, 7);
}
В момент вызова функции (например, foo) надо запомнить адрес, куда после её завершения надо вернуть управление. Ещё надо запомнить переданные в эту функцию параметры. Глобальные переменные для этого не подходят, ведь никто не знает, сколько вложенных вызовов (main→foo→bar→...) будет. Вместо этого используется специальный стек, называемый стек вызовов.
При вызове любой функции в него помещается адрес возврата и значения параметров, а затем выполнение переходит к тексту (коду) этой функции. Когда она доходит до возврата, специальная команда извлекает из стека параметры, адрес возврата и переходит на него.
В языке Java в стеке хранится информация (точнее ссылка на информацию), достаточная для получения точки вызова в коде — названия функции, имени файла, номера строки.
Замечание на полях: существует доступный программисту механизм, позволяющий узнать содержимое стека.
Возвращаемся к Exceptions
В объекте типа Exception хранится
- информация об исключении (например, в классе Exception хранится просто строка сообщения, в классе
ArrayIndexOutOfBoundsException— индекс и т.п.) - stacktrace — список из стека вызовов.
Отступление о запуске программ
В чем пока для нас заключается польза от исключений? В том, что мы можем точно узнать место и тип ошибки. Однако JIT-компилятор в процессе выполнения может оптимизировать код. Поэтому иногда, stacktrace может вместо информации о файле/строке просто выдать знак вопроса. А иногда просто ложную информацию. Поэтому, в некоторых случаях локализации ошибки может помочь отключение оптимизатора. До сих пор мы запускали программу просто командой
$ java Main
Java-машине можно указывать опции. Есть небольшой набор стандартных опций, которые должны поддерживать все, кто хочет называться гордым именем Java. А все остальные опции начинаются с -X. Например, есть такая нестандартная (правда её поддерживают все известные реализации Java-машин) опция -Xint (от слова interpreter — интерпретатор}, которая указывает Java-машине только интерпретировать код).
Ошибки, которые мы умеем делать
ArrayIndexOutOfBoundsException
Выход за границы массива. С этим все понятно.
ArithmeticException
Ошибка целочисленной арифметики. Например, деление на ноль.
ClassCastException
Ошибка приведения типов. Пример:
Matrix m = new Matrix(3, 3);
SquareMatrix s = (SquareMatrix)m;
Вторая строчка вызовет исключение, поскольку хотя матрица m является квадратной, она не является представителем класса SquareMatrix.
NullPointerException
Отступление: об инициализации переменных и полей классов.
Допустим у нас есть класс X:
class X {
int myN;
boolean myB;
int[] myA;
X myX;
// .....
}
И есть функция foo:
void foo() {
int n;
boolean b;
int[] a;
X x = null;
// .........
}
Локальные переменные не инициализируются. Попытка обратиться к любой из них до того, как мы присвоили ей значение, вызовет ошибку, причём на этапе компиляции. А вот поля в классе инициализируются значениями по-умолчанию. Для числовых типов — это 0. Для boolean — false. А вот классы инициализируются ссылкой на пустое место, т.е. null.
Собственно об ошибке.
Matrix m = null;
Matrix m2 = new Matrix();
m.sum (m2); // в этот момент случится ошибка
Вероятно, это наиболее распространённый вид ошибок.
Exceptions с точки зрения программиста
Я сам так хочу...
Начнём с примера. Как мы уже видели, попытка сложить две матрицы разного размера может привести к ошибке. С другой стороны понятно, что если попытаться прибавить матрицу большего размера к матрице меньшего, ошибки не произойдет, поскольку индекс суммирования за границы массива не выйдет. Да и потом, даже та ошибка, которую мы получили имеет лишь косвенное отношение к матрицам. Очень хочется научиться самим сообщать об ошибках. Как уже говорилось, ошибку нужно создать и бросить:
public class SquareMatrix extends Matrix {
// ......
SquareMatrix sum(SquareMatrix m) {
// проверка размеров матриц
if (myValues.length != m.myValues.length) {
Exception e = new RuntimeException();
throw e;
}
// остальной код метода....
}
//.....
}
А теперь чуть-чуть модифицируем пример. Во-первых, можно писать чуть-чуть короче:
// ...
throw new RuntimeException();
// ...
А ещё можно сказать, в чем собственно дело:
// ...
throw new RuntimeException("Matrix dimensions differ!");
// ...
Как ловить то, что бросают?
void main(String[] args) {
......
foo();
......
}
void foo() {
......
bar();
......
}
void bar() {
......
throw new RuntimeException();
......
}
После создания Exception, функции начинают аварийно завершаться, пока не завершится программа. Или пока его не поймают.
void foo() {
......
try {
......
bar(); // в процессе выполнения bar() будет брошен RuntimeException
// этот код не будет выполняться
......
} catch (Exception e) {
// после броска, управление будет передано сюда.
......
}
// а потом сюда.
......
}
При броске исключения, начнёт разматываться стек. Потом, попав в блок "try", управление перескочит в "catch", будет выполняться код из этого блока, а потом продолжит выполняться остальная программа после блоков try-catch.
Что можно сделать с пойманным Exception?
Его можно просто проигнорировать. Например вместо меток можно использовать такой фрагмент кода:
try {
......
throw new RuntimeException();
.....
} catch (Exception e) {
}
......
Можно снова бросить тот же Exception:
try {
......
throw new RuntimeException();
.....
} catch (Exception e) {
throw e;
}
Если мы поймали Exception, раскрутка стека останавливается, исключение дальше не идёт, и stacktrace не печатается. Об этом мы должны позаботиться сами, вызвав метод Exception.printStackTrace().
Ещё один почти синтетический пример: если нам захочется напечатать stacktrace не прерывая выполнение программы, можно написать так:
Exception e = new Exception();
e.printStackTrace();
Написав "catch (Exception e)", мы тем самым говорим системе, что мы хотим ловить все исключения. Как правило, какие-то действия заключаются в блок "try-catch", если программист ожидает, что в этом месте какое-то из действий может бросить какое-то (как правило конкретное) исключение. При этом, вероятно, нежелательно реагировать на другие виды ошибок. Правильнее бросать и ловить максимально конкретное исключение. Бросать всегда непосредственно RuntimeExpection — очень плохая идея, очень плохой стиль программирования. Например в следующем примере будут ловиться только арифметические исключения (т.е. ArithmeticException и его наследники).
try {
// какой-то код, который может кидать исключения.
......
} catch (ArithmeticException ae) {
// обработать арифметическую ошибку
......
}
Если нужно ловить несколько разных типов исключений можно поставить несколько catch-блоков подряд:
try {
// код
......
} catch (ArithmeticException e) {
......
} catch (MatrixException e) {
......
} catch (Exception e) {
// поймать все остальные исключения
}
Порядок catch'ей важен. Пытаться поймать родителя до потомка — синтаксическая ошибка. Например, нельзя написать catch (Exception e) {...} до catch (ArithmeticExpeption e) {...}, поскольку Excepton — предок класса ArithmeticException.
Блок finally
Опять начнём с примера. На этот раз пример будет на смеси java и русского. Будем работать с файлами. Почти все или даже все функции, работающие с файлами, могут бросатьIOException.
try {
открыть файл;
прочитать;
если (условие) {
выйти из функции;
}
и т.п.
} catch (IOException e) {
...
}
У приведённого фрагмента кода одна основная проблема: он не закрывает файл. Добавим этот код:
try {
открыть файл;
прочитать;
если (условие) {
закрыть файл;
выйти из функции;
}
и т.п.
закрыть файл;
} catch (IOException e) {
закрыть файл;
...
}
Итого, код закрыть файл продублирован три раза. Если мы усложним пример, дублированного кода станет ещё больше. Как раз для подобных ситуаций (когда какое-то действие нужно сделать в любом случае, независимо от того, как произойдет выход из некого куска кода) в Java существует последняя часть конструкции try-catch-finally:
try {
открыть файл;
прочитать;
если (условие) {
выйти из функции;
}
и т.п.
} catch (IOException e) {
...
} finally {
закрыть файл;
}
Код из блока finally выполнится в любом случае: при нормальном выходе из try, после обработки исключения или при выходе по команде return.
Код в блоке finally должен быть максимально простым: например, если внутри блока finally будет брошено какое-либо исключение или просто встретится оператор return, брошенное в блоке try исключение (если таковое было брошено) будет забыто. Некоторые языки (например, C#) запрещают использовать в блоке finally некоторые языковые конструкции.
Замечание: можно писать просто try-finally:
try {
// какой-то код
// ......
} finally {
// уборка мусора и т.п.
}
Виды ошибок

Дерево наследования для класса Throwable
В языке Java для всего, что можно бросить есть специальный класс — Throwable. Дерево его наследников см. рисунок. Класс Error и его подклассы предназначены для системных ошибок. Свои собственные классы-наследники для Error писать (за очень редкими исключениями) не нужно, а ловить их не принято. Как правило это действительно фатальные ошибки, пытаться обработать которые довольно бессмысленно.
Все исключения в Java делятся на две большие группы:
RuntimeExceptionи его наследники;- все остальные.
Обо всех бросаемых (и не обрабатываемых прямо в самом методе) исключениях из второй группы надо предупреждать в объявлении функции:
void foo() throws IOException {
...
throw new IOException();
...
}
В противном случае компилятор выдаст ошибку. Теперь, если мы из какой-то функции попытаемся вызвать foo, мы или должны обернуть вызов в try {...} catch(IOException e) {...}, или прописать в объявлении вызывающего метода throws IOException. И т.д. Если метод может бросить несколько разных исключений второй группы, их надо объявлять через запятую:
void bar() throws IOException, OtherException {
...
}
Замечание: если вдруг метод по каким-то причинам бросает Error, это не нужно объявлять. Считается, что Error и RuntimeException могут сами возникнуть в любом месте программы.
Замечание 2: теперь мы можем точно сформулировать, из чего состоит сигнатура метода: название, типы и порядок параметров и список бросаемых исключений.
