Автор статьи: Сергей Прощаев (@sproshchaev)
Руководитель направления Java‑разработки в FinTech 

Обработка исключений — одна из фундаментальных тем в Java, с которой сталкивается каждый разработчик. Правильная работа с ошибками не только делает приложение стабильным, но и значительно упрощает его отладку и поддержку. В отличие от многих других языков, Java имеет строгую и продуманную систему исключений, которая делит все ошибки на проверяемые (checked) и непроверяемые (unchecked). 

В этой статье мы подробно рассмотрим иерархию исключений, их типы, лучшие практики обработки, а также распространенные антипаттерны. Чтобы статья была практико‑ориентированной, мы разберем реальные примеры кода, которые покажут, как правильно выбрасывать, обрабатывать и создавать собственные исключения.

Что такое исключение и зачем оно нужно?

Исключение (Exception) в Java — это событие, которое возникает во время выполнения программы и нарушает нормальный ход ее инструкций. Когда происходит ошибка, JVM создает объект исключения, содержащий информацию о ошибке (тип, сообщение, стек вызовов). Этот объект затем «перебрасывается» (thrown) в метод, где произошла ошибка. 

Основные цели механизма исключений: 

  • Отделение нормальной логики от обработки ошибок. Код становится чище и легче для чтения. 

  • Пропагация ошибок вверх по стеку вызовов. Ошибка может быть обработана на том уровне, где для этого есть достаточно контекста. 

  • Группировка и классификация ошибок. Разные типы ошибок можно обрабатывать по‑разному. 

В основе иерархии исключений лежит класс Throwable. Его два главных потомка — Error и Exception. 

Error: Критические ошибки, которые приложение не должно пытаться обрабатывать (например, OutOfMemoryError). 

Exception: Собственно исключения, с которыми работает разработчик. Checked vs Unchecked исключения: в чем разница? 

Класс Exception, в свою очередь, делится на два ключевых подвида, которые определяют поведение компилятора. 

Checked исключения (проверяемые) 

Это исключения, которые проверяются на этапе компиляции. Если метод может выбросить проверяемое исключение, он должен либо обработать его с помощью try‑catch, либо объявить в сигнатуре с помощью ключевого слова throws. 

Примеры: IOException, SQLException, ClassNotFoundException.

// Пример: Чтение файла может выбросить FileNotFoundException (checked)
public String readFile(String filename) throws FileNotFoundException {
    File file = new File(filename);
    Scanner scanner = new Scanner(file); // Может выбросить FileNotFoundException
    return scanner.nextLine();
}

// Или обработка на месте:
public String readFileHandled(String filename) {
    try {
        File file = new File(filename);
        Scanner scanner = new Scanner(file);
        return scanner.nextLine();
    } catch (FileNotFoundException e) {
        System.err.println("Файл не найден: " + e.getMessage());
        return null;
    }
}

Unchecked исключения (непроверяемые) 

Это исключения, которые НЕ проверяются компилятором. Они являются подклассами RuntimeException. Разработчик не обязан их объявлять или обрабатывать, хотя может это делать. 

Примеры: NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException.

// Пример: Unchecked-исключение. Код скомпилируется, но упадет при выполнении, если arg = null.
public void processString(String arg) {
    if (arg == null) {
        // Не обязаны объявлять throws, но можем выбросить
        throw new IllegalArgumentException("Аргумент не может быть null");
    }
    System.out.println(arg.length());
}

Сравнение Checked и Unchecked

Далее мы рассмотрим эти два типа и обсудим, когда какой использовать.

Обязательность обработки

Главное различие — в обязательствах, накладываемых на разработчика. Checked‑исключения заставляют явно прописывать стратегию обработки ошибки: «либо обработай, либо объяви, что она может произойти». Это делает код более надежным, но иногда приводит к его «засорению» блоками try‑catch и объявлениями throws

Unchecked‑исключения дают больше свободы, но требуют большей дисциплины. Разработчик сам решает, когда и где обрабатывать ошибку, что может привести к неожиданным падениям программы, если не быть внимательным.

Влияние на дизайн приложения

Использование checked‑исключений может сильно влиять на архитектуру, особенно на сигнатуры методов и интерфейсов. Если вы добавляете в метод новое checked‑исключение, вам придется менять его сигнатуру и весь код, который его вызывает. 

Unchecked‑исключения более гибкие в этом плане. Их можно добавить, не ломая существующие контракты методов.

Производительность

Создание объекта исключения и «раскрутка» стека (stack unwinding) — операции дорогостоящие. В критичных к производительности участках кода не следует использовать исключения для управления обычным потоком выполнения (это антипаттерн). Однако для большинства приложений разница в производительности между checked и unchecked незначительна; главное — не злоупотреблять ими в циклах.

Преимущества и недостатки каждого подхода

Checked исключения: сила принуждения

Основное преимущество checked‑исключений — это безопасность на этапе компиляции. Компилятор не даст вам забыть обработать потенциальную ошибку, такую как отсутствие файла или разрыв сетевого соединения. Это особенно ценно в корпоративных приложениях, где надежность и предсказуемость критически важны. 

Опыт сообщества показывает, что checked‑исключения хороши для восстанавливаемых ошибок, когда есть четкий сценарий действий при их возникновении (например, попросить пользователя указать другой файл).

Checked исключения: понимание недостатков

Главный недостаток — нарушение инкапсуляции и избыточность кода. Методы низкого уровня (например, работа с файловой системой) вынуждены «протаскивать» через всю цепочку вызовов исключения, которые часто не могут обработать осмысленно. Это приводит к появлению длинных списков throws в сигнатурах методов или, что хуже, к «глухим» блокам catch, которые подавляют исключения.

// Антипаттерн: подавление исключения
try {
    // ... код, который может упасть
} catch (SomeCheckedException e) {
    // Пустой блок catch - исключение проигнорировано
}

Unchecked исключения: гибкость и чистота кода

Unchecked‑исключения не загромождают сигнатуры методов и не заставляют обрабатывать ошибки там, где это не нужно. Это делает код чище и более читаемым. Их основная сфера применения — ошибки программиста (bugs) или критические системные сбои, которые невозможно обработать на месте. 

Примеры: передача недопустимого аргумента в метод (IllegalArgumentException), обращение к объекту по null ссылке (NullPointerException), выход за границы массива.

Unchecked исключения: понимание недостатков

Отсутствие контроля со стороны компилятора — это палка о двух концах. Разработчик может забыть обработать потенциально опасную ситуацию, что приведет к внезапному падению приложения. Требуется высокая дисциплина и хорошее тестовое покрытие, чтобы выявить все возможные сценарии с исключениями.

Практические примеры обработки и создания исключений

Базовые конструкции: try‑catch‑finally

Основной механизм обработки — блок try‑catch.

public class BasicExceptionHandling {
    public static void main(String[] args) {
        try {
            // Код, который может вызвать исключение
            int result = 10 / 0; // Выбросит ArithmeticException
            System.out.println("Результат: " + result);
        } catch (ArithmeticException e) {
            // Обработка конкретного исключения
            System.err.println("Ошибка: Деление на ноль!");
        } catch (Exception e) {
            // Более общий обработчик (должен быть последним)
            System.err.println("Произошла неизвестная ошибка: " + e.getMessage());
        } finally {
            // Этот блок выполняется ВСЕГДА
            System.out.println("Блок finally выполнен.");
            // Часто используется для освобождения ресурсов
        }
    }
}

Try‑with‑resources: современный подход

Начиная с Java 7, для работы с ресурсами (такими как потоки ввода‑вывода или соединения с БД) рекомендуется использовать try‑with‑resources. Это гарантирует автоматическое закрытие ресурсов, даже если возникло исключение.

import java.io.*;

public class TryWithResourcesExample {
    // До Java 7: громоздкий код с finally
    public static void oldWay(String filePath) throws IOException {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));
            String line = br.readLine();
            System.out.println(line);
        } finally {
            if (br != null) {
                br.close(); // Может тоже выбросить IOException!
            }
        }
    }

    // С Java 7: элегантно и безопасно
    public static void newWay(String filePath) {
        // BufferedReader реализует интерфейс AutoCloseable
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
            String line = br.readLine();
            System.out.println(line);
        } catch (FileNotFoundException e) {
            System.err.println("Файл не найден: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("Ошибка чтения файла: " + e.getMessage());
        }
        // br.close() вызывается автоматически здесь
    }
}

Создание собственных исключений

Вы можете создавать свои классы исключений для отражения специфичных для вашего домена ошибок.

Checked‑исключение для бизнес‑логики:

public class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

// Unchecked-исключение для ошибок валидации  
public class InvalidAccountException extends RuntimeException {
    public InvalidAccountException(String message) {
        super(message);
    }
}

Использование в сервисе:

public class BankService {
    public void withdraw(double amount, String accountId) throws InsufficientFundsException {
        if (accountId == null || accountId.isBlank()) {
            throw new InvalidAccountException("ID счета не может быть пустым");
        }

        double balance = getBalance(accountId);
        if (balance < amount) {
            throw new InsufficientFundsException("Недостаточно средств");
        }
        // логика списания
    }

    private double getBalance(String accountId) {
        return 100.0;
    }
}

Рекомендации и лучшие практики 

  1. Будьте конкретны в catch‑блоках. Ловите наиболее специфичные исключения первыми, а Exception — в самом конце. 

  2. Не игнорируйте исключения. Пустой блок catch — это почти всегда плохая идея. Как минимум, логируйте ошибку. 

  3. Используйте исключения по назначению. Не используйте их для управления нормальным потоком выполнения (например, для выхода из цикла).

  4. Документируйте исключения. Используйте JavaDoc (@throws), чтобы показать, какие исключения может бросать ваш метод, особенно unchecked. 

  5. Сохраняйте цепочку исключений. При перехвате и выбрасывании нового исключения передавайте оригинальное исключение как причину (cause).

try {
    // ... какой-то код
} catch (IOException e) {
    // Сохраняем оригинальное исключение как причину
    throw new MyApplicationException("Не удалось выполнить операцию", e);
}

6. Предпочитайте unchecked‑исключения для ошибок программиста. Если ошибку можно избежать проверкой кода (например, проверка аргументов на null), используйте IllegalArgumentException или другие RuntimeException.

Заключение

Понимание системы исключений в Java — это не просто знание синтаксиса try‑catch. Это вопрос архитектурного выбора. Checked‑исключения обеспечивают безопасность на этапе компиляции, но могут привести к раздуванию кода. Unchecked‑исключения делают код чище и гибче, но требуют от разработчика большей дисциплины и внимания к тестированию. 

Современные тенденции в разработке, особенно в фреймворках типа Spring, склоняются к преимущественному использованию unchecked‑исключений. Однако окончательный выбор всегда зависит от конкретного контекста и требований вашего приложения. Главное — подходить к обработке ошибок осознанно, делая ваш код не только работающим, но и устойчивым к сбоям.


Курс Java Developer. Basic последовательно проходит через синтаксис и ООП, архитектурные основы, тестирование, базы данных, алгоритмы и базовую инфраструктуру (docker, сети и т.п.), чтобы к первым собеседованиям вы подходили уже как начинающий разработчик. Еще можно присоединиться к ноябрьскому потоку.

Комментарии (0)