🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!
В этой статье подробно рассмотрен механизм обработки исключений в Java. Значительная часть работы разработчика связана с устранением ошибок. Чаще всего — собственных. Ошибаются все, и программ без ошибок не существует.
При работе с ошибками важно в первую очередь установить их причину. А причин, по которым в коде появляются баги, может быть масса.
Создатели Java в какой-то момент задались вопросом: как обрабатывать наиболее распространённые ошибки программирования? Полностью исключить их невозможно — разработчики способны создавать самые непредсказуемые сценарии.
Поэтому в языке должен быть предусмотрен механизм для работы с ошибками. Иными словами, если в программе возникает ошибка, должен быть какой-то сценарий: что делать дальше? Как должна себя вести программа в такой ситуации?
Сегодня мы разберёмся, как это устроено. Механизм называется «исключения в Java».
Что такое исключение?
Исключение (exception) — это непредусмотренная ситуация, которая возникает во время выполнения программы.
Существует множество различных исключений.
Например, вы написали код, который читает текст из файла и выводит первую строку.
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
}
}
Run CodeНо что, если такого файла не существует?
В таком случае программа сгенерирует исключение — FileNotFoundException
.
Вывод:
Exception in thread “main” java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (The system cannot find the specified path)
В Java каждое исключение представлено отдельным классом.
И все они наследуются от одного общего “родителя” — класса Throwable
. Имена классов исключений, как правило, отражают причину возникновения исключения:
FileNotFoundException
— файл не найденArithmeticException
— что-то пошло не так при выполнении математической операцииArrayIndexOutOfBoundsException
— выход за пределы массива. Такое исключение возникнет, если, например, обратиться к элементу с индексом 23 в массиве, содержащем всего 10 элементов.
В целом, в Java существует почти 400 подобных классов. Почему так много? Это сделано для удобства разработчиков: чем точнее имя исключения, тем проще понять суть ошибки.
Представьте, что вы пишете программу, и во время её выполнения выбрасывается исключение следующего вида:
Exception in thread "main"
Это сообщение мало чем помогает. Непонятно, в чём заключается ошибка и где она возникла. Полезной информации — ноль.
Но как раз в этом и помогает большое количество классов исключений в Java: они дают разработчику главное — тип ошибки и её вероятную причину (которая обычно заложена прямо в названии класса). Совсем другое дело, если бы было написано, например:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (The system cannot find the specified path)
Сразу понятно, в чём может быть проблема и с чего начать, чтобы её устранить.
Исключения в Java, как и любые другие экземпляры классов, являются объектами.
Перехват и обработка исключений в Java
В Java для работы с исключениями предусмотрены специальные блоки кода: try
, catch
и finally
.

Код, в котором, по мнению программиста, может произойти исключение, помещается в блок try
. Это не значит, что ошибка обязательно возникнет. Это значит, что она возможна, и программист это понимает.
В блоке catch
указывается тип предполагаемого исключения. Также в нём размещается код, который должен выполниться в случае возникновения ошибки.
Пример:
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Error! File not found!");
}
}
Run CodeВывод:
Error! File not found!
Код организуется в два блока. В первом (try
) предполагается возможность возникновения исключения, например, “Файл не найден”. Это блок, где ожидается потенциальная ошибка.
Во втором блоке (catch
) указываем, что делать, если такая ошибка произойдёт. И конкретно какой тип ошибки — FileNotFoundException
. Если в круглых скобках блока catch
указать другой класс исключения, то FileNotFoundException
не будет перехвачен.
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (ArithmeticException e) {
System.out.println("Error! File not found!");
}
}
Run CodeВывод:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (The system cannot find the specified path)
Блок catch
не был выполнен, поскольку он был настроен на перехват исключений типа ArithmeticException
, в то время как в блоке try
возникло исключение типа FileNotFoundException
.
Так как для FileNotFoundException
не был реализован отдельный обработчик, программа вывела стандартное сообщение об ошибке для этого типа исключения.
Здесь стоит обратить внимание на три момента.
Первое: как только в блоке try
на какой-то строке возникает исключение, остальной код ниже по этому блоку выполняться не будет. Выполнение программы сразу же переходит в блок catch
. Например:
public static void main(String[] args) {
try {
System.out.println("Divide by zero");
System.out.println(366/0);
System.out.println("This");
System.out.println("code");
System.out.println("will not");
System.out.println("be");
System.out.println("executed!");
} catch (ArithmeticException e) {
System.out.println("The program jumped to the catch block!");
System.out.println("Error! You can't divide by zero!");
}
}
Run CodeВывод:
Divide by zero
The program jumped to the catch block!
Error! You can't divide by zero!
На второй строке блока try
происходит попытка деления на ноль, что вызывает исключение ArithmeticException
.
Следовательно, строки 3–10 в блоке try
не будут выполнены. Как уже отмечалось, программа сразу же переходит к выполнению блока catch
.
Второе. catch
-блоков может быть несколько. Если код в try
может выбросить разные типы исключений, можно написать catch
-блок для каждого из них.
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
System.out.println(366/0);
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Error! File not found!");
} catch (ArithmeticException e) {
System.out.println("Error! Division by 0!");
}
}
Run CodeВ приведенном примере два блока catch
. Если в блоке try
произойдёт исключение FileNotFoundException
, выполнится первый блок catch
. Если возникнет ArithmeticException
, будет выполнен второй блок.
При желании можно написать хоть 50 блоков catch
. Хотя, конечно, лучше не писать такой код, который может выбросить 50 разных исключений.
Третье. Как понять, какие исключения может выдавать ваш код?
Некоторые из них можно предугадать, но всё держать в голове нереально.
Именно поэтому компилятор Java обладает встроенными знаниями о наиболее типичных исключениях и ситуациях, в которых они возникают.
Например, если компилятор определит, что код потенциально может сгенерировать два различных исключения, он не допустит его компиляции до тех пор, пока эти исключения не будут явно обработаны. Далее мы рассмотрим соответствующие примеры.
Пара слов об обработке исключений
В Java есть 2 способа обработки исключений.
С первым мы уже сталкивались: метод может самостоятельно перехватить исключение с помощью блока catch()
и обработать его.
Есть и второй вариант: метод может «перекинуть» исключение выше по стеку вызовов. Что это значит?
Например, у нас есть класс с методом printFirstString()
, который читает файл и выводит его первую строку:
public static void printFirstString(String filePath) {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String firstString = reader.readLine();
System.out.println(firstString);
}
Run CodeСейчас код не компилируется, поскольку содержит необработанные исключения.
В первой строке указывается путь к файлу — компилятор распознаёт, что здесь потенциально может возникнуть FileNotFoundException
.
В третьей строке происходит чтение данных из файла, что может привести к IOException
(ошибке ввода-вывода).
Компилятор сообщает следующее:
«Я не могу скомпилировать этот код, пока ты не укажешь, как следует поступить при возникновении одного из возможных исключений. А они вполне могут произойти на основе написанного кода.»
Избежать этого не получится — необходимо обработать оба исключения. Напомним первый способ: поместить код в блок try
, а затем добавить два соответствующих блока catch
:
public static void printFirstString(String filePath) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Error, file not found!");
e.printStackTrace();
} catch (IOException e) {
System.out.println("File input/output error!");
e.printStackTrace();
}
}
Run CodeНо это не единственный вариант. Вместо реализации обработки исключений в теле метода, можно воспользоваться возможностью пробросить их выше по стеку вызовов.
Ключевые слова для обработки исключений в Java
Это делается с помощью ключевого слова throws
в объявлении метода:
public static void printFirstString(String filePath) throws FileNotFoundException, IOException {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String firstString = reader.readLine();
System.out.println(firstString);
}
Run CodeПосле ключевого слова throws
указывается список всех типов исключений, которые метод может выбросить, через запятую. Зачем это нужно?
Это делается для того, чтобы при вызове метода printFirstString()
ответственность за обработку потенциальных исключений ложилась на вызывающую сторону.
Допустим, что в другой части программы ваш коллега написал метод, который вызывает printFirstString()
:
public static void yourColleagueMethod() {
// Метод вашего коллеги что-то делает.
//...а затем вызывает ваш метод printFirstString(), передавая ему нужный файл
printFirstString("C:\\Users\\Henry\\Desktop\\testFile.txt");
}
Run CodeМы столкнулись с ошибкой и код не компилируется!
Так как в методе printFirstString()
не был реализован механизм обработки исключений, ответственность за это теперь лежит на тех, кто вызывает данный метод.
Другими словами, у метода methodWrittenByYourColleague()
есть два варианта: он должен либо использовать блок try-catch
для обработки обоих исключений, либо повторно выбросить их.
public static void yourColleagueMethod() throws FileNotFoundException, IOException {
// Метод выполняет какие-то действия
//...после чего вызывает метод printFirstString(), передавая ему необходимый файл
printFirstString("C:\\Users\\Henry\\Desktop\\testFile.txt");
}
Run CodeВо втором случае следующий метод в стеке вызовов — метод, вызывающий methodWrittenByYourColleague()
— обязан обработать исключения. Именно поэтому этот процесс называют “выбрасыванием или передачей исключения вверх”.
Если вы используете ключевое слово throws
для выброса исключений, ваш код будет скомпилирован. На этом этапе компилятор как бы говорит: “Хорошо, я понимаю, что в вашем коде могут возникнуть исключения, но я его скомпилирую. Однако мы ещё вернемся к этому вопросу!”
Когда же вы вызываете метод с необработанными исключениями, компилятор исполняет своё обещание и напоминает о них ещё раз.
Наконец, поговорим о блоке finally
. Это последняя часть триады обработки исключений try-catch-finally
.
Он отличается тем, что всегда выполняется, независимо от того, что происходит в программе.
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Error! File not found!");
e.printStackTrace();
} finally {
System.out.println ("And here's the finally block!");
}
}
Run CodeВ этом примере код внутри блока finally
будет выполнен в обоих случаях.
Если код в блоке try
выполнится без исключений, блок finally
сработает в конце.
Если выполнение кода в блоке try
прерывается исключением, и управление передается в блок catch
, блок finally
выполнится после выполнения кода внутри блока catch
.
Зачем это нужно?
Основная цель блока finally
— выполнить код, который должен быть выполнен в любом случае.
Например, он часто освобождает ресурсы, используемые программой.
В нашем коде мы открываем поток для чтения данных из файла и передаем его объекту BufferedReader
.
Необходимо закрыть ридер и освободить ресурсы, и это должно быть выполнено в любом случае — как при корректной работе программы, так и при возникновении исключения.
Именно в блоке finally
удобно это делать:
public static void main(String[] args) throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
System.out.println ("And here's the finally block!");
if (reader != null) {
reader.close();
}
}
}
Run CodeТеперь мы уверены, что гарантированно освободим ресурсы, независимо от того, что происходит во время работы программы.
Перевод статьи «Exception Handling in Java».