Раздел 9. Исключения, логирование, отладка
Глава 1. Иерархия исключений (Exceptions)
Создание своих исключений: наследование от Exception или RuntimeException. Когда кастомное исключение оправдано
Стандартные исключения Java — IOException, IllegalArgumentException, NullPointerException — описывают универсальные классы ошибок. Однако когда приложение вырастает за пределы учебных примеров, универсальные типы перестают адекватно передавать семантику сбоев. IOException не объясняет, что именно пошло не так в бизнес-процессе обработки платежа. IllegalArgumentException не указывает, какое поле запроса нарушило правило валидации.
Кастомное исключение — это класс, расширяющий Exception или RuntimeException, который несет доменную семантику ошибки. Его название становится частью языка предметной области (ubiquitous language в терминологии Domain-Driven Design), позволяя коду самодокументироваться и облегчая обработку ошибок через специфичные catch-блоки.
Однако создание кастомных исключений — это не бесплатная операция. Каждый новый класс исключения увеличивает сложность системы, требует поддержки и тестирования. Поэтому первый вопрос, который должен задать разработчик: действительно ли стандартное исключение не передает нужную семантику?
Когда кастомное исключение оправдано
Создание собственного класса исключения оправдано в следующих сценариях:
— Доменная специфика ошибки. Когда ошибка является неотъемлемой частью бизнес-логики и требует специфической обработки. InsufficientFundsException в банковской системе или SeatUnavailableException в системе бронирования — это не технические сбои, а валидные бизнес-сценарии, которые код должен обрабатыать осмысленно.
— Необходимость дополнительного контекста. Когда стандартное исключение не позволяет передать структурированную информацию об ошибке. Кастомный класс может содержать поля для error code, идентификатора сущности, деталей валидации, которые необходимы для формирования корректного ответа API или сообщения пользователю.
— Разделение обработки в catch-блоках. Когда разные типы ошибок требуют разной логики восстановления. Иерархия кастомных исключений позволяет использовать полиморфизм в обработке: перехватить общий тип для единообразной обработки, или специфичный для особой логики.
— API для внешнего использования. Когда код предназначен для использования как библиотека или сервис, и нужно предоставить четкий контракт возможных сбоев без экспонирования внутренних деталей реализации.
Обратные сценарии, когда кастомное исключение не нужно:
— Ошибка является стандартной программной ошибкой (null-аргумент, выход за границы) — используйте IllegalArgumentException, IndexOutOfBoundsException
— Ошибка не требует специфической обработки и обрабатывается единообразно с другими сбоями
— Сценарий покрывается существующим стандартным исключением с информативным сообщением
#Java #для_новичков #beginner #exception #custom_exception
Глава 1. Иерархия исключений (Exceptions)
Создание своих исключений: наследование от Exception или RuntimeException. Когда кастомное исключение оправдано
Стандартные исключения Java — IOException, IllegalArgumentException, NullPointerException — описывают универсальные классы ошибок. Однако когда приложение вырастает за пределы учебных примеров, универсальные типы перестают адекватно передавать семантику сбоев. IOException не объясняет, что именно пошло не так в бизнес-процессе обработки платежа. IllegalArgumentException не указывает, какое поле запроса нарушило правило валидации.
Кастомное исключение — это класс, расширяющий Exception или RuntimeException, который несет доменную семантику ошибки. Его название становится частью языка предметной области (ubiquitous language в терминологии Domain-Driven Design), позволяя коду самодокументироваться и облегчая обработку ошибок через специфичные catch-блоки.
Однако создание кастомных исключений — это не бесплатная операция. Каждый новый класс исключения увеличивает сложность системы, требует поддержки и тестирования. Поэтому первый вопрос, который должен задать разработчик: действительно ли стандартное исключение не передает нужную семантику?
Когда кастомное исключение оправдано
Создание собственного класса исключения оправдано в следующих сценариях:
— Доменная специфика ошибки. Когда ошибка является неотъемлемой частью бизнес-логики и требует специфической обработки. InsufficientFundsException в банковской системе или SeatUnavailableException в системе бронирования — это не технические сбои, а валидные бизнес-сценарии, которые код должен обрабатыать осмысленно.
— Необходимость дополнительного контекста. Когда стандартное исключение не позволяет передать структурированную информацию об ошибке. Кастомный класс может содержать поля для error code, идентификатора сущности, деталей валидации, которые необходимы для формирования корректного ответа API или сообщения пользователю.
— Разделение обработки в catch-блоках. Когда разные типы ошибок требуют разной логики восстановления. Иерархия кастомных исключений позволяет использовать полиморфизм в обработке: перехватить общий тип для единообразной обработки, или специфичный для особой логики.
— API для внешнего использования. Когда код предназначен для использования как библиотека или сервис, и нужно предоставить четкий контракт возможных сбоев без экспонирования внутренних деталей реализации.
Обратные сценарии, когда кастомное исключение не нужно:
— Ошибка является стандартной программной ошибкой (null-аргумент, выход за границы) — используйте IllegalArgumentException, IndexOutOfBoundsException
— Ошибка не требует специфической обработки и обрабатывается единообразно с другими сбоями
— Сценарий покрывается существующим стандартным исключением с информативным сообщением
#Java #для_новичков #beginner #exception #custom_exception
👍4
Выбор между Exception и RuntimeException
Это архитектурное решение, определяющее контракт метода и обязательства вызывающего кода. Современная практика существенно сдвинулась в сторону RuntimeException, но понимание обоих подходов необходимо.
Checked-исключения: наследование от Exception
Checked-исключения форсируют явную обработку на уровне вызывающего кода. Компилятор гарантирует, что каждый potential failure path либо перехвачен, либо проброшен дальше. Это делает checked-исключения подходящими для сценариев, где восстановление не просто возможно, но и требуется по бизнес-логике.
Пример корректного использования checked-исключения:
Это checked-исключение оправдано, потому что:
— Восстановление требует специфических действий от вызывающего кода (инициация MFA)
— Исключение является частью доменной модели и бизнес-процесса
— Пропуск обработки приведет к некорректному поведению системы
Unchecked-исключения: наследование от RuntimeException
Unchecked-исключения предпочтительны для подавляющего большинства сценариев в современной Java-разработке. Они не загромождают сигнатуры методов, совместимы с функциональным стилем (Stream API, лямбды), и соответствуют философии Spring, Kotlin и других современных фреймворков.
Пример доменного unchecked-исключения:
Использование в доменном коде:
#Java #для_новичков #beginner #exception #custom_exception
Это архитектурное решение, определяющее контракт метода и обязательства вызывающего кода. Современная практика существенно сдвинулась в сторону RuntimeException, но понимание обоих подходов необходимо.
Checked-исключения: наследование от Exception
Checked-исключения форсируют явную обработку на уровне вызывающего кода. Компилятор гарантирует, что каждый potential failure path либо перехвачен, либо проброшен дальше. Это делает checked-исключения подходящими для сценариев, где восстановление не просто возможно, но и требуется по бизнес-логике.
Пример корректного использования checked-исключения:
/**
* Исключение, сигнализирующее о необходимости ручного подтверждения
* платежа из-за превышения лимита. Вызывающий код должен инициировать
* процедуру двухфакторной аутентификации.
*/
public class ManualApprovalRequiredException extends Exception {
private final String transactionId;
private final BigDecimal amount;
private final String requiredApprovalLevel;
public ManualApprovalRequiredException(String transactionId, BigDecimal amount,
String requiredApprovalLevel) {
super(String.format("Transaction %s requires %s approval for amount %s",
transactionId, requiredApprovalLevel, amount));
this.transactionId = transactionId;
this.amount = amount;
this.requiredApprovalLevel = requiredApprovalLevel;
}
public String getTransactionId() { return transactionId; }
public BigDecimal getAmount() { return amount; }
public String getRequiredApprovalLevel() { return requiredApprovalLevel; }
}
Это checked-исключение оправдано, потому что:
— Восстановление требует специфических действий от вызывающего кода (инициация MFA)
— Исключение является частью доменной модели и бизнес-процесса
— Пропуск обработки приведет к некорректному поведению системы
Unchecked-исключения: наследование от RuntimeException
Unchecked-исключения предпочтительны для подавляющего большинства сценариев в современной Java-разработке. Они не загромождают сигнатуры методов, совместимы с функциональным стилем (Stream API, лямбды), и соответствуют философии Spring, Kotlin и других современных фреймворков.
Пример доменного unchecked-исключения:
/**
* Исключение, сигнализирующее о нарушении бизнес-правила.
* Восстановление невозможно без изменения входных данных.
*/
public class BusinessRuleViolationException extends RuntimeException {
private final String ruleCode;
private final String entityType;
private final String entityId;
public BusinessRuleViolationException(String ruleCode, String entityType,
String entityId, String message) {
super(message);
this.ruleCode = ruleCode;
this.entityType = entityType;
this.entityId = entityId;
}
public String getRuleCode() { return ruleCode; }
public String getEntityType() { return entityType; }
public String getEntityId() { return entityId; }
}
Использование в доменном коде:
public class Order {
private OrderStatus status;
private List<OrderItem> items;
public void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new BusinessRuleViolationException(
"ORDER_ALREADY_SHIPPED",
"Order",
this.id,
"Cannot cancel order that has already been shipped"
);
}
if (status == OrderStatus.CANCELLED) {
throw new BusinessRuleViolationException(
"ORDER_ALREADY_CANCELLED",
"Order",
this.id,
"Order is already cancelled"
);
}
this.status = OrderStatus.CANCELLED;
}
}#Java #для_новичков #beginner #exception #custom_exception
👍4
Антипаттерны при проектировании исключений
Антипаттерн "God Exception"
Создание единого кастомного исключения с enum-кодами ошибок вместо иерархии классов — распространенный, но вредный подход. Разработчик создает один класс ApplicationException с полем ErrorCode, и использует enum для различения сценариев:
Проблемы этого подхода:
— Клиентский код вынужден использовать if-else или switch по error code вместо полиморфизма catch-блоков
— Каждое добавление нового сценария требует модификации enum, нарушая Open/Closed Principle
— Невозможно использовать специфичные catch-блоки для разных типов ошибок
— Type safety теряется — компилятор не может проверить корректность обработки
Правильный подход — использовать иерархию классов для сценариев, требующих разной обработки :
Теперь клиентский код может использовать полиморфизм:
#Java #для_новичков #beginner #exception #custom_exception
Антипаттерн "God Exception"
Создание единого кастомного исключения с enum-кодами ошибок вместо иерархии классов — распространенный, но вредный подход. Разработчик создает один класс ApplicationException с полем ErrorCode, и использует enum для различения сценариев:
// Антипаттерн: God Exception с enum
public class ApplicationException extends RuntimeException {
private final ErrorCode errorCode;
public ApplicationException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
// Использование
throw new ApplicationException(ErrorCode.INVALID_DATA_FORMAT, "...");
throw new ApplicationException(ErrorCode.MISSING_REQUIRED_FIELD, "...");
Проблемы этого подхода:
— Клиентский код вынужден использовать if-else или switch по error code вместо полиморфизма catch-блоков
— Каждое добавление нового сценария требует модификации enum, нарушая Open/Closed Principle
— Невозможно использовать специфичные catch-блоки для разных типов ошибок
— Type safety теряется — компилятор не может проверить корректность обработки
Правильный подход — использовать иерархию классов для сценариев, требующих разной обработки :
public abstract class PaymentException extends RuntimeException {
protected PaymentException(String message, Throwable cause) {
super(message, cause);
}
}
public class InsufficientFundsException extends PaymentException {
private final BigDecimal available;
private final BigDecimal requested;
public InsufficientFundsException(BigDecimal available, BigDecimal requested) {
super(String.format("Insufficient funds: available %s, requested %s",
available, requested), null);
this.available = available;
this.requested = requested;
}
}
public class PaymentGatewayTimeoutException extends PaymentException {
private final String gatewayId;
private final int timeoutMillis;
public PaymentGatewayTimeoutException(String gatewayId, int timeoutMillis) {
super(String.format("Gateway %s timed out after %dms", gatewayId, timeoutMillis), null);
this.gatewayId = gatewayId;
this.timeoutMillis = timeoutMillis;
}
}Теперь клиентский код может использовать полиморфизм:
try {
paymentService.process(payment);
} catch (InsufficientFundsException e) {
// Специфическая логика: предложить пополнение счета
return ResponseEntity.status(402)
.body(new PaymentErrorResponse("INSUFFICIENT_FUNDS", e.getAvailable()));
} catch (PaymentGatewayTimeoutException e) {
// Специфическая логика: retry с другим шлюзом
return retryWithAlternativeGateway(payment);
} catch (PaymentException e) {
// Общая обработка для всех платежных ошибок
return ResponseEntity.status(500)
.body(new PaymentErrorResponse("PAYMENT_FAILED", e.getMessage()));
}#Java #для_новичков #beginner #exception #custom_exception
👍4
Антипаттерн "Destructive Wrapping"
Потеря стектрейса при оборачивании исключений — критическая ошибка, делающая отладку невозможной:
Проектирование иерархии исключений
Хорошо спроектированная иерархия исключений отражает структуру предметной области и уровни абстракции. Рекомендуется создавать базовые классы для каждого домена или слоя приложения, от которых наследуются конкретные исключения.
Эта иерархия позволяет:
— Перехватывать все ошибки каталога через catch (CatalogException e)
— Перехватывать все ошибки приложения через catch (LibraryException e)
— Обрабатывать специфичные сценарии через конкретные типы
— Добавлять новые исключения без модификации существующего кода
#Java #для_новичков #beginner #exception #custom_exception
Потеря стектрейса при оборачивании исключений — критическая ошибка, делающая отладку невозможной:
// Антипаттерн: потеря cause
try {
processFile(file);
} catch (IOException e) {
throw new FileProcessingException("Failed to process file"); // Cause потерян!
}
Правильный подход — всегда передавать оригинальное исключение в конструктор:
java
Copy
// Правильно: сохранение цепочки причин
try {
processFile(file);
} catch (IOException e) {
throw new FileProcessingException("Failed to process file: " + file.getName(), e);
}
Проектирование иерархии исключений
Хорошо спроектированная иерархия исключений отражает структуру предметной области и уровни абстракции. Рекомендуется создавать базовые классы для каждого домена или слоя приложения, от которых наследуются конкретные исключения.
// Базовое исключение для всего приложения
public abstract class LibraryException extends RuntimeException {
protected LibraryException(String message, Throwable cause) {
super(message, cause);
}
}
// Домен: каталог книг
public abstract class CatalogException extends LibraryException {
protected CatalogException(String message, Throwable cause) {
super(message, cause);
}
}
public class BookNotFoundException extends CatalogException {
private final String isbn;
public BookNotFoundException(String isbn) {
super("Book not found: " + isbn, null);
this.isbn = isbn;
}
public String getIsbn() { return isbn; }
}
public class DuplicateIsbnException extends CatalogException {
private final String isbn;
private final Long existingBookId;
public DuplicateIsbnException(String isbn, Long existingBookId) {
super(String.format("ISBN %s already exists for book %d", isbn, existingBookId), null);
this.isbn = isbn;
this.existingBookId = existingBookId;
}
}
// Домен: пользователи
public abstract class UserException extends LibraryException {
protected UserException(String message, Throwable cause) {
super(message, cause);
}
}
public class UserNotActiveException extends UserException {
private final String userId;
private final UserStatus currentStatus;
public UserNotActiveException(String userId, UserStatus currentStatus) {
super(String.format("User %s is not active, current status: %s", userId, currentStatus), null);
this.userId = userId;
this.currentStatus = currentStatus;
}
}
Эта иерархия позволяет:
— Перехватывать все ошибки каталога через catch (CatalogException e)
— Перехватывать все ошибки приложения через catch (LibraryException e)
— Обрабатывать специфичные сценарии через конкретные типы
— Добавлять новые исключения без модификации существующего кода
#Java #для_новичков #beginner #exception #custom_exception
👍4
Конструкторы и структурированные данные
Каждый кастомный класс исключения должен предоставлять набор конструкторов, соответствующий стандартным паттернам Java:
Наличие конструктора с Throwable cause критично для поддержки exception chaining — механизма, позволяющего сохранять полный контекст ошибки при трансляции через слои приложения.
Интеграция с Spring и глобальной обработкой
В современных Spring-приложениях кастомные исключения интегрируются с глобальной обработкой ошибок через @ControllerAdvice, что устраняет необходимость в checked-исключениях и распределенных try-catch блоках:
#Java #для_новичков #beginner #exception #custom_exception
Каждый кастомный класс исключения должен предоставлять набор конструкторов, соответствующий стандартным паттернам Java:
public class LibraryProcessingException extends RuntimeException {
private final String operation;
private final String entityId;
private final Instant timestamp;
// Базовый конструктор
public LibraryProcessingException(String operation, String entityId, String message) {
this(operation, entityId, message, null);
}
// Конструктор с cause для цепочки исключений
public LibraryProcessingException(String operation, String entityId,
String message, Throwable cause) {
super(message, cause);
this.operation = operation;
this.entityId = entityId;
this.timestamp = Instant.now();
}
// Геттеры для структурированного доступа к данным
public String getOperation() { return operation; }
public String getEntityId() { return entityId; }
public Instant getTimestamp() { return timestamp; }
}Наличие конструктора с Throwable cause критично для поддержки exception chaining — механизма, позволяющего сохранять полный контекст ошибки при трансляции через слои приложения.
Интеграция с Spring и глобальной обработкой
В современных Spring-приложениях кастомные исключения интегрируются с глобальной обработкой ошибок через @ControllerAdvice, что устраняет необходимость в checked-исключениях и распределенных try-catch блоках:
@ControllerAdvice
public class LibraryExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<ErrorResponse> handleBookNotFound(BookNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(
"BOOK_NOT_FOUND",
e.getMessage(),
Map.of("isbn", e.getIsbn())
));
}
@ExceptionHandler(DuplicateIsbnException.class)
public ResponseEntity<ErrorResponse> handleDuplicateIsbn(DuplicateIsbnException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse(
"DUPLICATE_ISBN",
e.getMessage(),
Map.of("isbn", e.getIsbn(), "existingBookId", e.getExistingBookId())
));
}
@ExceptionHandler(LibraryException.class)
public ResponseEntity<ErrorResponse> handleGenericLibrary(LibraryException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"LIBRARY_ERROR",
"An unexpected library error occurred",
Map.of()
));
}
}
#Java #для_новичков #beginner #exception #custom_exception
👍5
Раздел 9. Исключения, логирование, отладка
Глава 1. Иерархия исключений (Exceptions)
Несколько ресурсов в try-with-resources: порядок закрытия и effectively final переменные (Java 9+)
Конструкция try-with-resources, появившаяся в Java 7, поддерживает объявление нескольких ресурсов в одном блоке, разделенных точкой с запятой. Спецификация языка Java (JLS, раздел 14.20.3) однозначно определяет порядок их закрытия: ресурсы закрываются в обратном порядке относительно порядка их инициализации.
Архитектурная логика обратного порядка
Рассмотрим типичный сценарий копирования данных:
В этом примере создается цепочка зависимостей: BufferedInputStream оборачивает FileInputStream, BufferedOutputStream оборачивает FileOutputStream. При закрытии необходимо соблюдать иерархию зависимостей — внешний обертчик должен быть закрыт до внутреннего ресурса, чтобы корректно сбросить буферизованные данные и освободить высокоуровневые ресурсы до низкоуровневых.
Согласно спецификации, закрытие произойдет в следующем порядке:
bos.close() — сброс буфера вывода и закрытие обертки
fos.close() — закрытие файлового потока вывода
bis.close() — закрытие обертки ввода
fis.close() — закрытие файлового потока ввода
Этот порядок гарантирует, что буферизованные данные будут корректно записаны на диск до закрытия файлового дескриптора, а ресурсы освобождаются от высокоуровневых абстракций к низкоуровневым примитивам.
Механизм компилятора
Компилятор Java трансформирует try-with-resources с несколькими ресурсами во вложенные блоки try-finally. Спецификация JLS определяет рекурсивную структуру: для двух ресурсов генерируется вложенный try, для трех — третий уровень вложенности и так далее . Каждый уровень отвечает за один ресурс и содержит логику suppressed exceptions.
Эквивалентный псевдокод для двух ресурсов:
Важное следствие этой трансформации: если при создании второго ресурса возникает исключение, первый ресурс, уже успешно созданный, будет автоматически закрыт перед пробросом исключения. Это фундаментальное преимущество try-with-resources перед ручным управлением ресурсами, где при ошибке создания второго ресурса первый остался бы незакрытым.
#Java #для_новичков #beginner #exception #try_with_resources
Глава 1. Иерархия исключений (Exceptions)
Несколько ресурсов в try-with-resources: порядок закрытия и effectively final переменные (Java 9+)
Конструкция try-with-resources, появившаяся в Java 7, поддерживает объявление нескольких ресурсов в одном блоке, разделенных точкой с запятой. Спецификация языка Java (JLS, раздел 14.20.3) однозначно определяет порядок их закрытия: ресурсы закрываются в обратном порядке относительно порядка их инициализации.
Архитектурная логика обратного порядка
Рассмотрим типичный сценарий копирования данных:
try (FileInputStream fis = new FileInputStream("source.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream("dest.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
}В этом примере создается цепочка зависимостей: BufferedInputStream оборачивает FileInputStream, BufferedOutputStream оборачивает FileOutputStream. При закрытии необходимо соблюдать иерархию зависимостей — внешний обертчик должен быть закрыт до внутреннего ресурса, чтобы корректно сбросить буферизованные данные и освободить высокоуровневые ресурсы до низкоуровневых.
Согласно спецификации, закрытие произойдет в следующем порядке:
bos.close() — сброс буфера вывода и закрытие обертки
fos.close() — закрытие файлового потока вывода
bis.close() — закрытие обертки ввода
fis.close() — закрытие файлового потока ввода
Этот порядок гарантирует, что буферизованные данные будут корректно записаны на диск до закрытия файлового дескриптора, а ресурсы освобождаются от высокоуровневых абстракций к низкоуровневым примитивам.
Механизм компилятора
Компилятор Java трансформирует try-with-resources с несколькими ресурсами во вложенные блоки try-finally. Спецификация JLS определяет рекурсивную структуру: для двух ресурсов генерируется вложенный try, для трех — третий уровень вложенности и так далее . Каждый уровень отвечает за один ресурс и содержит логику suppressed exceptions.
Эквивалентный псевдокод для двух ресурсов:
// Псевдокод трансформации компилятором
{
final Resource r1 = new Resource("r1");
Throwable primaryExc1 = null;
try {
final Resource r2 = new Resource("r2");
Throwable primaryExc2 = null;
try {
// Основной блок
r1.work();
r2.work();
} catch (Throwable t) {
primaryExc2 = t;
throw t;
} finally {
if (r2 != null) {
if (primaryExc2 != null) {
try {
r2.close();
} catch (Throwable suppressed) {
primaryExc2.addSuppressed(suppressed);
}
} else {
r2.close();
}
}
}
} catch (Throwable t) {
primaryExc1 = t;
throw t;
} finally {
if (r1 != null) {
if (primaryExc1 != null) {
try {
r1.close();
} catch (Throwable suppressed) {
primaryExc1.addSuppressed(suppressed);
}
} else {
r1.close();
}
}
}
}
Важное следствие этой трансформации: если при создании второго ресурса возникает исключение, первый ресурс, уже успешно созданный, будет автоматически закрыт перед пробросом исключения. Это фундаментальное преимущество try-with-resources перед ручным управлением ресурсами, где при ошибке создания второго ресурса первый остался бы незакрытым.
#Java #для_новичков #beginner #exception #try_with_resources
👍3
Подводный камень: двойное закрытие и делегирование
Важно понимать, что многие обертки потоков в Java делегируют закрытие внутреннему ресурсу. BufferedReader.close() вызывает FileReader.close(), ObjectOutputStream.close() вызывает FileOutputStream.close(). В сочетании с обратным порядком закрытия try-with-resources это может привести к двойному вызову close() на одном и том же underlying ресурсе.
Хотя большинство реализаций Closeable идемпотентны и повторный вызов close() безопасен, это не гарантируется интерфейсом AutoCloseable. При проектировании собственных ресурсов следует обеспечивать идемпотентность метода close() или отслеживать состояние закрытия.
JDBC: классический пример множественных ресурсов
Работа с JDBC демонстрирует практическую необходимость обратного порядка закрытия:
Закрытие произойдет в порядке: rs → stmt → conn. Это корректно, так как ResultSet зависит от PreparedStatement, который зависит от Connection. Закрытие в прямом порядке привело бы к попытке использования закрытого Statement при закрытии ResultSet, что вызвало бы SQLException.
Effectively final переменные в try-with-resources (Java 9+)
Проблема Java 7/8: принудительное объявление новой переменной
До Java 9 ресурсы в try-with-resources должны были объявляться непосредственно внутри круглых скобок после ключевого слова try. Это создавало избыточность, когда ресурс уже существовал во внешней области видимости и был готов к использованию:
В этом примере br и reader ссылаются на один и тот же объект, но компилятор требует новую переменную внутри try. Это загромождает код и создает путаницу в именовании.
Решение Java 9: прямое использование существующих переменных
Java 9 (JEP 213) расширила синтаксис try-with-resources, позволяя использовать переменные, объявленные вне блока, при условии что они являются final или effectively final. Переменная считается effectively final, если ей присваивается значение ровно один раз и она не модифицируется впоследствии, даже без явного модификатора final.
Новый синтаксис:
Для нескольких ресурсов синтаксис остается лаконичным:
Важное ограничение: ресурс в скобках try должен быть именно переменной (expression name), а не произвольным выражением. Конструкция try (getResource()) по-прежнему недопустима.
#Java #для_новичков #beginner #exception #try_with_resources
Важно понимать, что многие обертки потоков в Java делегируют закрытие внутреннему ресурсу. BufferedReader.close() вызывает FileReader.close(), ObjectOutputStream.close() вызывает FileOutputStream.close(). В сочетании с обратным порядком закрытия try-with-resources это может привести к двойному вызову close() на одном и том же underlying ресурсе.
try (FileReader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr)) {
// ...
}
// Закрытие: br.close() вызывает fr.close(), затем fr.close() вызывается повторноХотя большинство реализаций Closeable идемпотентны и повторный вызов close() безопасен, это не гарантируется интерфейсом AutoCloseable. При проектировании собственных ресурсов следует обеспечивать идемпотентность метода close() или отслеживать состояние закрытия.
JDBC: классический пример множественных ресурсов
Работа с JDBC демонстрирует практическую необходимость обратного порядка закрытия:
public List<User> findUsersByRole(String role) throws SQLException {
String query = "SELECT id, name, email FROM users WHERE role = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
ResultSet rs = stmt.executeQuery()) {
stmt.setString(1, role);
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapRowToUser(rs));
}
return users;
}
}Закрытие произойдет в порядке: rs → stmt → conn. Это корректно, так как ResultSet зависит от PreparedStatement, который зависит от Connection. Закрытие в прямом порядке привело бы к попытке использования закрытого Statement при закрытии ResultSet, что вызвало бы SQLException.
Effectively final переменные в try-with-resources (Java 9+)
Проблема Java 7/8: принудительное объявление новой переменной
До Java 9 ресурсы в try-with-resources должны были объявляться непосредственно внутри круглых скобок после ключевого слова try. Это создавало избыточность, когда ресурс уже существовал во внешней области видимости и был готов к использованию:
// Java 7/8: избыточное объявление новой переменной
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
try (BufferedReader reader = br) { // Вынуждены создать reader, хотя br уже готов
String line = reader.readLine();
// ...
}
В этом примере br и reader ссылаются на один и тот же объект, но компилятор требует новую переменную внутри try. Это загромождает код и создает путаницу в именовании.
Решение Java 9: прямое использование существующих переменных
Java 9 (JEP 213) расширила синтаксис try-with-resources, позволяя использовать переменные, объявленные вне блока, при условии что они являются final или effectively final. Переменная считается effectively final, если ей присваивается значение ровно один раз и она не модифицируется впоследствии, даже без явного модификатора final.
Новый синтаксис:
// Java 9+: прямое использование существующей переменной
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
try (br) { // Нет необходимости в новой переменной
String line = br.readLine();
// ...
}
Для нескольких ресурсов синтаксис остается лаконичным:
// Java 9+: несколько effectively final ресурсов
final Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query); // effectively final
try (statement; resultSet) {
while (resultSet.next()) {
processRow(resultSet);
}
}
Важное ограничение: ресурс в скобках try должен быть именно переменной (expression name), а не произвольным выражением. Конструкция try (getResource()) по-прежнему недопустима.
#Java #для_новичков #beginner #exception #try_with_resources
👍3
Практическое применение: рефакторинг legacy-кода
Рассмотрим сценарий, где ресурс создается в одном методе, передается в другой, и должен быть закрыт после использования:
С Java 9 этот код упрощается:
Критическое ограничение: безопасность при инициализации
Главное архитектурное отличие между классическим try-with-resources и синтаксисом effectively final переменных заключается в моменте захвата ресурса. Когда ресурс объявляется внутри try, компилятор гарантирует, что он будет закрыт даже если исключение возникнет при создании другого ресурса в том же блоке. С effectively final переменными эта гарантия теряется, так как ресурсы создаются вне try.
В этом примере, если конструктор r2 выбрасывает исключение, управление передается вне try-with-resources, и r1 остается незакрытым. В классическом синтаксисе try (Resource r1 = new Resource("r1"); Resource r2 = new Resource("r2")) ресурс r1 был бы закрыт автоматически перед пробросом исключения.
Рекомендация: использовать effectively final синтаксис только когда ресурсы уже гарантированно созданы и инициализированы, или когда логика приложения сама управляет их жизненным циклом до входа в try-with-resources.
Интеграция с try-catch-finally
Расширенный синтаксис Java 9 полностью совместим с традиционными catch и finally блоками. При наличии нескольких ресурсов, объявленных вне try, закрытие происходит перед входом в catch, а finally выполняется после обработки исключений:
Эволюция демонстрирует тенденцию к уменьшению шаблонного кода при сохранении безопасности. Java 7 устранила необходимость явного finally для закрытия, Java 9 устранила необходимость дублирования переменных.
#Java #для_новичков #beginner #exception #try_with_resources
Рассмотрим сценарий, где ресурс создается в одном методе, передается в другой, и должен быть закрыт после использования:
public class ReportGenerator {
public void generateMonthlyReport(Path outputPath) throws IOException {
// Ресурс создается здесь
BufferedWriter writer = Files.newBufferedWriter(outputPath);
try {
writeHeader(writer);
writeBody(writer);
writeFooter(writer);
} finally {
// Ручное закрытие до Java 9
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
logger.warn("Failed to close writer", e);
}
}
}
}
}С Java 9 этот код упрощается:
public void generateMonthlyReport(Path outputPath) throws IOException {
BufferedWriter writer = Files.newBufferedWriter(outputPath);
try (writer) {
writeHeader(writer);
writeBody(writer);
writeFooter(writer);
}
// writer автоматически закрыт, suppressed exceptions сохранены
}Критическое ограничение: безопасность при инициализации
Главное архитектурное отличие между классическим try-with-resources и синтаксисом effectively final переменных заключается в моменте захвата ресурса. Когда ресурс объявляется внутри try, компилятор гарантирует, что он будет закрыт даже если исключение возникнет при создании другого ресурса в том же блоке. С effectively final переменными эта гарантия теряется, так как ресурсы создаются вне try.
// Потенциально опасный код с effectively final
Resource r1 = new Resource("r1");
Resource r2 = new Resource("r2"); // Если здесь исключение — r1 не будет закрыт!
try (r1; r2) {
// ...
}
В этом примере, если конструктор r2 выбрасывает исключение, управление передается вне try-with-resources, и r1 остается незакрытым. В классическом синтаксисе try (Resource r1 = new Resource("r1"); Resource r2 = new Resource("r2")) ресурс r1 был бы закрыт автоматически перед пробросом исключения.
Рекомендация: использовать effectively final синтаксис только когда ресурсы уже гарантированно созданы и инициализированы, или когда логика приложения сама управляет их жизненным циклом до входа в try-with-resources.
Интеграция с try-catch-finally
Расширенный синтаксис Java 9 полностью совместим с традиционными catch и finally блоками. При наличии нескольких ресурсов, объявленных вне try, закрытие происходит перед входом в catch, а finally выполняется после обработки исключений:
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
ResultSet rs = stmt.executeQuery();
try (conn; stmt; rs) {
return extractResults(rs);
} catch (SQLException e) {
// Выполняется после закрытия всех ресурсов
throw new DataAccessException("Query failed: " + query, e);
} finally {
// Выполняется после catch, все ресурсы уже закрыты
metrics.recordQueryExecution(query, System.currentTimeMillis() - startTime);
}
Эволюция демонстрирует тенденцию к уменьшению шаблонного кода при сохранении безопасности. Java 7 устранила необходимость явного finally для закрытия, Java 9 устранила необходимость дублирования переменных.
#Java #для_новичков #beginner #exception #try_with_resources
👍4
Раздел 9. Исключения, логирование, отладка
Глава 1. Иерархия исключений (Exceptions)
Исключения в лямбдах, Stream API, конструкторах и статических блоках
Java предоставляет единый механизм исключений на базе класса Throwable, но поведение этого механизма существенно различается в зависимости от контекста выполнения. Лямбда-выражения и Stream API, появившиеся в Java 8, наложили архитектурные ограничения на использование checked-исключений, требуя от разработчиков поиска обходных стратегий. Конструкторы и статические блоки инициализации представляют собой особые точки жизненного цикла класса, где исключения ведут себя нестандартно и требуют специфического подхода к управлению ресурсами и обработке ошибок.
Часть первая. Исключения в лямбдах и Stream API
Функциональные интерфейсы Java 8 — Function<T,R>, Consumer<T>, Supplier<T>, Predicate<T> и другие — объявляют свои абстрактные методы без throws. Это означает, что любой метод, выбрасывающий checked-исключение, не может быть использован напрямую как лямбда-выражение или method reference в контексте Stream API.
Это ограничение не является oversight в дизайне языка, а отражает фундаментальную несовместимость checked-исключений с функциональным программированием. В функциональном стиле функции рассматриваются как значения, которые можно передавать, комбинировать и композировать. Checked-исключения нарушают эту композицию, требуя явной обработки на каждом уровне трансформации.
Стратегия первая: обёртка в try-catch внутри лямбды
Наиболее прямолинейный подход — обернуть вызов метода с checked-исключением в try-catch блок внутри лямбды и транслировать checked в unchecked:
Этот подход работает, но имеет серьезные недостатки. Во-первых, он загромождает код шаблонной обработкой исключений, разрушая лаконичность функционального стиля. Во-вторых, выброс RuntimeException из лямбды в Stream API прерывает весь конвейер — невозможно обработать ошибку для одного элемента и продолжить обработку остальных. В-третьих, исключение теряет семантику checked, и вызывающий код может не осознавать возможность сбоя.
Для сценариев, где требуется игнорировать ошибки и продолжить обработку, можно возвращать значение по умолчанию:
Здесь NumberFormatException — unchecked-исключение, но паттерн применим и к checked. Фильтрация Objects::nonNull удаляет null-значения, соответствующие ошибкам парсинга. Однако этот подход имеет побочный эффект: информация об ошибках теряется без логирования.
Улучшенная версия с логированием:
#Java #для_новичков #beginner #exception
Глава 1. Иерархия исключений (Exceptions)
Исключения в лямбдах, Stream API, конструкторах и статических блоках
Java предоставляет единый механизм исключений на базе класса Throwable, но поведение этого механизма существенно различается в зависимости от контекста выполнения. Лямбда-выражения и Stream API, появившиеся в Java 8, наложили архитектурные ограничения на использование checked-исключений, требуя от разработчиков поиска обходных стратегий. Конструкторы и статические блоки инициализации представляют собой особые точки жизненного цикла класса, где исключения ведут себя нестандартно и требуют специфического подхода к управлению ресурсами и обработке ошибок.
Часть первая. Исключения в лямбдах и Stream API
Функциональные интерфейсы Java 8 — Function<T,R>, Consumer<T>, Supplier<T>, Predicate<T> и другие — объявляют свои абстрактные методы без throws. Это означает, что любой метод, выбрасывающий checked-исключение, не может быть использован напрямую как лямбда-выражение или method reference в контексте Stream API.
// Метод с checked-исключением
public String fetchUrl(String url) throws IOException {
return httpClient.fetch(url);
}
// Ошибка компиляции: unreported exception IOException
List<String> urls = List.of("http://api1.com", "http://api2.com");
List<String> results = urls.stream()
.map(this::fetchUrl) // Не компилируется
.collect(Collectors.toList());
Это ограничение не является oversight в дизайне языка, а отражает фундаментальную несовместимость checked-исключений с функциональным программированием. В функциональном стиле функции рассматриваются как значения, которые можно передавать, комбинировать и композировать. Checked-исключения нарушают эту композицию, требуя явной обработки на каждом уровне трансформации.
Стратегия первая: обёртка в try-catch внутри лямбды
Наиболее прямолинейный подход — обернуть вызов метода с checked-исключением в try-catch блок внутри лямбды и транслировать checked в unchecked:
List<String> results = urls.stream()
.map(url -> {
try {
return fetchUrl(url);
} catch (IOException e) {
throw new RuntimeException("Failed to fetch: " + url, e);
}
})
.collect(Collectors.toList());
Этот подход работает, но имеет серьезные недостатки. Во-первых, он загромождает код шаблонной обработкой исключений, разрушая лаконичность функционального стиля. Во-вторых, выброс RuntimeException из лямбды в Stream API прерывает весь конвейер — невозможно обработать ошибку для одного элемента и продолжить обработку остальных. В-третьих, исключение теряет семантику checked, и вызывающий код может не осознавать возможность сбоя.
Для сценариев, где требуется игнорировать ошибки и продолжить обработку, можно возвращать значение по умолчанию:
// Парсинг чисел из списка строк с игнорированием ошибок
List<String> rawValues = List.of("42", "invalid", "17", "3.14", "100");
List<Integer> parsedNumbers = rawValues.stream()
.map(s -> {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
// Возвращаем null для невалидных значений
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
// Результат: [42, 17, 100]
Здесь NumberFormatException — unchecked-исключение, но паттерн применим и к checked. Фильтрация Objects::nonNull удаляет null-значения, соответствующие ошибкам парсинга. Однако этот подход имеет побочный эффект: информация об ошибках теряется без логирования.
Улучшенная версия с логированием:
List<Integer> parsedNumbers = rawValues.stream()
.map(s -> {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
logger.warn("Failed to parse integer from '{}'", s);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
#Java #для_новичков #beginner #exception
👍2
Стратегия вторая: кастомный функциональный интерфейс с throws
Для сохранения семантики checked-исключений и повторного использования обработки можно создать собственные функциональные интерфейсы, объявляющие throws:
Однако эти интерфейсы несовместимы со стандартным Stream API. Для интеграции требуется обёртка, транслирующая кастомный интерфейс в стандартный:
Этот подход сохраняет чистоту вызова, но по-прежнему теряет checked-семантику на границе обёртки.
#Java #для_новичков #beginner #exception
Для сохранения семантики checked-исключений и повторного использования обработки можно создать собственные функциональные интерфейсы, объявляющие throws:
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
void accept(T t) throws E;
}
@FunctionalInterface
public interface ThrowingSupplier<T, E extends Exception> {
T get() throws E;
}
Эти интерфейсы позволяют объявлять методы, принимающие функциональные объекты с checked-исключениями:
java
Copy
public <T, R, E extends Exception> List<R> mapWithException(
List<T> list,
ThrowingFunction<T, R, E> function) throws E {
List<R> result = new ArrayList<>();
for (T item : list) {
result.add(function.apply(item));
}
return result;
}
// Использование
List<String> urls = List.of("http://api1.com", "http://api2.com");
try {
List<String> contents = mapWithException(urls, this::fetchUrl);
} catch (IOException e) {
// Обработка ошибки
}
Однако эти интерфейсы несовместимы со стандартным Stream API. Для интеграции требуется обёртка, транслирующая кастомный интерфейс в стандартный:
public static <T, E extends Exception> Consumer<T> throwingConsumerWrapper(
ThrowingConsumer<T, E> throwingConsumer) {
return item -> {
try {
throwingConsumer.accept(item);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
// Использование
urls.forEach(throwingConsumerWrapper(url -> {
writeToFile(url); // Метод, объявляющий throws IOException
}));
Этот подход сохраняет чистоту вызова, но по-прежнему теряет checked-семантику на границе обёртки.
#Java #для_новичков #beginner #exception
👍2
Стратегия третья: sneaky throws
Sneaky throws — техника, позволяющая выбросить checked-исключение без объявления его в сигнатуре метода, используя особенности generics и стирания типов в Java:
Механизм работает благодаря стиранию типов: компилятор не может проверить, что E не является RuntimeException, поэтому разрешает выброс без throws. Однако это крайне опасная практика. Вызывающий код не знает о возможности checked-исключения и не обрабатывает его. JVM вынуждена обрабатывать исключение как unchecked, что нарушает контракт типов и может привести к непредсказуемому поведению при межпроцессном взаимодействии или сериализации.
Sneaky throws оправдан только в крайне специфических сценариях: библиотечный код, полностью контролирующий контекст выполнения, или временный рефакторинг legacy-систем. В production-коде этот паттерн следует избегать.
Стратегия четвертая: Either и Try из функциональных библиотек
Наиболее элегантный подход для функциональной обработки ошибок — использование типов-результатов из библиотек вроде Vavr или самостоятельная реализация. Вместо выброса исключений метод возвращает объект, явно моделирующий два состояния: успех или ошибку.
Применение для парсинга с сохранением ошибок:
Этот подход полностью устраняет исключения из потока управления, превращая их в значения. Клиентский код вынужден обрабатывать оба случая, что устраняет риск "забытого catch". Библиотека Vavr предоставляет готовую реализацию io.vavr.control.Either и io.vavr.control.Try с богатым API для композиции и обработки результатов.
#Java #для_новичков #beginner #exception
Sneaky throws — техника, позволяющая выбросить checked-исключение без объявления его в сигнатуре метода, используя особенности generics и стирания типов в Java:
@SuppressWarnings("unchecked")
public static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
throw (E) e;
}
// Использование в лямбде
List<String> results = urls.stream()
.map(url -> {
try {
return fetchUrl(url);
} catch (IOException e) {
sneakyThrow(e); // Компилятор не требует throws
return null; // Недостижимый код, необходим для компиляции
}
})
.collect(Collectors.toList());Механизм работает благодаря стиранию типов: компилятор не может проверить, что E не является RuntimeException, поэтому разрешает выброс без throws. Однако это крайне опасная практика. Вызывающий код не знает о возможности checked-исключения и не обрабатывает его. JVM вынуждена обрабатывать исключение как unchecked, что нарушает контракт типов и может привести к непредсказуемому поведению при межпроцессном взаимодействии или сериализации.
Sneaky throws оправдан только в крайне специфических сценариях: библиотечный код, полностью контролирующий контекст выполнения, или временный рефакторинг legacy-систем. В production-коде этот паттерн следует избегать.
Стратегия четвертая: Either и Try из функциональных библиотек
Наиболее элегантный подход для функциональной обработки ошибок — использование типов-результатов из библиотек вроде Vavr или самостоятельная реализация. Вместо выброса исключений метод возвращает объект, явно моделирующий два состояния: успех или ошибку.
// Упрощенная реализация Either (левая сторона — ошибка, правая — успех)
public class Either<L, R> {
private final L left;
private final R right;
private final boolean isLeft;
private Either(L left, R right, boolean isLeft) {
this.left = left;
this.right = right;
this.isLeft = isLeft;
}
public static <L, R> Either<L, R> left(L value) {
return new Either<>(value, null, true);
}
public static <L, R> Either<L, R> right(R value) {
return new Either<>(null, value, false);
}
public boolean isLeft() { return isLeft; }
public boolean isRight() { return !isLeft; }
public L getLeft() { return left; }
public R getRight() { return right; }
}
Применение для парсинга с сохранением ошибок:
public Either<String, Integer> parseInteger(String input) {
try {
return Either.right(Integer.parseInt(input));
} catch (NumberFormatException e) {
return Either.left("Invalid number format: " + input);
}
}
// Обработка в Stream API
List<String> rawValues = List.of("42", "invalid", "17", "not_a_number", "100");
List<Either<String, Integer>> results = rawValues.stream()
.map(this::parseInteger)
.collect(Collectors.toList());
List<Integer> successes = results.stream()
.filter(Either::isRight)
.map(Either::getRight)
.collect(Collectors.toList());
List<String> failures = results.stream()
.filter(Either::isLeft)
.map(Either::getLeft)
.collect(Collectors.toList());Этот подход полностью устраняет исключения из потока управления, превращая их в значения. Клиентский код вынужден обрабатывать оба случая, что устраняет риск "забытого catch". Библиотека Vavr предоставляет готовую реализацию io.vavr.control.Either и io.vavr.control.Try с богатым API для композиции и обработки результатов.
#Java #для_новичков #beginner #exception
👍2
Практический пример: парсинг чисел с игнорированием ошибок
Объединим подходы в комплексном примере. Задача: преобразовать список строк в список целых чисел, игнорируя невалидные значения и логируя ошибки.
Первый метод parseAllIgnoringErrors использует Optional для фильтрации ошибок — подход, нативно поддерживаемый Java. Второй метод parseAllWithDetails сохраняет информацию о неудачах для последующего анализа или отчетности.
Часть вторая. Исключения в конструкторах и статических блоках
Конструктор в Java — это специальный метод, вызываемый при создании объекта оператором new. Если конструктор выбрасывает исключение, объект не создается. Это фундаментальное свойство имеет важное следствие: ресурсы, выделенные внутри конструктора до момента исключения, не требуют закрытия через try-finally или try-with-resources, потому что объект не существует и не будет существовать.
#Java #для_новичков #beginner #exception
Объединим подходы в комплексном примере. Задача: преобразовать список строк в список целых чисел, игнорируя невалидные значения и логируя ошибки.
public class NumberParser {
private static final Logger logger = LoggerFactory.getLogger(NumberParser.class);
// Стратегия: обёртка с возвратом Optional
public List<Integer> parseAllIgnoringErrors(List<String> inputs) {
return inputs.stream()
.map(this::safeParse)
.flatMap(Optional::stream) // Java 9+: фильтрация пустых Optional
.collect(Collectors.toList());
}
private Optional<Integer> safeParse(String input) {
try {
return Optional.of(Integer.parseInt(input.trim()));
} catch (NumberFormatException e) {
logger.warn("Skipping invalid number: '{}'", input);
return Optional.empty();
}
}
// Стратегия: разделение на успешные и неуспешные
public ParseResult parseAllWithDetails(List<String> inputs) {
Map<Boolean, List<Either<String, Integer>>> partitioned = inputs.stream()
.map(this::parseWithError)
.collect(Collectors.partitioningBy(Either::isRight));
List<Integer> numbers = partitioned.get(true).stream()
.map(Either::getRight)
.collect(Collectors.toList());
List<String> errors = partitioned.get(false).stream()
.map(Either::getLeft)
.collect(Collectors.toList());
return new ParseResult(numbers, errors);
}
private Either<String, Integer> parseWithError(String input) {
try {
return Either.right(Integer.parseInt(input.trim()));
} catch (NumberFormatException e) {
return Either.left(input);
}
}
public record ParseResult(List<Integer> numbers, List<String> failedInputs) {}
}Первый метод parseAllIgnoringErrors использует Optional для фильтрации ошибок — подход, нативно поддерживаемый Java. Второй метод parseAllWithDetails сохраняет информацию о неудачах для последующего анализа или отчетности.
Часть вторая. Исключения в конструкторах и статических блоках
Конструктор в Java — это специальный метод, вызываемый при создании объекта оператором new. Если конструктор выбрасывает исключение, объект не создается. Это фундаментальное свойство имеет важное следствие: ресурсы, выделенные внутри конструктора до момента исключения, не требуют закрытия через try-finally или try-with-resources, потому что объект не существует и не будет существовать.
public class FileProcessor {
private final FileInputStream inputStream;
private final BufferedReader reader;
public FileProcessor(String path) throws FileNotFoundException {
// Если new FileInputStream выбросит исключение,
// объект FileProcessor не будет создан
this.inputStream = new FileInputStream(path);
// Эта строка выполнится только если FileInputStream создан успешно
this.reader = new BufferedReader(new InputStreamReader(inputStream));
}
}#Java #для_новичков #beginner #exception
👍2
В этом примере, если new FileInputStream(path) выбросит FileNotFoundException, конструктор прервется, объект FileProcessor не будет инстанцирован, и reader не будет создан. Никаких утечек ресурсов не происходит, так как FileInputStream сам не был создан.
Однако если конструктор выделяет несколько ресурсов последовательно, ситуация усложняется:
Если statement.executeQuery(query) выбросит SQLException, объект MultiResourceProcessor не будет создан, но connection и statement уже были созданы и останутся незакрытыми. Это классический сценарий утечки ресурсов в конструкторе.
Решение — использование локальных переменных и try для гарантии закрытия частично созданных ресурсов:
Этот паттерн гарантирует, что любые ресурсы, созданные до возникновения исключения, будут закрыты. Однако он чрезвычайно многословен. Современный подход — отказ от сложной инициализации в конструкторе и использование фабричных методов:
Важное ограничение: try-with-resources закроет ресурсы при выходе из блока, поэтому возврат ResultSet из фабричного метода требует отказа от автоматического закрытия или использования специальных оберток. В большинстве случаев лучше отложить создание ресурсов до момента фактического использования, а не выполнять их в конструкторе.
#Java #для_новичков #beginner #exception
Однако если конструктор выделяет несколько ресурсов последовательно, ситуация усложняется:
public class MultiResourceProcessor {
private final Connection connection;
private final Statement statement;
private final ResultSet resultSet;
public MultiResourceProcessor(String query) throws SQLException {
this.connection = DriverManager.getConnection(DB_URL);
this.statement = connection.createStatement();
this.resultSet = statement.executeQuery(query);
}
}Если statement.executeQuery(query) выбросит SQLException, объект MultiResourceProcessor не будет создан, но connection и statement уже были созданы и останутся незакрытыми. Это классический сценарий утечки ресурсов в конструкторе.
Решение — использование локальных переменных и try для гарантии закрытия частично созданных ресурсов:
public MultiResourceProcessor(String query) throws SQLException {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(DB_URL);
stmt = conn.createStatement();
rs = stmt.executeQuery(query);
// Все ресурсы созданы успешно — присваиваем полям
this.connection = conn;
this.statement = stmt;
this.resultSet = rs;
} catch (SQLException e) {
// Закрытие частично созданных ресурсов
if (rs != null) try { rs.close(); } catch (SQLException ignored) {}
if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
if (conn != null) try { conn.close(); } catch (SQLException ignored) {}
throw e;
}
}Этот паттерн гарантирует, что любые ресурсы, созданные до возникновения исключения, будут закрыты. Однако он чрезвычайно многословен. Современный подход — отказ от сложной инициализации в конструкторе и использование фабричных методов:
public class MultiResourceProcessor {
private final Connection connection;
private final Statement statement;
private final ResultSet resultSet;
private MultiResourceProcessor(Connection connection, Statement statement, ResultSet resultSet) {
this.connection = connection;
this.statement = statement;
this.resultSet = resultSet;
}
public static MultiResourceProcessor create(String query) throws SQLException {
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query)) {
// ResultSet не закроется при выходе из try, так как мы его возвращаем
// Это требует специальной обработки — см. ниже
return new MultiResourceProcessor(conn, stmt, rs);
}
}
}Важное ограничение: try-with-resources закроет ресурсы при выходе из блока, поэтому возврат ResultSet из фабричного метода требует отказа от автоматического закрытия или использования специальных оберток. В большинстве случаев лучше отложить создание ресурсов до момента фактического использования, а не выполнять их в конструкторе.
#Java #для_новичков #beginner #exception
👍2
Исключения в статических блоках
Статический блок инициализации выполняется при загрузке класса JVM, до создания любых экземпляров. Если в процессе выполнения статического блока или инициализации статической переменной возникает исключение, JVM автоматически оборачивает его в ExceptionInInitializerError.
При первом обращении к классу ConfigHolder будет выброшено:
ExceptionInInitializerError наследует LinkageError, что сигнализирует о фатальной проблеме при загрузке класса. Ключевое следствие: класс, выбросивший ExceptionInInitializerError, помечается как неинициализированный и не может быть использован в дальнейшем . Все последующие попытки обращения к этому классу приведут к NoClassDefFoundError с причиной "initialization error".
Это поведение делает ExceptionInInitializerError особенно опасным: одна ошибка при загрузке класса делает его непригодным на всю жизнь JVM. Перезагрузка класса возможна только через создание нового ClassLoader.
Checked-исключения напрямую запрещены в статических блоках. Компилятор отклонит код, пытающийся выбросить checked из static initializer:
Для обработки checked-исключений в статических блоках применяется паттерн оборачивания в ExceptionInInitializerError:
В этом примере IOException из loadProperties() перехватывается и оборачивается в ExceptionInInitializerError. Важно: если мы явно выбрасываем ExceptionInInitializerError, JVM не оборачивает его повторно — сохраняется чистый стектрейс . Если же мы обернем checked в RuntimeException, JVM дополнительно обернет его в ExceptionInInitializerError, создавая избыточную вложенность:
#Java #для_новичков #beginner #exception
Статический блок инициализации выполняется при загрузке класса JVM, до создания любых экземпляров. Если в процессе выполнения статического блока или инициализации статической переменной возникает исключение, JVM автоматически оборачивает его в ExceptionInInitializerError.
public class ConfigHolder {
private static final Map<String, String> config;
static {
config = loadConfig(); // Может выбросить RuntimeException
}
private static Map<String, String> loadConfig() {
throw new RuntimeException("Configuration file corrupted");
}
}При первом обращении к классу ConfigHolder будет выброшено:
java.lang.ExceptionInInitializerError
Caused by: java.lang.RuntimeException: Configuration file corrupted
ExceptionInInitializerError наследует LinkageError, что сигнализирует о фатальной проблеме при загрузке класса. Ключевое следствие: класс, выбросивший ExceptionInInitializerError, помечается как неинициализированный и не может быть использован в дальнейшем . Все последующие попытки обращения к этому классу приведут к NoClassDefFoundError с причиной "initialization error".
// Первое обращение — ExceptionInInitializerError
try {
ConfigHolder holder = new ConfigHolder();
} catch (ExceptionInInitializerError e) {
// Обработка
}
// Второе обращение — NoClassDefFoundError, даже если причина устранена
try {
ConfigHolder holder = new ConfigHolder(); // Не работает!
} catch (NoClassDefFoundError e) {
// Класс навсегда непригоден
}
Это поведение делает ExceptionInInitializerError особенно опасным: одна ошибка при загрузке класса делает его непригодным на всю жизнь JVM. Перезагрузка класса возможна только через создание нового ClassLoader.
Checked-исключения напрямую запрещены в статических блоках. Компилятор отклонит код, пытающийся выбросить checked из static initializer:
public class InvalidStatic {
static {
throw new IOException("Not allowed"); // Ошибка компиляции
}
}Для обработки checked-исключений в статических блоках применяется паттерн оборачивания в ExceptionInInitializerError:
public class SafeStaticInit {
private static final Properties props;
static {
try {
props = loadProperties();
} catch (IOException e) {
// Оборачиваем checked в ExceptionInInitializerError
throw new ExceptionInInitializerError(e);
}
}
private static Properties loadProperties() throws IOException {
Properties p = new Properties();
try (InputStream is = SafeStaticInit.class.getResourceAsStream("/config.properties")) {
if (is == null) {
throw new IOException("Config file not found");
}
p.load(is);
}
return p;
}
}В этом примере IOException из loadProperties() перехватывается и оборачивается в ExceptionInInitializerError. Важно: если мы явно выбрасываем ExceptionInInitializerError, JVM не оборачивает его повторно — сохраняется чистый стектрейс . Если же мы обернем checked в RuntimeException, JVM дополнительно обернет его в ExceptionInInitializerError, создавая избыточную вложенность:
// Антипаттерн: избыточная вложенность
static {
try {
props = loadProperties();
} catch (IOException e) {
throw new RuntimeException(e); // JVM обернет в ExceptionInInitializerError
}
}
// Результат: ExceptionInInitializerError -> RuntimeException -> IOException
#Java #для_новичков #beginner #exception
👍4
Раздел 9. Исключения, логирование, отладка
Глава 1. Иерархия исключений (Exceptions)
Objects.requireNonNull() – стандартная фабрика NullPointerException
Null-ссылка, введенная Тони Хоаром в 1965 году, впоследствии названная им "миллиардной ошибкой" (billion-dollar mistake), остается одним из наиболее частых источников runtime-ошибок в Java. NullPointerException занимает второе место по частоте среди всех дефектов программного обеспечения, что подчеркивает масштаб проблемы.
В Java nullability является неявной: если API не документировано явно, разработчик не может быть уверен, может ли возвращаемое значение быть null, что приводит к недопониманию и багам.
До Java 7 проверка параметров на null выполнялась через явные условия:
Этот подход многословен и не предоставляет стандартизированного способа валидации. Java 7 ввела класс java.util.Objects с методом requireNonNull(), который превратил проверку null в однострочную операцию с гибкими возможностями кастомизации сообщений.
Методы requireNonNull: три перегрузки
Класс Objects предоставляет три перегруженные версии requireNonNull, каждая из которых подходит для разных сценариев:
Все три версии возвращают переданный объект, если он не null, что позволяет использовать их inline при инициализации полей или передаче параметров. Если объект null, выбрасывается NullPointerException с соответствующим сообщением.
Базовая версия: fail-fast без лишних слов
Здесь requireNonNull(repository) возвращает repository, если он не null, или выбрасывает NullPointerException с дефолтным сообщением. Этот паттерн идеален для конструкторов, где требуется гарантия ненулевых зависимостей, а специфичное сообщение не критично.
Версия со статическим сообщением: контекст для отладки
Статическое сообщение предоставляет контекст при возникновении исключения, облегчая отладку. Однако сообщение вычисляется всегда, даже если объект не null, что незначительно, но избыточно для горячих путей выполнения.
Версия с Supplier: ленивое вычисление сообщений
Третья перегрузка принимает Supplier<String> и вычисляет сообщение только при фактической необходимости — когда объект равен null:
Эта версия критически важна для сценариев, где формирование сообщения требует значительных вычислений: конкатенация строк, форматирование дат, обращение к внешним ресурсам. При нормальном выполнении Supplier не вызывается, что устраняет накладные расходы.
Сравнение производительности:
В первом случае конкатенация строк выполняется при каждом вызове метода. Во втором — только при нарушении предусловия.
#Java #для_новичков #beginner #exception #requireNonNull
Глава 1. Иерархия исключений (Exceptions)
Objects.requireNonNull() – стандартная фабрика NullPointerException
Null-ссылка, введенная Тони Хоаром в 1965 году, впоследствии названная им "миллиардной ошибкой" (billion-dollar mistake), остается одним из наиболее частых источников runtime-ошибок в Java. NullPointerException занимает второе место по частоте среди всех дефектов программного обеспечения, что подчеркивает масштаб проблемы.
В Java nullability является неявной: если API не документировано явно, разработчик не может быть уверен, может ли возвращаемое значение быть null, что приводит к недопониманию и багам.
До Java 7 проверка параметров на null выполнялась через явные условия:
public void processOrder(Order order) {
if (order == null) {
throw new NullPointerException("Order cannot be null");
}
// Основная логика
}Этот подход многословен и не предоставляет стандартизированного способа валидации. Java 7 ввела класс java.util.Objects с методом requireNonNull(), который превратил проверку null в однострочную операцию с гибкими возможностями кастомизации сообщений.
Методы requireNonNull: три перегрузки
Класс Objects предоставляет три перегруженные версии requireNonNull, каждая из которых подходит для разных сценариев:
// 1. Базовая версия без сообщения
public static <T> T requireNonNull(T obj)
// 2. Версия со статическим сообщением
public static <T> T requireNonNull(T obj, String message)
// 3. Версия с ленивым сообщением через Supplier
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier)
Все три версии возвращают переданный объект, если он не null, что позволяет использовать их inline при инициализации полей или передаче параметров. Если объект null, выбрасывается NullPointerException с соответствующим сообщением.
Базовая версия: fail-fast без лишних слов
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = Objects.requireNonNull(repository);
}
}Здесь requireNonNull(repository) возвращает repository, если он не null, или выбрасывает NullPointerException с дефолтным сообщением. Этот паттерн идеален для конструкторов, где требуется гарантия ненулевых зависимостей, а специфичное сообщение не критично.
Версия со статическим сообщением: контекст для отладки
public void updateInventory(Inventory inventory, String warehouseId) {
Objects.requireNonNull(inventory, "Inventory object cannot be null");
Objects.requireNonNull(warehouseId, "Warehouse ID must be specified");
inventory.adjustStock(warehouseId);
}Статическое сообщение предоставляет контекст при возникновении исключения, облегчая отладку. Однако сообщение вычисляется всегда, даже если объект не null, что незначительно, но избыточно для горячих путей выполнения.
Версия с Supplier: ленивое вычисление сообщений
Третья перегрузка принимает Supplier<String> и вычисляет сообщение только при фактической необходимости — когда объект равен null:
public void processTransaction(Transaction tx) {
Objects.requireNonNull(tx,
() -> String.format("Transaction cannot be null at %s", Instant.now()));
// Сложное форматирование выполняется только если tx == null
}Эта версия критически важна для сценариев, где формирование сообщения требует значительных вычислений: конкатенация строк, форматирование дат, обращение к внешним ресурсам. При нормальном выполнении Supplier не вызывается, что устраняет накладные расходы.
Сравнение производительности:
// Избыточно: сообщение формируется всегда
Objects.requireNonNull(user, "User " + userId + " not found in database " + dbName);
// Эффективно: сообщение формируется только при null
Objects.requireNonNull(user,
() -> "User " + userId + " not found in database " + dbName);
В первом случае конкатенация строк выполняется при каждом вызове метода. Во втором — только при нарушении предусловия.
#Java #для_новичков #beginner #exception #requireNonNull
👍3
Когда использовать requireNonNull
Валидация параметров методов и конструкторов
Основное применение requireNonNull — проверка предусловий (preconditions) на границе метода. Это реализация принципа fail-fast: ошибка обнаруживается немедленно, предотвращая каскадные сбои и упрощая отладку. Если метод валидирует параметры upfront, он быстро завершается с четким исключением, указывающим источник проблемы.
Гарантия ненулевых возвращаемых значений
requireNonNull может использоваться для защиты от непреднамеренного возврата null из методов, особенно при делегировании к внутренним компонентам:
Здесь requireNonNull служит последней линией защиты: если репозиторий вернул null (что не должно происходить по контракту, но возможно из-за бага), метод выбросит NullPointerException с информативным сообщением вместо передачи null вызывающему коду.
Защита внутреннего состояния при делегировании
При реализации методов, делегирующих вызовы внутренним объектам, requireNonNull гарантирует, что поле инициализировано:
Вызов fetchData() до initialize() выбросит NullPointerException с понятным сообщением, вместо стандартного NPE на разыменовании null.
Документирование через Javadoc
Использование requireNonNull должно сопровождаться документированием в Javadoc. Тег @throws указывает, что метод выбрасывает NullPointerException при нарушении параметрических ограничений:
Для классов, где множество методов выбрасывают NullPointerException при нарушении предусловий, можно документировать это на уровне класса, избегая повторений в каждом методе.
Связь с аннотациями @NonNull
Разделение ответственности: runtime vs compile-time
Objects.requireNonNull() обеспечивает runtime-проверку: исключение возникает во время выполнения, если null передан в метод. Аннотации @NonNull (и их аналоги) предоставляют compile-time информацию о nullability, позволяя IDE и статическим анализаторам предупреждать о потенциальных проблемах до запуска программы.
Эти механизмы комплементарны, а не взаимоисключающие. Аннотации @NonNull документируют контракт и помогают инструментам, но не обеспечивают runtime-защиту. requireNonNull гарантирует защиту во время выполнения, но не предоставляет информации для статического анализа. Идеальный подход — комбинировать оба механизма.
#Java #для_новичков #beginner #exception #requireNonNull
Валидация параметров методов и конструкторов
Основное применение requireNonNull — проверка предусловий (preconditions) на границе метода. Это реализация принципа fail-fast: ошибка обнаруживается немедленно, предотвращая каскадные сбои и упрощая отладку. Если метод валидирует параметры upfront, он быстро завершается с четким исключением, указывающим источник проблемы.
public class PaymentProcessor {
private final PaymentGateway gateway;
private final TransactionLogger logger;
public PaymentProcessor(PaymentGateway gateway, TransactionLogger logger) {
this.gateway = Objects.requireNonNull(gateway, "PaymentGateway is required");
this.logger = Objects.requireNonNull(logger, "TransactionLogger is required");
}
public PaymentResult process(PaymentRequest request) {
Objects.requireNonNull(request, "PaymentRequest cannot be null");
gateway.authorize(request);
logger.log(request);
return new PaymentResult();
}
}Гарантия ненулевых возвращаемых значений
requireNonNull может использоваться для защиты от непреднамеренного возврата null из методов, особенно при делегировании к внутренним компонентам:
public Customer getCustomer(String customerId) {
Customer customer = customerRepository.findById(customerId);
return Objects.requireNonNull(customer,
() -> "Customer not found for ID: " + customerId);
}Здесь requireNonNull служит последней линией защиты: если репозиторий вернул null (что не должно происходить по контракту, но возможно из-за бага), метод выбросит NullPointerException с информативным сообщением вместо передачи null вызывающему коду.
Защита внутреннего состояния при делегировании
При реализации методов, делегирующих вызовы внутренним объектам, requireNonNull гарантирует, что поле инициализировано:
public class CachedDataProvider {
private DataProvider delegate;
public void initialize(DataProvider provider) {
this.delegate = Objects.requireNonNull(provider);
}
public Data fetchData() {
return Objects.requireNonNull(delegate, "Provider not initialized").fetch();
}
}Вызов fetchData() до initialize() выбросит NullPointerException с понятным сообщением, вместо стандартного NPE на разыменовании null.
Документирование через Javadoc
Использование requireNonNull должно сопровождаться документированием в Javadoc. Тег @throws указывает, что метод выбрасывает NullPointerException при нарушении параметрических ограничений:
/**
* Обрабатывает платеж через указанный шлюз.
*
* @param request запрос на платеж, не может быть null
* @param gateway платежный шлюз, не может быть null
* @return результат обработки платежа
* @throws NullPointerException если request или gateway равны null
*/
public PaymentResult process(PaymentRequest request, PaymentGateway gateway) {
Objects.requireNonNull(request, "PaymentRequest is required");
Objects.requireNonNull(gateway, "PaymentGateway is required");
return gateway.process(request);
}
Для классов, где множество методов выбрасывают NullPointerException при нарушении предусловий, можно документировать это на уровне класса, избегая повторений в каждом методе.
Связь с аннотациями @NonNull
Разделение ответственности: runtime vs compile-time
Objects.requireNonNull() обеспечивает runtime-проверку: исключение возникает во время выполнения, если null передан в метод. Аннотации @NonNull (и их аналоги) предоставляют compile-time информацию о nullability, позволяя IDE и статическим анализаторам предупреждать о потенциальных проблемах до запуска программы.
Эти механизмы комплементарны, а не взаимоисключающие. Аннотации @NonNull документируют контракт и помогают инструментам, но не обеспечивают runtime-защиту. requireNonNull гарантирует защиту во время выполнения, но не предоставляет информации для статического анализа. Идеальный подход — комбинировать оба механизма.
#Java #для_новичков #beginner #exception #requireNonNull
👍3
Экосистема аннотаций nullability
В Java-экосистеме существует множество аннотаций @NonNull из разных источников, каждая со своей семантикой и областью применения:
JSpecify (org.jspecify.annotations.NonNull) — современный стандарт, разработанный консорциумом Google, JetBrains, Spring и других. Применяется к использованию типа (type use), что позволяет различать nullability элементов массивов и generic-типов.
Spring Framework (org.springframework.lang.NonNull) — устаревшие аннотации из Spring 5, deprecated в Spring 7 в пользу JSpecify. Применялись к полям, параметрам и возвращаемым значениям.
JetBrains (org.jetbrains.annotations.NotNull) — аннотации IntelliJ IDEA, широко поддерживаемые IDE и Kotlin-компилятором.
JSR-305 (javax.annotation.Nonnull) — спецификация, больше не поддерживаемая активно, но широко распространенная в legacy-коде.
Jakarta Bean Validation (jakarta.validation.constraints.NotNull) — используется для runtime-валидации в фреймворках вроде Hibernate Validator.
JSpecify: современный стандарт
JSpecify, выпущенный в версии 1.0.0, представляет собой наиболее перспективный стандарт для null safety в Java. Он определяет три состояния nullability: unspecified (не указано), nullable (@Nullable) и non-null (@NonNull). Ключевая особенность — аннотация @NullMarked, применяемая на уровне пакета, которая устанавливает non-null как значение по умолчанию для всех типов в пакете, устраняя необходимость в явном @NonNull для каждого параметра.
После этого все параметры, возвращаемые значения и поля в пакете считаются non-null по умолчанию. Только явно аннотированные @Nullable типы могут содержать null.
Интеграция requireNonNull с аннотациями
Комбинированный подход использует @NonNull (или неявный non-null через @NullMarked) для документирования контракта и статического анализа, и requireNonNull для runtime-защиты:
В этом примере:
IDE и статические анализаторы (NullAway, Checker Framework) предупреждают о попытке передать null в non-null параметры на этапе разработки
requireNonNull гарантирует защиту во время выполнения, если статический анализ был проигнорирован или null пришел из неаннотированного кода
Кастомные сообщения в requireNonNull обеспечивают контекст при runtime-ошибках
#Java #для_новичков #beginner #exception #requireNonNull
В Java-экосистеме существует множество аннотаций @NonNull из разных источников, каждая со своей семантикой и областью применения:
JSpecify (org.jspecify.annotations.NonNull) — современный стандарт, разработанный консорциумом Google, JetBrains, Spring и других. Применяется к использованию типа (type use), что позволяет различать nullability элементов массивов и generic-типов.
Spring Framework (org.springframework.lang.NonNull) — устаревшие аннотации из Spring 5, deprecated в Spring 7 в пользу JSpecify. Применялись к полям, параметрам и возвращаемым значениям.
JetBrains (org.jetbrains.annotations.NotNull) — аннотации IntelliJ IDEA, широко поддерживаемые IDE и Kotlin-компилятором.
JSR-305 (javax.annotation.Nonnull) — спецификация, больше не поддерживаемая активно, но широко распространенная в legacy-коде.
Jakarta Bean Validation (jakarta.validation.constraints.NotNull) — используется для runtime-валидации в фреймворках вроде Hibernate Validator.
JSpecify: современный стандарт
JSpecify, выпущенный в версии 1.0.0, представляет собой наиболее перспективный стандарт для null safety в Java. Он определяет три состояния nullability: unspecified (не указано), nullable (@Nullable) и non-null (@NonNull). Ключевая особенность — аннотация @NullMarked, применяемая на уровне пакета, которая устанавливает non-null как значение по умолчанию для всех типов в пакете, устраняя необходимость в явном @NonNull для каждого параметра.
// package-info.java
@NullMarked
package com.example.service;
import org.jspecify.annotations.NullMarked;
После этого все параметры, возвращаемые значения и поля в пакете считаются non-null по умолчанию. Только явно аннотированные @Nullable типы могут содержать null.
package com.example.service;
import org.jspecify.annotations.Nullable;
public class UserService {
// Не требует @NonNull — non-null по умолчанию благодаря @NullMarked
public User findById(String id) {
// ...
}
// Явно указано, что может вернуть null
public @Nullable User findByEmail(String email) {
// ...
}
// Параметр явно nullable
public void updateNickname(String id, @Nullable String nickname) {
// ...
}
}
Интеграция requireNonNull с аннотациями
Комбинированный подход использует @NonNull (или неявный non-null через @NullMarked) для документирования контракта и статического анализа, и requireNonNull для runtime-защиты:
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class OrderService {
private final PaymentGateway gateway;
public OrderService(PaymentGateway gateway) {
// Runtime-защита: выбросит NPE с сообщением
this.gateway = Objects.requireNonNull(gateway, "PaymentGateway is required");
}
// Метод принимает non-null по умолчанию (благодаря @NullMarked)
public Order processOrder(String orderId) {
Objects.requireNonNull(orderId, "Order ID is required");
return gateway.process(orderId);
}
// Явно nullable параметр
public Order processOrderWithNotes(String orderId, @Nullable String notes) {
Objects.requireNonNull(orderId, "Order ID is required");
// notes может быть null — допустимо по контракту
return gateway.process(orderId, notes);
}
}
В этом примере:
IDE и статические анализаторы (NullAway, Checker Framework) предупреждают о попытке передать null в non-null параметры на этапе разработки
requireNonNull гарантирует защиту во время выполнения, если статический анализ был проигнорирован или null пришел из неаннотированного кода
Кастомные сообщения в requireNonNull обеспечивают контекст при runtime-ошибках
#Java #для_новичков #beginner #exception #requireNonNull
👍3
Раздел 9. Исключения, логирование, отладка
Глава 1. Иерархия исключений (Exceptions)
Практика: Защитное программирование в «Библиотеке»
Подготовка: обновление модели данных
К этому моменту в проекте «Библиотека» должны существовать:
Класс Book с полями id (String или UUID), title, author, year, genres
Класс Library с коллекцией книг и методами поиска
Функциональность импорта/экспорта
Добавьте поле publishedDate типа LocalDate в Book для задачи с парсингом дат.
Часть 1. Автоматическое управление ресурсами
Проблема: ручное закрытие ресурсов
Проблемы: многословно, finally выполняется всегда (даже если ресурс не создан), исключение при close может затереть основное исключение.
Решение: try-with-resources
Ключевые моменты:
Ресурсы в скобках после try должны реализовывать AutoCloseable
Закрываются в обратном порядке создания: buffered, затем reader
Если исключение в try и при закрытии — основное сохраняется, вторичное прицепляется через Suppressed
Можно объявлять ресурс вне try, если он effectively final
Несколько ресурсов
Часть 2. Собственное исключение
Зачем нужно своё исключение
Семантическая ясность: LibraryException говорит о проблеме в предметной области
Единая точка обработки в верхних слоях
Возможность добавить контекст (ID книги, имя файла)
Реализация: unchecked-исключение
Создайте класс LibraryException:
Почему unchecked:
Ошибки библиотеки обычно не восстановимы на месте
Не загромождает сигнатуры методов throws
В Stream API и лямбдах не требует обёртки (в отличие от checked)
Использование в проекте
#Java #для_новичков #beginner #exception #практика
Глава 1. Иерархия исключений (Exceptions)
Практика: Защитное программирование в «Библиотеке»
Подготовка: обновление модели данных
К этому моменту в проекте «Библиотека» должны существовать:
Класс Book с полями id (String или UUID), title, author, year, genres
Класс Library с коллекцией книг и методами поиска
Функциональность импорта/экспорта
Добавьте поле publishedDate типа LocalDate в Book для задачи с парсингом дат.
Часть 1. Автоматическое управление ресурсами
Проблема: ручное закрытие ресурсов
// Плохо: ресурс может остаться открытым при исключении
public List<Book> importFromJsonManual(String filename) {
FileReader reader = null;
try {
reader = new FileReader(filename);
// ... парсинг ...
return parseBooks(reader);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (reader != null) {
try {
reader.close(); // Может бросить исключение, подавляя основное
} catch (IOException e) {
// игнорируем
}
}
}
}
Проблемы: многословно, finally выполняется всегда (даже если ресурс не создан), исключение при close может затереть основное исключение.
Решение: try-with-resources
// Хорошо: автоматическое закрытие, правильная обработка исключений
public List<Book> importFromJson(String filename) {
try (FileReader reader = new FileReader(filename);
BufferedReader buffered = new BufferedReader(reader)) {
StringBuilder json = new StringBuilder();
String line;
while ((line = buffered.readLine()) != null) {
json.append(line);
}
return parseBooks(json.toString());
} catch (IOException e) {
throw new LibraryException("Не удалось прочитать файл: " + filename, e);
}
}
Ключевые моменты:
Ресурсы в скобках после try должны реализовывать AutoCloseable
Закрываются в обратном порядке создания: buffered, затем reader
Если исключение в try и при закрытии — основное сохраняется, вторичное прицепляется через Suppressed
Можно объявлять ресурс вне try, если он effectively final
Несколько ресурсов
public void exportToJson(String inputFilename, String outputFilename) {
try (BufferedReader reader = Files.newBufferedReader(Path.of(inputFilename));
BufferedWriter writer = Files.newBufferedWriter(Path.of(outputFilename))) {
String json = reader.readLine();
List<Book> books = parseBooks(json);
String formatted = formatBooks(books);
writer.write(formatted);
} catch (IOException e) {
throw new LibraryException("Ошибка при копировании данных", e);
}
}Часть 2. Собственное исключение
Зачем нужно своё исключение
Семантическая ясность: LibraryException говорит о проблеме в предметной области
Единая точка обработки в верхних слоях
Возможность добавить контекст (ID книги, имя файла)
Реализация: unchecked-исключение
Создайте класс LibraryException:
public class LibraryException extends RuntimeException {
public LibraryException(String message) {
super(message);
}
public LibraryException(String message, Throwable cause) {
super(message, cause);
}
public LibraryException(String message, String bookId, Throwable cause) {
super(message + " [книга: " + bookId + "]", cause);
}
}Почему unchecked:
Ошибки библиотеки обычно не восстановимы на месте
Не загромождает сигнатуры методов throws
В Stream API и лямбдах не требует обёртки (в отличие от checked)
Использование в проекте
public Book findBookOrThrow(String bookId) {
return findById(bookId)
.orElseThrow(() -> new LibraryException("Книга не найдена", bookId, null));
}#Java #для_новичков #beginner #exception #практика
👍6
Часть 3. Многоцелевой перехват
Сценарий: разные I/O-ошибки — одинаковая реакция
Правила multi-catch:
Типы должны быть несовместимыми (нет иерархии)
Переменная e effectively final внутри блока
Нельзя вызвать метод, который есть не во всех типах (без приведения)
Часть 4. Checked-исключения в Stream API
Проблема: Stream.map() не принимает throws
Уточнение: DateTimeParseException на самом деле unchecked (наследник DateTimeException). Возьмём более показательный пример с реальным checked-исключением — чтение из файла внутри stream:
Решение 1: обёртка внутри лямбды
Недостаток: загромождает код, повторяется в каждой лямбде.
Решение 2: вспомогательный метод-обёртка
Использование:
Решение 3: специализированный обёртчик для Optional
Часть 5. Защитные проверки аргументов
Objects.requireNonNull
#Java #для_новичков #beginner #exception #практика
Сценарий: разные I/O-ошибки — одинаковая реакция
public List<Book> loadFromMultipleSources(String jsonPath, String csvPath) {
List<Book> result = new ArrayList<>();
try {
result.addAll(loadJson(jsonPath));
} catch (FileNotFoundException | NoSuchFileException e) {
// Файла нет — не критично, продолжаем с CSV
System.err.println("JSON не найден, пробуем CSV: " + e.getMessage());
} catch (IOException e) {
throw new LibraryException("Ошибка чтения JSON", e);
}
try {
result.addAll(loadCsv(csvPath));
} catch (FileNotFoundException | NoSuchFileException e) {
System.err.println("CSV не найден: " + e.getMessage());
} catch (IOException e) {
throw new LibraryException("Ошибка чтения CSV", e);
}
if (result.isEmpty()) {
throw new LibraryException("Не удалось загрузить книги ни из одного источника");
}
return result;
}Правила multi-catch:
Типы должны быть несовместимыми (нет иерархии)
Переменная e effectively final внутри блока
Нельзя вызвать метод, который есть не во всех типах (без приведения)
Часть 4. Checked-исключения в Stream API
Проблема: Stream.map() не принимает throws
// Не компилируется: parseDate бросает checked DateTimeParseException
public List<LocalDate> parsePublicationDates(List<String> dateStrings) {
return dateStrings.stream()
.map(this::parseDate) // Ошибка: unreported exception
.collect(Collectors.toList());
}
private LocalDate parseDate(String s) throws DateTimeParseException {
return LocalDate.parse(s); // checked в Java 8, но...
}
Уточнение: DateTimeParseException на самом деле unchecked (наследник DateTimeException). Возьмём более показательный пример с реальным checked-исключением — чтение из файла внутри stream:
// Реальная проблема: Files.readString бросает IOException
public List<String> loadDescriptions(List<Path> paths) {
return paths.stream()
.map(Files::readString) // Ошибка: IOException не обработана
.collect(Collectors.toList());
}
Решение 1: обёртка внутри лямбды
public List<String> loadDescriptionsWrapped(List<Path> paths) {
return paths.stream()
.map(path -> {
try {
return Files.readString(path);
} catch (IOException e) {
throw new LibraryException("Не удалось прочитать: " + path, e);
}
})
.collect(Collectors.toList());
}Недостаток: загромождает код, повторяется в каждой лямбде.
Решение 2: вспомогательный метод-обёртка
// Утилитный метод для оборачивания checked в unchecked
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> throwing) {
return t -> {
try {
return throwing.apply(t);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new LibraryException("Ошибка в потоковой операции", e);
}
};
}
Использование:
public List<String> loadDescriptionsClean(List<Path> paths) {
return paths.stream()
.map(wrap(Files::readString))
.collect(Collectors.toList());
}Решение 3: специализированный обёртчик для Optional
public Optional<Book> parseBookFromJson(String json) {
return Optional.ofNullable(json)
.map(wrap(this::parseBook))
.orElseThrow(() -> new LibraryException("Пустой JSON"));
}
private Book parseBook(String json) throws JsonParseException {
// парсинг...
}Часть 5. Защитные проверки аргументов
Objects.requireNonNull
public void addBook(Book book) {
Objects.requireNonNull(book, "Книга не может быть null");
Objects.requireNonNull(book.getTitle(), "Название не может быть null");
if (book.getYear() < 1450 || book.getYear() > Year.now().getValue() + 1) {
throw new IllegalArgumentException("Недопустимый год: " + book.getYear());
}
books.add(book);
}#Java #для_новичков #beginner #exception #практика
👍5
Преимущества:
Стандартный метод — читается как утверждение
Сообщение об ошибке встроено в исключение
Компактнее ручной проверки if (x == null) throw ...
Вариант с кастомным сообщением
Часть 6. Optional и явные исключения
Поиск с гарантией результата
Альтернативы:
Цепочка с промежуточными проверками
Интеграция в проект «Библиотека»
Обновлённый класс Library с защитными приёмами
Практические задания
Задача 1: try-with-resources для базы данных
Предположим, что Library теперь использует Connection для JDBC. Обёрните операции в try-with-resources. Обработайте SQLException через LibraryException.
Задача 2: иерархия исключений
Расширьте LibraryException:
BookNotFoundException — для findById
DuplicateBookException — для addBook
ImportException — для операций импорта
Все наследуют LibraryException. Покажите multi-catch для разных типов импорта.
Задача 3: обёртка для Stream с результатом
Создайте метод parseDatesWithFallback:
Задача 4: защита от null во всём API
Проверьте все публичные методы Library. Добавьте Objects.requireNonNull где аргументы не должны быть null. Документируйте в Javadoc, какие методы принимают null (например, findById(null) → LibraryException vs orElse(null)).
#Java #для_новичков #beginner #exception #практика
Стандартный метод — читается как утверждение
Сообщение об ошибке встроено в исключение
Компактнее ручной проверки if (x == null) throw ...
Вариант с кастомным сообщением
public Book findById(String id) {
Objects.requireNonNull(id, () -> "ID книги не может быть null, доступные: " + listIds());
// ленивое вычисление сообщения — вызывается только при ошибке
return internalFind(id);
}Часть 6. Optional и явные исключения
Поиск с гарантией результата
public Book getBook(String bookId) {
return findById(bookId)
.orElseThrow(() -> new LibraryException("Книга не найдена", bookId, null));
}Альтернативы:
Цепочка с промежуточными проверками
public LocalDate getPublicationDate(String bookId) {
return findById(bookId)
.map(Book::getPublishedDate)
.filter(Objects::nonNull)
.orElseThrow(() -> new LibraryException("Дата неизвестна", bookId, null));
}Интеграция в проект «Библиотека»
Обновлённый класс Library с защитными приёмами
public class Library {
private final List<Book> books = new ArrayList<>();
public void addBook(Book book) {
Objects.requireNonNull(book, "Книга не может быть null");
Objects.requireNonNull(book.getId(), "ID книги не может быть null");
if (findById(book.getId()).isPresent()) {
throw new LibraryException("Книга с ID " + book.getId() + " уже существует");
}
books.add(book);
}
public Book getBook(String id) {
Objects.requireNonNull(id, "ID не может быть null");
return findById(id)
.orElseThrow(() -> new LibraryException("Книга не найдена", id, null));
}
public List<Book> importFromJson(String filename) {
Objects.requireNonNull(filename, "Имя файла не может быть null");
try (BufferedReader reader = Files.newBufferedReader(Path.of(filename))) {
String json = reader.lines().collect(Collectors.joining());
return parseBooks(json);
} catch (FileNotFoundException | NoSuchFileException e) {
throw new LibraryException("Файл не найден: " + filename, e);
} catch (IOException e) {
throw new LibraryException("Ошибка чтения файла: " + filename, e);
}
}
public List<Book> importFromMultipleSources(List<String> filenames) {
Objects.requireNonNull(filenames, "Список файлов не может быть null");
return filenames.stream()
.filter(Objects::nonNull)
.map(wrap(this::importFromJson))
.flatMap(List::stream)
.collect(Collectors.toList());
}
private Optional<Book> findById(String id) {
return books.stream()
.filter(b -> b.getId().equals(id))
.findFirst();
}
private List<Book> parseBooks(String json) {
// реализация парсинга
}
}Практические задания
Задача 1: try-with-resources для базы данных
Предположим, что Library теперь использует Connection для JDBC. Обёрните операции в try-with-resources. Обработайте SQLException через LibraryException.
Задача 2: иерархия исключений
Расширьте LibraryException:
BookNotFoundException — для findById
DuplicateBookException — для addBook
ImportException — для операций импорта
Все наследуют LibraryException. Покажите multi-catch для разных типов импорта.
Задача 3: обёртка для Stream с результатом
Создайте метод parseDatesWithFallback:
public List<LocalDate> parseDatesWithFallback(List<String> inputs) {
// Парсит даты, при ошибке возвращает LocalDate.MIN вместо исключения
// Используйте обёртку, возвращающую Optional<LocalDate>
}Задача 4: защита от null во всём API
Проверьте все публичные методы Library. Добавьте Objects.requireNonNull где аргументы не должны быть null. Документируйте в Javadoc, какие методы принимают null (например, findById(null) → LibraryException vs orElse(null)).
#Java #для_новичков #beginner #exception #практика
👍7