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