Нередко в процессе выполнения программы могут возникать ошибки, при том необязательно по вине разработчика. Некоторые из них трудно предусмотреть или предвидеть, а иногда и вовсе невозможно. Так, например, может неожиданно оборваться сетевое подключение при передаче файла. Подобные ситуации называются исключениями.
В языке Java предусмотрены специальные средства для обработки подобных ситуаций. Одним из таких средств является конструкция try...catch...finally. При возникновении исключения в блоке try управление переходит в блок catch, который может обработать данное исключение. Если такого блока не найдено, то пользователю отображается сообщение о необработанном исключении, а дальнейшее выполнение программы останавливается. И чтобы программа не упала из-за возникшей ошибки, применяется блок try..catch..finally:
try{
}
catch (Тип_исключения переменная){
}
finally{
}
При применении блока try...catch..finally вначале выполняются все инструкции в блоке try. Если в
этом блоке не возникло исключений, то после его выполнения начинает выполняться блок finally. И затем конструкция try..catch..finally завершает свою работу.
Если же в блоке try вдруг возникает исключение, то обычный порядок выполнения останавливается, и среда выполнения ищет блок catch, который может обработать данный тип исключения.
Если нужный блок catch найден, то он выполняется, и после его завершения выполняется блок finally.
Если нужный блок catch
Рассмотрим простеший пример:
class Program{
public static void main(String[] args) {
int a = 5;
int b = 0;
int result = a / b;
System.out.printf("Результат: %d\n", result);
}
}
В данном случае происходит деление числа на 0, что приведет к генерации исключения. В итоге мы увидим на консоли следующую картину:
Exception in thread "main" java.lang.ArithmeticException: / by zero at Program.main(Program.java:7)
Таким образом, консоль нас проинформирует, что в процессе выполнения программы произошло исключение типа java.lang.ArithmeticException и снабдит сообщение куцей информацией об исключении: "by zero".
Тем не менее по этому сообщению мы можем понять, что проблема в делении на ноль. Кроме того, мы видим, что непосредственно проблема возникла в методе Program.main, который располагается в файле
Program.java на строке 7. Это облегчает поиск проблемого участка кода в исходном файле.
Конечно, конкретно эта ситуация в некотором роде искуственна - зачем делить на ноль? А если число деление приходит извне, то перед делением мы можем проверить, ноль это или не ноль, и тем самым избежать падения программы:
class Program{
public static void main(String[] args) {
divide(10, 2);
divide(10, 0);
}
static void divide(int a, int b){
if(b != 0) System.out.println(a / b);
else System.out.println("Деление на ноль");
}
}
Но проблема заключается в том, что не все ситуации мы можем предотвратить таким образом. Поэтому посмотрим, как мы можем применить конструкцию try...catch...finally,
чтобы избежать подобного аварийного завершения программы:
class Program{
public static void main(String[] args) {
divide(10, 0);
System.out.println("Конец программы");
}
static void divide(int a, int b){
try{
int result = a / b;
System.out.printf("Результат: %d\n", result);
}
catch(Throwable _){
System.out.println("Возникло исключение!");
}
finally{
System.out.println("Блок finally");
}
}
}
Рассмотрим основные моменты. Прежде всего у нас есть некоторый код, который потенциально может вызвать исключение:
int result = a / b;
Поэтому его помещаем блок try.
try{
int result = a / b;
System.out.printf("Результат: %d\n", result);
}
Если в блоке try вдруг возникает исключение (при b = 0), то обычный порядок выполнения останавливается и переходит к инструкции сatch.
Выражение catch имеет следующий синтаксис:
catch (тип_исключения имя_переменной){
// выполняемые инструкции при возвникновении исключения
}
После оператора catch в скобках указывается тип исключения и затем имя переменной этого исключения (то есть как и при объявлении переменной).
Если возникшее исключение соответствует этому типу, то выполняется данный блок catch.
В нашем случае в качестве типа исключения использован максимально широкий тип, который соответствует любому исключению - это класс Throwable:
catch(Throwable _){
System.out.println("Возникло исключение!");
}
Таким образом, обработка исключения типа Throwable предполагает обработку любого исключения, в том числе деления на ноль. Но здесь в целях упрощения примера нам пока переменная этого исключения не важна, мы ее никак не используем, поэтому вместо имени переменной я использовал прочерк. А в самом блоке
catch просто выводится диагностическое сообщение.
И после блока catch идет блок finally, который также просто выводит сообщение на консоль.
finally{
System.out.println("Блок finally");
}
Посмотрим выполнение выше определенной конструкции try..catch..finally на двух примерах: при упешном и неудачном выполнении программы.
Возьмем первую ситуацию - когда делитель равен 0:
public static void main(String[] args) {
divide(10, 0);
System.out.println("Конец программы");
}
Поскольку делитель равен 0, и очевидно возникнет ошибка, соответственно программа не сможет выполнить деление, и мы полуим следующий консольный вывод:
Возникло исключение! Блок finally Конец программы
Если смотреть по инструкциям, то ход программы выглядит следующим образом:
divide(10, 0);
Пеореходим в метод divide
int result = a / b;
Поскольку b равно 0, переход в блок catch
System.out.println("Возникло исключение!");
Переход в блок finally
System.out.println("Блок finally");
Блок finally отработал, метод divide завершил свое выполнение, поэтому возвращаемся в мтеод main
System.out.println("Конец программы");
Последняя инструкция метода main выполнена, программа завершена
Теперь посмотрим на другую ситуацию - когда делитель не равен 0:
public static void main(String[] args) {
divide(10, 2);
System.out.println("Конец программы");
}
Эта программа успешно выполнит деление, и мы полуим следующий консольный вывод:
Результат: 5 Блок finally Конец программы
Если смотреть по инструкциям, то ход программы выглядит следующим образом:
divide(10, 2);
Пеореходим в метод divide
int result = a / b;
Поскольку b равно 2, выполняется деление, а переменная result получает значение 5
System.out.printf("Результат: %d\n", result);
Блок try отработал, поэтому переходим в блок finally
System.out.println("Блок finally");
Блок finally отработал, метод divide завершил свое выполнение, поэтому возвращаемся в мтеод main
System.out.println("Конец программы");
Последняя инструкция метода main выполнена, программа завершена
При использовании конструкции try..catch..finally обязательным блоком является только блок try. Один из остальных двух блоков можно опустить и оставить либо catch
(если блок try..catch:
static void divide(int a, int b){
try{
int result = a / b;
System.out.printf("Результат: %d\n", result);
}
catch(Throwable _){
System.out.println("Возникло исключение!");
}
}
Можно оставить блок finally, но в этом случае при возникновении исключения оно не будет обработано, и программа упадет с ошибкой:
static void divide(int a, int b){
try{
int result = a / b;
System.out.printf("Результат: %d\n", result);
}
finally{
System.out.println("Блок finally");
}
}
Внутри конструкции try..catch..finally может использоваться оператор return для возвращения результата из метода. Например:
static int parse(String s)
{
try
{
return Integer.parseInt(s);
}
catch(Throwable _){
System.out.println("Ошибка преобразования");
return 0;
}
}
Здесь в блоке try возвращается результат метода Integer.parseInt(). Этот метод преобразует строку в число. Однако если строку нельзя конвертировать в число,
то возникает ошибка. С помощью блока catch мы перехватываем ошибку и возвращаем число 0. Хотя здесь спорный момент: стоит ли возвращать значение при ошибке, а если возвращать, то какое. И Java для
подобных ситуаций имеет более гибкие механизмы. Но возьмем данный пример за основу.
Далее в программе мы можем вызвать метод parse и получить из него результат:
class Program{
public static void main(String[] args) throws Exception {
int result = parse("5");
System.out.println(result); // 5
result = parse("t"); // Ошибка преобразования
System.out.println(result); // 0
}
static int parse(String s)
{
try
{
return Integer.parseInt(s);
}
catch(Throwable _){
System.out.println("Ошибка преобразования");
return 0;
}
}
}
Но стоит учитывать, что, если конструкция try содержит блок finally, то этот блок обязательно выполняется. И если блок finally также содержит оператор return, то он маскирует исходное возвращаемое значение. Например
class Program{
public static void main(String[] args) throws Exception {
int result = parse("5");
System.out.println(result); // -1
result = parse("t"); // Ошибка преобразования
System.out.println(result); // -1
}
static int parse(String s)
{
try
{
return Integer.parseInt(s);
}
catch(Throwable _){
System.out.println("Ошибка преобразования");
return 0;
}
finally{
return -1;
}
}
}
Здесь блок finally возвращает значение -1. И вне зависимости от того, успешно было ли преобразование или нет, метод все равно возвратит -1, так как после блока try и catch
обязательно выполняется блок finally
Стоит отметить, что конструкция try..catch на низком уровне довольно дорогая операция. И если ряд исключительных ситуаций может быть предвиден разработчиком, то лучше обойтись без try..catch, а подобные ситуации обработать с помощью условных конструкций. Как, например,
в случае с делением на 0 предпочтительнее проверить второе число на 0, чем использовать конструкцию try..catch.