Язык программирования Java, как и множество других языков, имеет интегрированные инструменты работы с ошибками — исключительными ситуациями (исключениями), при которых сбой в работе программы обрабатывается специальным кодом, отличным от базового алгоритма.
Благодаря исключениям программист может заранее предвидеть слабые места кодовой базы и предвосхитить возникновение фатальных ошибок в тот момент, когда программа уже выполняется.
Поэтому обработка исключений в Java — хорошая практика, повышающая общую надежность кода.
Цель этой публикации — рассмотреть принципы перехвата и обработки исключений, а также разобрать соответствующие синтаксические конструкции языка, предназначенные для этого.
Все примеры из этого руководства запускались в операционной системе Ubuntu 22.04, установленной на облачном сервере Timeweb Cloud.
В блоге Timeweb Cloud есть отдельная статья, подробно описывающая установку Java в Ubuntu 22.04, но мы также кратко опишем этот процесс ниже.
Примеры, показанные в этом руководстве, запускались с помощью OpenJDK. Его установка не представляет особой сложности.
Сперва необходимо обновить список доступных репозиториев:
sudo apt update
После этого необходимо запросить список доступных для загрузки версий OpenJDK:
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
В консоли появится небольшой список:
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
Как можно заметить, наиболее свежая версия, доступная для загрузки, — openjdk-21-jdk
. В этом руководстве используется именно она:
sudo apt install openjdk-21-jdk
После этого можно проверить корректность установки Java, запросив ее версию:
java --version
Консольный вывод будет примерно таким:
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
Как видно, точная версия OpenJDK — 21.0.5.
Все показанные в этом руководстве примеры необходимо сохранять в отдельном файле с расширением .java
:
sudo nano App.java
После чего созданный файл наполняется кодом примера. Например, таким:
class App {
public static void main(String[] args) {
System.out.println("Этот текст выводится в консоль");
}
}
Обратите внимание, что имя класса должно совпадать с именем файла.
Далее файл с кодом компилируется:
javac App.java
И запускается:
java App
В консольном терминале появляется соответствующий вывод:
Этот текст выводится в консоль
Все исключения в языке Java имеют определенный тип, ассоциированный с причиной возникновения исключения — конкретным видом сбоя в работе программы.
Всего существует два базовых типа исключений:
Checked Exceptions. Такие исключения возникают на этапе компиляции программы. Если их не обработать, программа не скомпилируется.
Unchecked Exceptions. Такие исключения возникают на этапе выполнения программы. Если их не обработать, программа прекратит свое выполнение.
Тип Errors можно считать исключением лишь условно — это полноценная ошибка, приводящая к неминуемому прекращению работы программы.
Исключения, которые можно обработать с помощью пользовательского кода и продолжить выполнение программы, — Checked Exceptions и Unchecked Exceptions.
Таким образом, ошибки и исключения в Java являются разными сущностями. Однако и то (Errors), и другое (Checked Exceptions и Unchecked Exceptions), будучи типами, имеет дополнительные подтипы, уточняющие причину сбоя программы.
cloud
Рассмотрим пример кода, который вызывает исключение сразу на этапе компиляции программы:
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // создание указателя на файл
Scanner scanner = new Scanner(someFile); // парсинг содержимого файла
}
}
Компиляция прервется, а в консольном терминале появится сообщение об исключении FileNotFoundException
:
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
Если перехватить и обработать это исключение, то код скомпилируется и станет доступен для запуска.
Рассмотрим еще один пример кода, который вызывает исключение только на этапе выполнения программы:
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // создание массива из 5 элементов
System.out.println(someArray[10]); // попытка доступа к несуществующему элементу
}
}
Сразу в момент компиляции исключения не возникнут, однако после запуска скомпилированного кода в консольном терминале появится информация об исключении ArrayIndexOutOfBoundsException
:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
Это означает, что подобное исключение возможно обработать с помощью пользовательского кода, продолжив выполнение программы.
Рассмотрим, наконец, пример кода, который вызывает ошибку во время выполнения программы:
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // вызываем переполнение стека
}
}
Компиляция пройдет успешно, однако во время выполнения программы в консольном терминале появится сообщение об ошибке StackOverflowError
:
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
В данном случае ошибку никак нельзя обработать — ее можно только исправить.
Во внутренней реализации языка Java все исключения (и ошибки) представлены в виде набора классов, некоторые из которых наследуют свойства друг друга.
Базовым для всех ошибок и исключений является класс Throwable. От этого класса наследуются два других — Error и Exception, которые являются базовыми для широкого набора других классов, ассоциированных с конкретными типами исключений.
Класс Error описывает исключения типа Error, упомянутых в предыдущем разделе, а класс Exception — типа Checked Exceptions.
В свою очередь, от класса Exception наследуется класс RuntimeException, который описывает исключения типа Unchecked Exceptions.
Сокращенная схема иерархии классов исключений (источник: Java Training School)
В более наглядном виде неполную иерархию классов исключений в Java можно выразить в виде следующего вложенного списка:
Throwable
Error
Exception
CloneNotSupportedException
InterruptedException
ReflectiveOperationException
ClassNotFoundException
IllegalAccessException
InstantiationException
NoSuchFieldException
NoSuchMethodException
RuntimeException
NullPointerException
ArithmeticException
IllegalArgumentException
IndexOutOfBoundException
NumberFormatException
Соответственно, в каждом классе исключения есть методы для получения дополнительной информации о возникшем сбое.
Полную классификацию исключения Java с учетом дополнительных пакетов можно найти в отдельном справочнике.
Любые исключения выполняются с помощью специальных блоков try
и catch
, которые являются стандартными для большинства языков программирования, в том числе и Java.
Внутри блока try
пишется код с потенциальной ошибкой, которая может вызвать исключение.
Внутри блока catch
пишется код, который обрабатывает исключение, возникшее в коде ранее указанного блока try
.
Например, конструкция try-catch
может выглядеть так:
public class App {
public static void main(String[] args) {
try {
// тут пишется код, способный вызвать исключение
int someVariable = 5 / 0;
System.out.println("А кто сказал, что на ноль делить нельзя?");
} catch (ArithmeticException someExeption) {
// тут пишется код, способный обработать исключение
System.out.println("Вообще-то на ноль делить нельзя...");
}
}
}
Результатом работы этого кода станет следующий консольный вывод:
Вообще-то на ноль делить нельзя...
Конкретно этот пример основан на запрещенной операции деления на ноль, обернутой в блок try
, которая вызывает исключения типа ArithmeticException
.
Соответственно, в блоке catch
выполняется обработка этого исключения, а именно вывод сообщения об ошибке в консольный терминал.
Благодаря такой конструкции программа сможет продолжить выполнение работы после возникновения ошибки во время деления на ноль.
В отличие от многих других языков программирования в Java есть специальный блок finally
, относящийся к механизму обработки исключений, который выполняется всегда — вне зависимости от того, было исключение или нет.
Поэтому показанную ранее конструкцию можно дополнить:
public class App {
public static void main(String[] args) {
try {
// тут пишется код, способный вызвать исключение
int someVariable = 5 / 0;
} catch (ArithmeticException someExeption) {
// тут пишется код, который обрабатывать исключение
System.out.println("Вообще-то на ноль делить нельзя...");
} finally {
// тут пишется код, которые выполняется всегда
System.out.println("Какая разница, делится ли число на ноль или нет? Этот текст все равно появится!");
}
}
}
После запуска этого кода в консольном терминале появится такой вывод:
Вообще-то на ноль делить нельзя...
Какая разница, делится ли число на ноль или нет? Этот текст все равно появится!
Чтобы понять практическую необходимость блока finally
, можно рассмотреть следующий пример кода вне какого-либо контекста:
try {
parseJson(response.json);
} catch (JSONException someExeption) {
System.out.println("Похоже с JSON что-то не так...");
}
// некая функция, скрывающая индикатор загрузки
hideLoaderUI();
В программе, содержащей такую конструкцию, функция hideLoaderUI()
никогда не выполнится, если возникло какое-либо исключение.
В этом случае можно попробовать вызвать функцию hideLoaderUI()
в обработчике исключения в случае, если оно возникло, а также после обработчика, если исключения не было:
try {
parseJson(response.json);
} catch (JSONException someExeption) {
hideLoaderUI(); // дубль
System.out.println("Похоже с JSON что-то не так...");
}
hideLoaderUI(); // дубль
Однако в этом случае возникает нежелательное дублирование вызовы функции. К тому же, вместо функции может быть полноценный кусок кода, дублирование которого — плохая практика.
Поэтому гарантировать выполнение функции hideLoaderUI()
без дублирования ее вызова можно с помощью блока finally
:
try {
parseJson(response.json);
} catch (JSONException someExeption) {
System.out.println("Похоже с JSON что-то не так...");
} finally {
// индикатор загрузки скроется в любом случае
hideLoaderUI();
}
Язык Java позволяет вручную создавать (выбрасывать) исключения через специальный оператор throw
:
public class App {
public static void main(String[] args) {
throw new Exception("Кажется случилось что-то странное...");
}
}
Можно даже создать переменную с исключением заранее, а уже потом осуществить ее выброс:
public class App {
public static void main(String[] args) {
var someException = new Exception("Кажется случилось что-то странное...");
throw someException;
}
}
Еще одно ключевое слово throws
(обратите внимание на букву «s» в конце) позволяет явно указывать типы исключений (в виде перечисления их классов), которые может выбрасывать объявляемый метод.
Если в таком методе возникнет исключение, оно поднимется наверх в вызывающий код, который должен будет его обработать:
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someExeption) {
System.out.println("Опять делим на ноль? А ты знаешь, что такое безумие?");
}
}
}
В консольном терминале появится указанное сообщение:
Опять делим на ноль? А ты знаешь, что такое безумие?
Иерархическая структура исключений закономерно разрешает создание пользовательских классов исключений, которые сами по себе наследуются от базовых.
Благодаря пользовательским исключениям язык разрешает реализовывать уникальные для конкретной программы пути обработки сбоев.
Таким образом, к уже стандартным исключениям в Java можно добавить собственные.
Каждое пользовательское исключение, впрочем как и любое предопределенное, возможно обработать через стандартные блоки try-catch-finally
:
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // запуск конструктора родительского класса
System.out.println("Внимание! Сейчас вывалится исключение!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Просто исключение. Без объяснения причин. Кто-то против?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
В консольном терминале появится следующий вывод:
Внимание! Щас вывалится исключение!
Просто исключение. Без объяснения причин. Кто-то против?
Разворачивайте Java-проекты в облаке
В этом руководстве было показано на примерах, зачем нужны исключения в Java, как они возникают (в частности, как выбросить исключение вручную) и как их обработать с помощью соответствующих инструментов языка.
Исключения, доступные для перехвата и обработки, бывают двух типов:
Checked Exceptions. Обрабатываются, когда код компилируется.
Unchecked Exceptions. Обрабатываются, когда код выполняется.
В дополнение к ним бывают фатальные ошибки, разрешить которые можно только переписывая код:
Errors. Обработка невозможна.
При этом существует несколько синтаксических конструкций (в виде блоков) для обработки исключений:
try. Код, в котором возможно исключение.
catch. Код, который обрабатывает возможное исключение.
finally. Код, который выполняется вне зависимости от наличия исключения.
А также ключевые слова для управления процессом выброса исключений:
throw. Вручную выбрасывает исключение.
throws. Перечисляет возможные исключения внутри объявленного метода.
Полный список методов родительского класса Exception можно посмотреть в официальной документации Oracle.