Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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;
}

Причины возникновения исключений

Исключение может быть брошено в четырёх случаях:

  1. Явный throw — программист бросает исключение
  2. Провал assert — при assert false
  3. Синхронная ошибка JVM — деление на ноль, выход за границы массива, null-разыменование
  4. Асинхронная ошибка 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