Обработка исключений в Java

🔥 🚀 Важно для всех, кто работает с 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».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх