Java try...catch
Обрабатывайте ошибки времени выполнения в Java с помощью блоков try...catch, чтобы программа продолжала работу при сбоях.
try/catch — наименьшая единица обработки исключений в Java. Вы оборачиваете рискованный код в try, а затем добавляете один или несколько блоков catch, указывая, что делать при возникновении конкретного типа исключения. Вместе они образуют единую конструкцию: нельзя использовать одно без другого (или без finally, который мы рассмотрим в следующей главе).
Синтаксис
try {
// code that might throw
} catch (ExceptionType e) {
// code that runs only if a matching exception was thrown
}Несколько вещей, которые легко упустить поначалу:
- Переменная в
catch(eпо соглашению) ограничена областью видимости блока catch. Вы можете назвать её как угодно;eиex— наиболее распространённые варианты. - Параметр catch фактически является финальным — вы можете его читать, но не следует переприсваивать. Обращение с ним как с иммутабельным делает код понятнее.
- Блок
tryимеет собственную область видимости. Переменные, объявленные внутри него, не видны вcatchили после конструкции. Если вам нужно значение, вычисленное вtry, объявите переменную снаружи.
String content;
try {
content = Files.readString(path);
} catch (IOException e) {
content = ""; // works because `content` was declared outside the try
}
System.out.println(content);Какие catch срабатывают
Блок catch (T e) выполняется, когда брошенное исключение является экземпляром T — включая любой подтип T. Таким образом:
catch (Exception e)перехватывает почти всё:IOException,NullPointerException, ваши собственные пользовательские исключения.catch (RuntimeException e)перехватывает ошибки времени выполнения, но не проверяемые исключения вродеIOException.catch (NullPointerException e)перехватывает только NPE (и подклассы, которых обычно нет).
Внутри try выполняется только первый подходящий catch. Если вы указываете более широкий тип перед более узким, узкий catch становится недостижимым, и компилятор выдаст ошибку:
try { ... }
catch (Exception e) { ... } // matches everything
catch (IOException e) { ... } // ERROR: unreachableВсегда перечисляйте catch от наиболее специфичного к наиболее общему.
Что делать в catch
Блок catch — это не место, где исключения просто исчезают. Это место, где нужно решить, что делать при известном сбое. Реалистичные варианты:
- Восстановление — повторная попытка, откат к значению по умолчанию, переключение на другой ресурс. Это наилучший сценарий, и он встречается реже, чем кажется.
- Журналирование и повторное бросание — запишите детали и позвольте исключению распространиться дальше к обработчику верхнего уровня, который знает, что делать.
- Оборачивание и повторное бросание — преобразуйте низкоуровневое исключение в то, что соответствует словарю данного слоя (
IOException→ConfigLoadException). - Преобразование в возвращаемое значение — когда сбой действительно ожидаем (например, разбор пользовательского ввода), верните
Optional.empty()или сигнальное значение.
Чего не следует делать — перехватывать исключение и молча продолжать работу без журналирования или реакции. Именно так ошибки исчезают в никуда. Мы вернёмся к этому в главе о лучших практиках.
Чтение информации об исключении
Параметр catch — это объект. Три метода, которые вы будете использовать постоянно:
e.getMessage()— человекочитаемое описание. Может бытьnull.e.toString()— имя класса плюс сообщение, напримерjava.io.IOException: file not found.e.printStackTrace()— выводит трассировку стека вSystem.err. Удобно при отладке, но в рабочем коде используйте настоящий логгер (он направляет трассировку через тот же канал, что и всё остальное).
Четвёртый метод, e.getCause(), возвращает исходное исключение, если одно из них было обёрнуто — полезно, когда нужно понять первопричину сбоя внутри слоя преобразования.
Перехват Exception и Throwable
Можно написать catch (Throwable t), и это перехватит всё — включая Error-ы вроде OutOfMemoryError. Не делайте этого. Error-ы означают, что JVM находится в затруднительном положении, и ваш код не в состоянии адекватно на них реагировать. Перехват Throwable маскирует ошибки, которые должны завершать процесс.
Используйте Exception или один из его подтипов. Если вам действительно нужно залогировать неожиданное исключение перед тем, как оно прервёт выполнение, правильная форма: catch (RuntimeException e) { log; throw; }.
Проработанный пример
Небольшая утилита, которая разбирает целое число из каждой строки входных данных. Некоторые строки содержат допустимые числа, некоторые — нет, одна равна null. Мы используем try/catch, чтобы продолжать работу после плохих строк и подсчитывать результаты. Вызов line.trim() бросает NullPointerException на записи null, а Integer.parseInt бросает NumberFormatException на всё, что не является целым числом — два отдельных типа исключений, каждый со своим catch.
Вывод программы:
not an integer: "hello"
null line — skipped
not an integer: "3.14"
---
parsed: 3, failed: 3, sum: 118
Обратите внимание на два момента. Во-первых, программа завершается нормально. Без блоков catch первая плохая строка прервала бы выполнение, и всё последующее не было бы учтено. Во-вторых, два типа сбоев различаются: строка null попадает в ветку NullPointerException с соответствующим сообщением, а "hello" и "3.14" — в ветку NumberFormatException. Выбор catch по типу не даёт разным сбоям смешиваться.
Что дальше
Один catch — простейший случай. В реальном коде обычно нужно обрабатывать несколько разных сбоев одновременно. Перейдите к нескольким блокам catch и multi-catch в Java. После этого блок finally и try-with-resources завершают синтаксис, а throw и throws охватывают другую сторону: сигнализацию об ошибках из собственного кода.