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

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

Привет! Сегодня мы подробно рассмотрим обработку исключений в Java. Не хочется об этом говорить, но огромная часть работы программиста – это работа с ошибками. Чаще всего с собственными. Оказывается, нет людей, которые не совершают ошибок. И программ таких тоже не существует.

Конечно, при работе с ошибкой главное – понять ее причину. А сбои в работе программы случаются по множеству причин: от банальной опечатки до непредвиденного поведения внешних ресурсов. В какой-то момент создатели Java задались вопросом: как обрабатывать наиболее вероятные исключительные ситуации, возникающие во время выполнения программы? Полностью избежать их нереально, программисты способны написать такое, что вы даже представить себе не можете. 🙂

Поэтому нам нужно дать языку механизм для работы с ошибками. Другими словами, если в вашей программе произошла непредвиденная ситуация – вам нужен некий сценарий действий: как программа должна реагировать, продолжать ли выполнение, каким образом уведомлять об ошибке и можно ли восстановиться без потерь.

Сегодня мы познакомимся с этим механизмом. Он называется “обработка исключений в Java“.

Что такое исключение?

Исключение – это исключительная, незапланированная ситуация, которая возникает во время работы программы и нарушает ее нормальное выполнение.

Существует множество исключений.

Например, вы написали код, который считывает текст из файла и выводит на экран первую строку.

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. Вывод в консоли будет выглядеть так:

Исключение в потоке “main” java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Система не может найти указанный путь)

В Java каждое исключение представлено отдельным классом. Все эти классы исключений происходят от общего “предка” – класса Throwable. Он является родительским для двух основных ветвей:

  • Error – серьезные ошибки, от которых программа обычно не может оправиться (например, OutOfMemoryError);
  • Exception – исключения, которые можно перехватить и обработать.

Имя класса исключения обычно кратко отражает причину возникновения исключения:

  • FileNotFoundException – файл не найден;
  • ArithmeticException – ошибка при выполнении математической операции, например, деление на ноль;
  • ArrayIndexOutOfBoundsException – индекс выходит за границы массива. Например, это исключение возникает, если вы пытаетесь отобразить позицию 23 массива, который имеет всего 10 элементов.
int[] numbers = new int[10];
System.out.println(numbers[23]); // здесь будет выброшено исключение ArrayIndexOutOfBoundsException

Всего в Java таких классов почти 400! Зачем так много? Чтобы программистам было удобнее с ними работать. Представьте себе ситуацию: вы запускаете программу, и она завершает работу с сообщением:

Exception in thread "main"

Ухххх :/ Это не очень помогает. Непонятно, что произошло и почему. В сообщении нет полезной информации. Но если класс исключения указан явно, как в следующем примере:

Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (The system cannot find the specified path)

Сразу становится ясно, в чем проблема. Имя класса подсказывает тип ошибки, а иногда и ее причину. Это сильно упрощает диагностику и исправление проблем в программе.

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

В Java есть специальные блоки кода для работы с обработкой исключений: try, catch и finally.

Код, в котором, по мнению программиста, может возникнуть исключение, помещается в блок try. Это не означает, что исключение произойдет именно здесь. Это значит, что оно может здесь возникнуть, и программист знает об этой возможности.

Тип ожидаемой ошибки указывается в блоке catch. Здесь же содержится весь код, который должен быть выполнен при возникновении исключения. В скобках блока 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);// This line of code will throw an exception

       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 мы пытаемся разделить на “0”, что приводит к 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 блоков, если хотите. Конечно, лучше не писать код, который может выдавать 50 различных видов исключений. 🙂

Номер три. Как узнать, какие исключения может выдавать ваш код?

Некоторые из них вы можете предсказать сами, но все предусмотреть невозможно. Именно поэтому Java-компилятор знает о многих потенциальных исключениях и требует, чтобы вы их обрабатывали.

Например, если вы пишете код, который может выбросить проверяемые (checked) исключения, такие как IOException или FileNotFoundException, и не обрабатываете их, то компилятор не даст вам собрать программу – вы получите ошибку компиляции. Мы увидим примеры этого ниже.

Также существует блок finally, который выполняется всегда, независимо от того, произошло исключение или нет. Это удобно, например, для закрытия файлов, освобождения ресурсов или других обязательных действий.

try {
// код с возможным исключением<br>
} catch (Exception e) {
System.out.println("Произошло исключение!");
} finally {
System.out.println("Этот код выполнится в любом случае.");
}

Теперь несколько слов об обработке исключений

В Java есть два способа обработки исключений:

  • С первым мы уже сталкивались: метод может обрабатывать исключения самостоятельно с помощью блока 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

В текущем виде этот код не компилируется, потому что в нем есть необработанные исключения:

  • В строке 1 мы указываем путь к файлу. Компилятор знает, что этот код может привести к FileNotFoundException.
  • В строке 2 – читаем строку из файла. Это может вызвать 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 мы указываем через запятую список всех типов исключений, которые может выбросить метод. В данном случае мы указали IOException, и этого достаточно, так как FileNotFoundException является ее подклассом.

Зачем это нужно?

Теперь, если кто-то захочет вызвать метод printFirstString() в своей программе, ему придется решить, как поступить с потенциальными исключениями.

Например, предположим, что где-то в программе один из ваших коллег написал метод, который вызывает printFirstString():

public static void yourColleagueMethod() {

   // Your colleague's method does something

   //...and then calls your printFirstString() method with the file it needs
   printFirstString("C:\\Users\\Henry\\Desktop\\testFile.txt");
}
Run Code

Этот код не компилируется! Почему?

Потому что printFirstString() может выбросить исключение, но в yourColleagueMethod() это никак не обработано. Поэтому теперь ответственность за исключения ложится на того, кто вызывает метод.

Другими словами, у yourColleagueMethod() теперь два варианта:

  • либо обернуть вызов метода в try-catch блок;
  • либо тоже добавить throws и передать исключения еще выше.
public static void yourColleagueMethod() throws FileNotFoundException, IOException {
   // The method does something

   //...and then calls your printFirstString() method with the file it needs
   printFirstString("C:\\Users\\Henry\\Desktop\\testFile.txt");
}
Run Code

В этом случае следующему методу в стеке вызовов придётся иметь дело с исключениями. Поэтому это поведение называют “выбрасыванием исключения вверх”. Если вы бросаете исключения вверх с помощью ключевого слова throws, ваш код скомпилируется. В этот момент компилятор как бы говорит: “Ладно, ладно. Ваш код может выбрасывать исключения, но если вы честно это указали – я вас пропущу. Но мы еще вернемся к этому разговору”!

И действительно: при каждом следующем вызове этого метода компилятор будет требовать обработки исключений, пока они не будут перехвачены или программа не завершится с ошибкой.

Наконец, мы поговорим о finally блоке (извините за каламбур). Это последняя часть try-catch-finally триады обработки исключений.

Блок 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 выполнится после него.
  • Если произойдет исключение, и выполнится catch – после него тоже выполнится finally.

Даже если в try или catch есть оператор return, блок finally все равно будет выполнен перед выходом из метода.

Зачем нужен блок finally

Главное предназначение блока finally – выполнение действий, которые должны быть завершены в любом случае, независимо от того, произошло исключение или нет.

Чаще всего 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

Теперь мы уверены, что ресурсы будут освобождены в любом случае, даже если в блоке try произойдет исключение.

Это не все, что вам нужно знать об исключениях. Обработка ошибок – одна из самых важных тем в программировании. Ей посвящено множество статей, и чем глубже вы погружаетесь в разработку, тем больше начинаете понимать ее значение.

В одной из следующих статей мы расскажем о том, какие бывают типы исключений и как создавать свои собственные исключения. 🙂 Увидимся!

Перевод статьи «Exception Handling in Java».

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

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

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