Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Раздел 10. Исключения, логирование, отладка

Глава 1. Иерархия исключений (Exceptions)

Throwable — корень всех ошибок: Error, Exception, RuntimeException

В основе механизма обработки ошибок в Java лежит класс Throwable, расположенный в пакете java.lang. Это единственный тип данных в Java, экземпляры которого могут быть выброшены с помощью ключевого слова throw и перехвачены в блоке catch. Любой другой класс, не наследующий Throwable напрямую или косвенно, не может участвовать в механизме исключений — компилятор отклонит такой код на этапе сборки.

Класс Throwable наследуется напрямую от Object и служит суперклассом для двух основных ветвей иерархии: Error и Exception. Это разделение отражает фундаментальное различие между двумя категориями проблем, с которыми может столкнуться приложение: критическими сбоями системы, от которых невозможно восстановиться, и исключительными ситуациями, которые программа может обработать и продолжить выполнение.

Структура иерархии и ключевые свойства
java.lang.Object
└── java.lang.Throwable (checked)
├── java.lang.Error (unchecked)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── NoClassDefFoundError
│ └── ...
└── java.lang.Exception (checked)
├── IOException
├── SQLException
└── java.lang.RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
├── IllegalStateException
└── ...


Каждый узел этой иерархии несет семантическую нагрузку. Throwable определяет базовый контракт для всех выбрасываемых объектов, включая методы для получения сообщения об ошибке (getMessage()), стектрейса (getStackTrace(), printStackTrace()) и причины исключения (getCause()). Конструкторы Throwable поддерживают цепочку причин (cause chaining), что позволяет сохранять контекст оригинальной ошибки при оборачивании исключений.

Важно понимать термин checked (проверяемое) и unchecked (непроверяемое) исключение.

Проверяемые исключения — это те, которые компилятор обязывает обрабатывать либо через try-catch, либо через объявление в сигнатуре метода (throws). К ним относятся все наследники Exception, кроме RuntimeException и его подклассов.
Непроверяемые исключения включают Error и всех наследников RuntimeException; компилятор не требует их явной обработки, так как они обычно сигнализируют о программных ошибках или критических сбоях инфраструктуры.


Класс Error: когда приложение бессильно

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

Рассмотрим ключевые представители этой ветви:
OutOfMemoryError возникает, когда JVM не может выделить память для объекта в heap-пространстве, а сборщик мусора не может освободить достаточно места. Это может произойти при утечках памяти, чрезмерном кешировании или обработке больших объемов данных. Перехват этого исключения бессмысленен — даже если код поймает OutOfMemoryError, JVM находится в нестабильном состоянии, и любая дальнейшая операция может привести к непредсказуемым последствиям.

StackOverflowError возникает при переполнении стека вызовов, обычно из-за бесконечной или слишком глубокой рекурсии. В отличие от OutOfMemoryError, здесь проблема локализована в конкретном потоке, но восстановление все равно невозможно без изменения кода — стек уже поврежден.

NoClassDefFoundError сигнализирует о том, что JVM не смогла найти класс, который был доступен во время компиляции. Это типичная проблема развертывания: отсутствующий JAR-файл, конфликт версий зависимостей или проблемы с classpath. Хотя технически это Error, в некоторых сценариях (например, плагинная архитектура) приложение может перехватить его для отключения конкретного модуля, но это скорее исключение из правила.

#Java #для_новичков #beginner #exception #Throwable
👍41
Практический пример демонстрирует бессмысленность обработки Error:
public class ErrorHandlingDemo {

public static void main(String[] args) {
try {
recursiveCall(0);
} catch (StackOverflowError e) {
// Технически возможно, но бесполезно
System.out.println("Stack overflow caught: " + e.getMessage());
// Попытка продолжить работу здесь крайне опасна
// JVM находится в нестабильном состоянии
}
}

private static void recursiveCall(int depth) {
// Каждый вызов добавляет фрейм в стек
recursiveCall(depth + 1);
}
}


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


Класс Exception: контролируемые сбои

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

Классические примеры включают IOException для операций ввода-вывода, SQLException для работы с базами данных, InterruptedException для управления потоками. Эти исключения сигнализируют о внешних проблемах: отсутствующий файл, недоступный сервер базы данных, запрос на прерывание потока. Приложение может реагировать на них осмысленно: повторить операцию, использовать альтернативный источник данных, уведомить пользователя.

Важная деталь реализации: Exception и его checked-подклассы предназначены для восстановимых ситуаций. Если исключение отражает программную ошибку (передача null вместо валидного аргумента, выход за границы массива), оно должно быть unchecked-наследником RuntimeException.


RuntimeException: программные ошибки

RuntimeException — специальная ветвь в иерархии Exception, которая помечает исключения как unchecked. Это означает, что компилятор не требует их явной обработки, но это не делает их менее важными. Напротив, RuntimeException обычно сигнализирует о нарушении контракта API или инвариантов программы.

NullPointerException — самое известное исключение Java, возникает при попытке разыменовать null-ссылку. В современной Java с внедрением Optional и улучшенными анализаторами кода (null analysis) частота этого исключения снижается, но оно остается индикатором недостаточной валидации входных данных.

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

IllegalStateException сигнализирует о вызове метода в неподходящем состоянии объекта. Например, попытка чтения из закрытого потока ввода или модификации коллекции во время итерации. Это исключение отличается от IllegalArgumentException тем, что проблема не в параметрах вызова, а в состоянии самого объекта.

ArithmeticException, ArrayIndexOutOfBoundsException, ClassCastException
— другие распространенные представители, каждый из которых указывает на конкретный вид программной ошибки.


#Java #для_новичков #beginner #exception #Throwable
👍4
Механизм JVM: как исключения работают под капотом

Когда в Java-коде возникает исключительная ситуация (через оператор throw или аппаратное прерывание, например, деление на ноль), JVM выполняет сложную последовательность действий.

Сначала JVM создает экземпляр соответствующего класса исключения, заполняя его стектрейс — массив объектов StackTraceElement, каждый из которых содержит имя класса, метода, имени файла и номера строки. Эта операция относительно дорогостоящая, так как требует обхода стека вызовов текущего потока.

Затем JVM начинает раскрутку стека (stack unwinding): последовательно проверяет каждый фрейм стека вызовов, начиная с текущего метода, на наличие подходящего обработчика в блоках catch. Поиск ведется по типу исключения с учетом иерархии — подходящим считается блок, объявляющий тип, равный классу исключения или его суперклассу.

Если подходящий обработчик найден, управление передается в соответствующий блок catch, а стектрейс сохраняется в объекте исключения для последующего анализа. Если обработчик не найден до дна стека вызовов, поток завершается, и JVM вызывает глобальный обработчик неперехваченных исключений (UncaughtExceptionHandler), который обычно выводит стектрейс в stderr.

Важный нюанс: для unchecked-исключений компилятор не генерирует проверки на этапе компиляции, но JVM обрабатывает их идентично checked-исключениям во время выполнения. Разница только в статической проверке кода.


Практические паттерны работы с иерархией

Правильное наследование при создании кастомных исключений

При проектировании собственных исключений критически важно выбрать правильного родителя. Это решение определяет семантику ошибки и обязательства вызывающего кода:
// Проверяемое исключение для восстановимых бизнес-ошибок
public class PaymentProcessingException extends Exception {
private final String transactionId;
private final PaymentErrorCode errorCode;

public PaymentProcessingException(String message, String transactionId,
PaymentErrorCode errorCode, Throwable cause) {
super(message, cause);
this.transactionId = transactionId;
this.errorCode = errorCode;
}

// Геттеры для структурированной информации об ошибке
public String getTransactionId() { return transactionId; }
public PaymentErrorCode getErrorCode() { return errorCode; }
}

// Непроверяемое исключение для программных ошибок валидации
public class InvalidPaymentRequestException extends RuntimeException {
private final String fieldName;
private final Object rejectedValue;

public InvalidPaymentRequestException(String fieldName, Object rejectedValue, String message) {
super(message);
this.fieldName = fieldName;
this.rejectedValue = rejectedValue;
}
}


PaymentProcessingException наследует Exception, делая исключение checked, потому что проблемы с платежами (недоступность процессинга, таймауты) — это внешние, восстановимые сбои. Вызывающий код обязан явно обработать эту ситуацию или делегировать дальше.

InvalidPaymentRequestException наследует RuntimeException, так как передача невалидных данных — это ошибка программиста, вызывающего API. Проверка аргументов должна происходить до вызова, и исключение служит последней линией защиты, не требуя загромождения сигнатур методов.


#Java #для_новичков #beginner #exception #Throwable
👍3
Антипаттерн: перехват Throwable

Перехват Throwable на верхнем уровне метода — серьезная ошибка, которая маскирует критические сбои:
// Антипаттерн — никогда так не делайте
public void processRequest(Request request) {
try {
executeBusinessLogic(request);
} catch (Throwable t) { // Опасно: перехватывает Error
logger.error("Request failed", t);
// Если это был OutOfMemoryError, приложение продолжит работу
// с нестабильной JVM, что приведет к непредсказуемым последствиям
}
}


Правильный подход — перехватывать конкретные исключения, с которыми код умеет работать, и позволять Error распространяться для аварийного завершения JVM.

Использование цепочки причин

При оборачивании низкоуровневых исключений в высокоуровневые бизнес-исключения всегда сохраняйте оригинальную причину:
public User loadUserById(String userId) throws UserRepositoryException {
try {
return database.executeQuery("SELECT * FROM users WHERE id = ?", userId);
} catch (SQLException e) {
// Сохраняем оригинальное исключение как cause
throw new UserRepositoryException(
"Failed to load user with ID: " + userId,
e // Цепочка причин сохраняется
);
}
}



Это позволяет при анализе логов проследить полный путь ошибки от бизнес-операции до конкретной проблемы с базой данных (например, разорванное соединение или deadlock).


Современный контекст: эволюция подходов

Современная экосистема Java претерпела значительные изменения в отношении исключений. Если в классической Java (до Java 8) checked-исключения считались нормой для любых внешних сбоев, то современные фреймворки (Spring, Hibernate, JPA) и языки, работающие на JVM (Kotlin, Scala), склоняются к использованию unchecked-исключений даже для восстановимых ситуаций.

Spring Framework, например, транслирует большинство checked-исключений (таких как SQLException или IOException из репозиториев) в иерархию unchecked DataAccessException. Это упрощает сигнатуры методов и интеграцию с лямбда-выражениями, которые не поддерживают checked-исключения в своих функциональных интерфейсах.

Kotlin полностью отказался от checked-исключений на уровне языка, считая, что механизм не оправдывает своих затрат на verboseness. Это влияет на дизайн API, совместимых с Kotlin — они также предпочитают unchecked-исключения.

Тем не менее, понимание иерархии Throwable остается фундаментальным. Даже в функциональном стиле с использованием Optional, Result-типов из библиотек (Vavr, Arrow) или sealed-классов (Java 17+), исключения не исчезают — они трансформируются в другие формы представления ошибок, но базовая модель JVM с Throwable в корне остается неизменной.

#Java #для_новичков #beginner #exception #Throwable
👍4