В этом уроке мы поговорим про механизм исключений. С его помощью происходит управление ошибками, которые возникают во время исполнения программы. Мы разберем концепцию проверяемых и не проверяемых исключений, научимся выбрасывать и перехватывать их.
Концепция исключений не связана с ООП, но во многих языках, включая Java, исключения завязаны на классы, поэтому эта тема рассматривается в данном курсе.
Непроверяемые исключения
Начнем с проблемы. Далеко не все ошибки можно выявить на этапе компиляции, например обращение к несуществующему индексу в массиве. Подобная ошибка возникнет уже во время работы программы и скорее всего остановит ее выполнение:
int[] items = {1, 2, 3};
System.out.println(items[5]);
Запуск такого кода приведет к выбрасыванию (возбуждению) исключения и прерыванию работы программы. В консоли это будет выглядеть так:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
at io.hexlet.Application.main (Application.java:24)
Ошибка содержит не только описание того что произошло, но и указывает на то место, где она возникла включая файл и строку. Это важно для отладки.
Другая похожая ситуация – это деление на ноль. Оно тоже приводит к исключению:
var x = 0;
var value = 3 / x;
// Exception in thread "main" java.lang.ArithmeticException: / by zero
Ошибки такого рода почти всегда являются багами, которые нужно исправить. В Java такие исключения называются непроверяемыми (unchecked) так как они никак специально не обрабатываются и не отслеживаются в отличие от проверяемых исключений.
Такие исключения возможны не только внутри самой Java, но и в коде библиотек и даже в коде вашего приложения. Например, если какая-то библиотека используется неправильно, внутри нее может сработать код, который выбрасывает соответствующее исключение:
throw new RuntimeException("Сообщение об ошибке");
Из этого кода видно, что исключение это объект. В этот объект передается сообщение об ошибке плюс в него автоматически записывается информация о том, где это исключение было выброшено. Под выбрасыванием подразумевается использование конструкции throw
, а не создание объекта исключения, объект можно создать и заранее.
var error = new RuntimeException("Сообщение об ошибке");
throw error;
В данном случае используется класс RuntimeException
, но так бывает не всегда. Под разные типы ошибок создаются разные исключения для удобства работы с ними, например, это помогает во время анализа текста ошибки, так как глядя на класс исключения сразу понятно о чем идет речь.
Проверяемые исключения
Проверяемые исключения – это исключения, которые могут возникнуть в любом случае, даже если в программе нет багов. Чаще всего они возникают при взаимодействии Java с внешним миром. Самое простое – это чтение файла, если файла не существует, то во время его чтения возникнет исключение.
import java.nio.file.Files;
import java.nio.file.Paths;
public class Application {
public static void main(String[] args) {
// Создаем объект Paths для описания пути
var path = Paths.get("path/to/file.txt");
// Читаем файл и преобразуем в строку
var content = new String(Files.readAllBytes(path));
// Выводим содержимое на экран
System.out.println(content);
}
}
Если мы напишем такой код, то компилятор выдаст ошибку. Он знает, что метод Files.readAllBytes()
выбрасывает исключение IOException
, которое является проверяемым. Такое исключение должно быть обработано, так как оно может возникнуть независимо от желания программиста. Обработка исключений делается с помощью конструкции try..catch.
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
public class Application {
public static void main(String[] args) {
var path = Paths.get("path/to/file.txt");
try {
var content = new String(Files.readAllBytes(path));
System.out.println(content);
} catch (IOException e) {
// Обрабатываем ошибку так как нужно в этой ситуации
System.out.println("Проверьте что файл " + path + " существует и к нему есть доступ");
}
// Код который идет тут, будет выполнен
}
}
# Так выглядит текст ошибки, если ее вывести в консоль
# e.printStackTrace()
Exception in thread "main" java.nio.file.NoSuchFileException: path/to/file.txt
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:261)
at java.base/java.nio.file.Files.newByteChannel(Files.java:379)
at java.base/java.nio.file.Files.newByteChannel(Files.java:431)
at java.base/java.nio.file.Files.readAllBytes(Files.java:3263)
at io.hexlet.Application.main(Application.java:27)
Блок catch
перехватывает исключение, которое было выброшено в блоке try
. Само исключение попадает в catch
как объект e
, который можно при необходимости использовать. Вся остальная обработка лежит на плечах программиста, вплоть до того, что внутри catch
может быть выброшено какое-то другое исключение, но это уже продвинутая техника, обычно используемая в библиотеках.
Перехват исключения позволяет программе продолжить работать дальше без остановки. Код после конструкции try..catch продолжит выполнение как ни в чем не бывало.
В примере выше мы обрабатываем исключение в том же месте где оно и произошло, но в реальности, ошибки обычно происходят не там, где они могут быть обработаны. Более того, сам механизм исключений появился именно по этой причине. В коде реальных проектов десятки, сотни и миллионы строк кода. Такой код разбит на множество слоев, в которых один метод вызывает другой, этот вызывает третий и так далее, подобные цепочки могут достигать сотни методов в глубину вызовов. Здесь проявляется то, что код, который вызывается на нижнем уровне, может не знать как конкретно нужно обработать возникшее исключение. Представьте себе библиотеку, которая умеет скачивать файлы по сети. Эта библиотека может использоваться в совершенно разных приложениях, которые по-разному показывают ошибки загрузки.
Похожую ситуацию мы можем имитировать и в нашем примере, если вынесем код чтения файла в отдельный метод:
public class Application {
public static void main(String[] args) {
// Переменная задается до try..catch чтобы к ней можно было обратиться из try и из catch
var path = "/path/to/file.txt";
try {
var content = readFile(path);
System.out.println(content);
} catch (IOException e) {
System.out.println("Проверьте что файл " + path + " существует и к нему есть доступ");
}
// Код который идет тут, будет выполнен
}
public static String readFile(String path) {
var preparedPath = Paths.get(path);
var content = new String(Files.readAllBytes(preparedPath));
return content;
}
}
Этот код не пройдет компиляцию, так как исключение IOException
является проверяемым (checked), но в методе Application.readFile()
нет его обработки, как того требуют проверяемые исключения. В этом месте возникает противоречие. Мы не хотим обрабатывать исключение, так как обработка будет где-то дальше, в нашем случае в методе Application.main()
. Java разрешает такие ситуации через указание того, что метод выбрасывает проверяемое исключение. Определение Application.readFile()
будет выглядеть так:
public static String readFile(String path) throws IOException {
// Тут содержимое
}
Теперь любой метод, который вызывает внутри себя метод Application.readFile()
должен сделать одно из двух:
- Указать через
throws
какие проверяемые исключения могут быть выброшены внутри него. - Обработать проверяемое исключение и тогда не придется использовать
throws
.
Выбор зависит от того, в каком месте мы хотим обрабатывать исключения.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.