Базовым классом для всех исключений в языке Java является класс Throwable. Поэтому, если мы хотим обработать в блоке catch все возможные исключения, то мы можем использовать этот тип.
Данный класс определяет ряд базовых методов, отмечу основные из них:
String getMessage(): возвращает строку подробного сообщения об исключении
StackTraceElement[] getStackTrace(): возвращает информацию о трассировке стека, выведенной функцией printStackTrace()
void printStackTrace(): выводит трассировку стека в стандартный поток ошибок
Используем эти методы для получения информации об исключении при его обработке:
class Program{
public static void main(String[] args) {
try{
// для эмуляции ошибки делим число на 0
System.out.println(5 / 0);
}
catch(Throwable ex){
// выводим сообщение об ошибке
System.out.println("Error: " + ex.getMessage());
// выводим трасировку стека
ex.printStackTrace();
}
}
}
Здесь для генерации исключения в блоке try делим число на 0. А в блоке catch объявляем переменную ex, которая представляет тип Throwable и выводим на консоль
по отдельности сообщение об ошибке и трассировку стека (которая также включает сообщение об ошибке). В итоге мы получим следующий консольный вывод:
Error: / by zero java.lang.ArithmeticException: / by zero at Program.main(Program.java:7)
Трассировка стека позволяет более детально узнать, какой реально тип ошибки, где произвошла ошибка. Так, из выведенной информации мы видим, что в реальности исключение представляет тип
java.lang.ArithmeticException (то есть тип исключений для арифметических операций). А сама ошибка возникла на 7 строке файла Program.java в методе Program.main
От класса Throwable наследуются два других класса: Error
и Exception. Все остальные классы являются производными от этих двух классов.
Класс Error описывает внутренние ошибки в исполняющей среде Java. Программист имеет очень ограниченные возможности для обработки подобных ошибок. При возникновении такой ошибки мало что можно сделать, кроме как уведомить пользователя и попытаться корректно завершить программу. Такие ситуации встречаются довольно редко.
Собственно исключения наследуются от класса Exception. Среди этих исключений следует выделить класс RuntimeException. Все классы исключений, которые унаследованы от классов
Error и RuntimeException еще называются непроверяемыми исключениями (unchecked exceptions) -
компилятор не проверяет факт обработки таких исключений.
Такие исключения нередко являются следствием ошибок разработчика, например, неверное преобразование типов или выход за пределы массива.
Некоторые из классов непроверяемых исключений:
ArithmeticException: исключение, возникающее при делении на ноль
IndexOutOfBoundException: индекс вне границ массива
IllegalArgumentException: использование неверного аргумента при вызове метода
NullPointerException: использование пустой ссылки
NumberFormatException: ошибка преобразования строки в число
Все остальные классы, образованные от класса Exception, называются проверяемыми исключениями (checked exceptions).
Некоторые из классов проверяемых исключений:
CloneNotSupportedException: класс, для объекта которого вызывается клонирование, не реализует интерфейс Cloneable
InterruptedException: поток прерван другим потоком
ClassNotFoundException: невозможно найти класс
В итоге получается примерно следующая иерархия исключений:
Таким образом, вместо исключения общего типа Throwable мы можем обрабатывать конкретные типы исключений.
Например, при делении на 0 генерируется исключение типа ArithmeticException. Обработаем его:
class Program{
public static void main(String[] args) {
try{
// для эмуляции ошибки делим число на 0
System.out.println(5 / 0);
}
catch(ArithmeticException ex){
// выводим сообщение об ошибке
System.out.println("Error: " + e.getMessage());
// выводим трасировку стека
e.printStackTrace();
}
}
}
В одной и той же программе на одном и том же участке кода может возникать несколько разных типов исключений. Обрабатывая исключения типа Throwable или Exception,
мы можем покрыть все эти типы исключений. Но Java также позволяет разграничить обработку различных видов исключений, добавив дополнительные блоки catch для каждого обрабатываемого типа исключения:
class Program{
public static void main(String[] args) {
printNumber(0); // По индексу 0 число 1
printNumber(10); // Выход за пределы массива
printNumber(3); // Ошибка преобразования из строки в число
}
static void printNumber(int index){
String[] data = {"1", "a", "2", "b", "3", "c"};
try{
// пытаемся преобразовать значение массива по индексу в число
int result =Integer.parseInt(data[index]);
System.out.printf("По индексу %d число %d\n", index, result);
}
catch(ArrayIndexOutOfBoundsException _){
System.out.println("Выход за пределы массива");
}
catch(NumberFormatException _){
System.out.println("Ошибка преобразования из строки в число");
}
}
}
В данном случае основные действия разворачиваются в методе printNumber(). Этот метод получает извне индекс и пытается преобразовать значение из массива по этому индексу в число.
И в этом случае у нас может быть несколько вариантов развития ситуации:
По переданному индексу в массиве располагается число (только в виде строки), тогда преобразование завершается успешно
Переданный индекс находится вне допустимого диапазона, тогда мы столкнемся с исключением типа ArrayIndexOutOfBoundsException
Переданный индекс действителен, но значение по нему невозможно преобразовать в число. Тогда мы столкнемся с исключением типа NumberFormatException
Для отслеживания последних двух ситуаций в конструкции try..catch определено два блока catch:
catch(ArrayIndexOutOfBoundsException _){
System.out.println("Выход за пределы массива");
}
catch(NumberFormatException _){
System.out.println("Ошибка преобразования из строки в число");
}
Если у нас возникает исключение определенного типа, то оно переходит к соответствующему блоку catch.
Подобная стратегия позволяет разграничить обработку различных типов исключений. Однако, что, если у нас в коде может возникнуть большее количество исключений - не создавать же на каждый тип исключений из стандартной
библиотеки Java свой блок catch! В этом случае мы можем определить ряд блоков catch для тех типов, для которых мы хотим определить специальную обработку. А для всех остальных типов
исключений определить блок catch с обработкой более общего типа как Throwable или Exception. Например:
class Program{
public static void main(String[] args) {
printNumber(0); // По индексу 0 число 1
printNumber(10); // Выход за пределы массива
printNumber(3); // For input string: "b"
}
static void printNumber(int index){
String[] data = {"1", "a", "2", "b", "3", "c"};
try{
// пытаемся преобразовать значение массива по индексу в число
int result =Integer.parseInt(data[index]);
System.out.printf("По индексу %d число %d\n", index, result);
}
catch(ArrayIndexOutOfBoundsException _){ // обрабатываем выход за границы массива
System.out.println("Выход за пределы массива");
}
catch(Exception ex){ // обрабатываем все остальные типы исключений
System.out.println(ex.getMessage()); // выводим сообщение об исключении
ex.printStackTrace();
}
}
}
В данном случае отдельно обрабатываем исключение типа ArrayIndexOutOfBoundsException, то есть выход за границы массива. А для других типов обрабатываем исключения типа Exception
При этом стоит учитывать, что блоки catch для обработки более конкретных (производных) типов (как ArrayIndexOutOfBoundsException) должны располагаться сначала, а блоки
catch для обработки более общих (базовых) типов (как Exception или Throwable) - в конце, как в примере выше.