2.x. exceptions
exceptions
Материалы
Когда программа нарушает семантические правила Java, виртуальная машина сигнализирует об этом через исключение. Исключение вызывает нелокальную передачу управления от точки возникновения к обработчику, который может её перехватить.
В отличие от возврата специальных значений (вроде -1 или null), исключения нельзя случайно проигнорировать — они требуют явной обработки или объявления.
Иерархия исключений
Все исключения в Java представлены объектами, унаследованными от класса Throwable:
Throwable
├── Error (unchecked)
│ ├── VirtualMachineError
│ │ ├── StackOverflowError
│ │ └── OutOfMemoryError
│ ├── LinkageError
│ │ ├── NoClassDefFoundError
│ │ └── UnsatisfiedLinkError
│ └── AssertionError
│
└── Exception (checked*)
├── IOException
│ ├── FileNotFoundException
│ └── EOFException
├── SQLException
├── ReflectiveOperationException
│ ├── ClassNotFoundException
│ └── NoSuchMethodException
│
└── RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
│ └── NumberFormatException
├── IllegalStateException
├── IndexOutOfBoundsException
│ ├── ArrayIndexOutOfBoundsException
│ └── StringIndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
└── UnsupportedOperationException
Checked vs Unchecked исключения
Java делит исключения на две категории:
Checked (проверяемые)
Checked исключения — это все исключения, кроме RuntimeException, Error и их подклассов. Компилятор требует их обработки или объявления в throws.
// FileNotFoundException — checked, компилятор требует обработки
public void readFile(String path) throws FileNotFoundException {
FileInputStream fis = new FileInputStream(path);
// ...
}
Checked исключения представляют восстановимые ситуации: файл не найден, сеть недоступна, неверный формат данных. Программа может и должна на них реагировать.
Unchecked (непроверяемые)
Unchecked исключения — это RuntimeException, Error и их подклассы. Компилятор не требует их обработки.
// NullPointerException — unchecked, обработка не обязательна
public int getLength(String s) {
return s.length(); // Может бросить NPE, но компилятор не требует try-catch
}
RuntimeException представляет ошибки программирования: обращение к null, выход за границы массива, деление на ноль. Такие ошибки должны исправляться в коде, а не обрабатываться в runtime.
Error представляет серьёзные проблемы JVM, от которых программа обычно не может восстановиться: нехватка памяти, переполнение стека, ошибки загрузки классов.
| Тип | Проверка компилятором | Примеры | Когда использовать |
|---|---|---|---|
| Checked | Да | IOException, SQLException | Восстановимые внешние ошибки |
| RuntimeException | Нет | NullPointerException, IllegalArgumentException | Ошибки программирования |
| Error | Нет | OutOfMemoryError, StackOverflowError | Критические проблемы JVM |
Бросание исключений
Исключение бросается оператором throw:
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Возраст не может быть отрицательным: " + age);
}
this.age = age;
}
Причины возникновения исключений
Исключение может быть брошено в четырёх случаях:
- Явный
throw— программист бросает исключение - Провал
assert— приassert false - Синхронная ошибка JVM — деление на ноль, выход за границы массива,
null-разыменование - Асинхронная ошибка JVM — критические проблемы вроде
OutOfMemoryError
// 1. Явный throw
throw new RuntimeException("Что-то пошло не так");
// 2. Провал assert
assert value > 0 : "Значение должно быть положительным";
// 3. Синхронные ошибки JVM
int result = 10 / 0; // ArithmeticException
String s = null; s.length(); // NullPointerException
int[] arr = new int[5]; arr[10] = 1; // ArrayIndexOutOfBoundsException
Объявление throws
Метод, который может бросить checked исключение, должен объявить его в сигнатуре:
public void readConfig(String path) throws IOException, ParseException {
// ...
}
Объявление throws — это контракт между методом и вызывающим кодом. Вызывающий код обязан либо обработать исключение, либо передать его дальше.
// Вариант 1: обработать
public void loadSettings() {
try {
readConfig("settings.json");
} catch (IOException e) {
useDefaultSettings();
} catch (ParseException e) {
throw new IllegalStateException("Некорректный файл конфигурации", e);
}
}
// Вариант 2: передать дальше
public void loadSettings() throws IOException, ParseException {
readConfig("settings.json");
}
Перехват исключений: try-catch-finally
Базовый try-catch
try {
riskyOperation();
} catch (SpecificException e) {
// Обработка конкретного исключения
handleError(e);
}
При возникновении исключения JVM ищет подходящий catch-блок, проверяя каждый по порядку. Блок подходит, если класс исключения — это указанный класс или его подкласс.
try {
readFile("data.txt");
} catch (FileNotFoundException e) {
System.out.println("Файл не найден: " + e.getMessage());
} catch (IOException e) {
System.out.println("Ошибка ввода-вывода: " + e.getMessage());
}
Важно: Более специфичные исключения должны идти раньше более общих.
catch (Exception e)передcatch (IOException e)вызовет ошибку компиляции.
Multi-catch (Java 7+)
Если обработка нескольких исключений одинакова, можно объединить их в один блок:
try {
processData();
} catch (IOException | ParseException | ValidationException e) {
logger.error("Ошибка обработки данных", e);
throw new ProcessingException("Не удалось обработать данные", e);
}
В multi-catch параметр e неявно final — его нельзя переприсвоить.
finally
Блок finally выполняется всегда — независимо от того, возникло исключение или нет:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
processStream(fis);
} catch (IOException e) {
handleError(e);
} finally {
// Выполнится в любом случае
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// Игнорируем ошибку закрытия
}
}
}
finally выполняется даже если:
- В
tryилиcatchестьreturn - В
tryилиcatchброшено исключение tryзавершился нормально
public int getValue() {
try {
return 1;
} finally {
System.out.println("finally выполнен"); // Выведется!
}
}
Предупреждение: Если
finallyбросает исключение или содержитreturn, это подавляет исходное исключение изtry/catch. Избегайте этого.
try {
throw new RuntimeException("Оригинальное исключение");
} finally {
throw new RuntimeException("Исключение из finally");
// Оригинальное исключение потеряно!
}
try-with-resources (Java 7+)
Для ресурсов, реализующих AutoCloseable, используйте try-with-resources — ресурс будет автоматически закрыт:
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
} catch (IOException e) {
handleError(e);
}
// fis и reader автоматически закрыты здесь
Ресурсы закрываются в обратном порядке объявления. Если close() бросает исключение, а в try уже было исключение, то исключение из close() добавляется как suppressed.
try (ProblematicResource r = new ProblematicResource()) {
throw new RuntimeException("Основное исключение");
}
// Если r.close() тоже бросит исключение, оно будет suppressed
// Получить suppressed исключения:
catch (RuntimeException e) {
Throwable[] suppressed = e.getSuppressed();
}
Класс Throwable
Все исключения наследуют от Throwable, который предоставляет:
public class Throwable {
// Сообщение об ошибке
String getMessage();
// Подробное сообщение (обычно совпадает с getMessage)
String getLocalizedMessage();
// Причина исключения (для chained exceptions)
Throwable getCause();
// Установить причину (можно вызвать только один раз)
Throwable initCause(Throwable cause);
// Стек вызовов
StackTraceElement[] getStackTrace();
// Печать стека вызовов
void printStackTrace();
void printStackTrace(PrintStream s);
void printStackTrace(PrintWriter s);
// Подавленные исключения (suppressed)
void addSuppressed(Throwable exception);
Throwable[] getSuppressed();
}
Сообщение об ошибке
Всегда передавайте информативное сообщение:
// Плохо
throw new IllegalArgumentException();
// Хорошо
throw new IllegalArgumentException(
"Порт должен быть в диапазоне 1-65535, получено: " + port
);
Stack trace
Stack trace показывает цепочку вызовов до точки исключения:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke method on null
at com.example.Service.process(Service.java:42)
at com.example.Controller.handle(Controller.java:28)
at com.example.Main.main(Main.java:15)
Читается снизу вверх: main вызвал handle, который вызвал process, где произошло исключение на строке 42.
Chained exceptions (цепочка исключений)
При преобразовании исключения сохраняйте оригинальную причину:
try {
lowLevelOperation();
} catch (SQLException e) {
// Сохраняем оригинальное исключение как причину
throw new ServiceException("Не удалось выполнить операцию", e);
}
Это позволяет видеть полную цепочку в stack trace:
ServiceException: Не удалось выполнить операцию
at com.example.Service.doWork(Service.java:50)
...
Caused by: java.sql.SQLException: Connection refused
at com.mysql.jdbc.Driver.connect(Driver.java:123)
...
Создание собственных исключений
Создавайте собственные исключения для domain-специфичных ошибок:
// Checked исключение
public class InsufficientFundsException extends Exception {
private final BigDecimal balance;
private final BigDecimal amount;
public InsufficientFundsException(BigDecimal balance, BigDecimal amount) {
super(String.format(
"Недостаточно средств: баланс %.2f, требуется %.2f",
balance, amount
));
this.balance = balance;
this.amount = amount;
}
public BigDecimal getBalance() { return balance; }
public BigDecimal getAmount() { return amount; }
}
// Unchecked исключение
public class ConfigurationException extends RuntimeException {
public ConfigurationException(String message) {
super(message);
}
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
Какой тип выбрать?
| Выбирайте | Когда |
|---|---|
Checked (extends Exception) | Вызывающий код может и должен обработать ошибку |
Unchecked (extends RuntimeException) | Ошибка программирования или неисправимая ситуация |
Примечание: Класс исключения не может быть generic (
class MyException<T> extends Exception— ошибка компиляции).
Переопределение методов и throws
При переопределении метода нельзя объявлять новые checked исключения или более широкие, чем в родительском методе:
class Parent {
void process() throws IOException { }
}
class Child extends Parent {
// OK: то же исключение
@Override
void process() throws IOException { }
}
class Child2 extends Parent {
// OK: более узкое исключение (подкласс)
@Override
void process() throws FileNotFoundException { }
}
class Child3 extends Parent {
// OK: вообще без исключений
@Override
void process() { }
}
class Child4 extends Parent {
// ОШИБКА: SQLException не объявлен в Parent
@Override
void process() throws SQLException { } // Не компилируется!
}
Это правило обеспечивает подстановку Лисков: код, работающий с Parent, ожидает только IOException.
Обработка исключений: лучшие практики
1. Не игнорируйте исключения
// ПЛОХО: исключение проглочено
try {
doSomething();
} catch (Exception e) {
// пусто
}
// Как минимум — логируйте
try {
doSomething();
} catch (Exception e) {
logger.error("Ошибка в doSomething", e);
}
2. Не ловите Exception или Throwable без необходимости
// ПЛОХО: ловит всё, включая NullPointerException
try {
processData();
} catch (Exception e) {
// Можем скрыть баги
}
// ЛУЧШЕ: ловим конкретные исключения
try {
processData();
} catch (IOException e) {
handleIOError(e);
} catch (ParseException e) {
handleParseError(e);
}
3. Используйте try-with-resources для ресурсов
// ПЛОХО: многословно и легко ошибиться
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// ...
} finally {
if (fis != null) {
try { fis.close(); } catch (IOException e) { }
}
}
// ХОРОШО
try (FileInputStream fis = new FileInputStream("file.txt")) {
// ...
}
4. Fail fast — бросайте исключения рано
public void processOrder(Order order) {
// Проверяем аргументы сразу
if (order == null) {
throw new NullPointerException("order не может быть null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Заказ не может быть пустым");
}
// Основная логика
// ...
}
5. Документируйте исключения
/**
* Переводит средства между счетами.
*
* @param from счёт-источник
* @param to счёт-получатель
* @param amount сумма перевода
* @throws InsufficientFundsException если на счёте-источнике недостаточно средств
* @throws AccountLockedException если один из счетов заблокирован
* @throws IllegalArgumentException если amount <= 0
*/
public void transfer(Account from, Account to, BigDecimal amount)
throws InsufficientFundsException, AccountLockedException {
// ...
}
6. Преобразуйте исключения на границах слоёв
// В слое репозитория
public User findById(Long id) throws UserNotFoundException {
try {
return jdbcTemplate.queryForObject(SQL, mapper, id);
} catch (EmptyResultDataAccessException e) {
throw new UserNotFoundException("Пользователь не найден: " + id, e);
}
// SQLException пробрасывается как DataAccessException (unchecked)
}
Пример: обработка ошибок в приложении
public class OrderService {
private final OrderRepository repository;
private final PaymentGateway paymentGateway;
public Order placeOrder(OrderRequest request) throws OrderException {
// Валидация (fail fast)
validateRequest(request);
Order order = createOrder(request);
try {
// Попытка оплаты
PaymentResult result = paymentGateway.charge(
request.getPaymentMethod(),
order.getTotal()
);
if (!result.isSuccessful()) {
throw new PaymentFailedException(result.getErrorMessage());
}
order.setPaymentId(result.getTransactionId());
order.setStatus(OrderStatus.PAID);
// Сохранение
return repository.save(order);
} catch (PaymentGatewayException e) {
// Преобразуем в domain-исключение
throw new OrderException("Ошибка при обработке платежа", e);
} catch (DataAccessException e) {
// Пытаемся отменить платёж
tryRefund(order);
throw new OrderException("Ошибка при сохранении заказа", e);
}
}
private void validateRequest(OrderRequest request) {
if (request == null) {
throw new IllegalArgumentException("request не может быть null");
}
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new IllegalArgumentException("Заказ должен содержать товары");
}
if (request.getPaymentMethod() == null) {
throw new IllegalArgumentException("Способ оплаты обязателен");
}
}
private void tryRefund(Order order) {
if (order.getPaymentId() != null) {
try {
paymentGateway.refund(order.getPaymentId());
} catch (Exception e) {
// Логируем, но не бросаем — основное исключение важнее
logger.error("Не удалось отменить платёж: {}", order.getPaymentId(), e);
}
}
}
}
Резюме
Система исключений Java обеспечивает надёжную обработку ошибок:
- Checked исключения требуют явной обработки и представляют восстановимые ошибки
- Unchecked исключения (
RuntimeException) представляют ошибки программирования - Error — критические проблемы JVM, которые обычно не обрабатываются
- Используйте try-with-resources для автоматического закрытия ресурсов
- Сохраняйте цепочку исключений через конструктор с
cause - Fail fast — бросайте исключения при первых признаках проблемы
- Не игнорируйте исключения — как минимум логируйте их
- Документируйте исключения в Javadoc