Разберите последствия: какое исключение увидит вызывающий код, если тело try выбросило исключение, а блок finally завершился другим исключением?
Вызывающий код увидит исключение из finally. Исключение, возникшее в try, будет потеряно как основное, если его явно не сохранить или не связать с новым исключением.
Конструкция try–finally предназначена для гарантированного выполнения завершающих действий: освобождения блокировок, закрытия ресурсов и восстановления состояния объекта. Она отделяет основную операцию от cleanup-логики, которая должна выполняться как при штатном завершении, так и при исключении.
Однако finally выполняется во время уже начатого распространения исключения. Поэтому новое исключение из этого блока должно получить приоритет в обычной модели Java, даже если до него существовало другое исключение.
Представим, что операция чтения завершилась ошибкой, а закрытие ресурса также не удалось. Если закрытие выполняется вручную в finally, ошибка закрытия заменит исходную ошибку чтения.
Это опасно, потому что исходная причина может быть наиболее важной для диагностики. Кроме того, вызывающий код может перехватить неожидаемый тип исключения или ошибочно решить, что отказ произошёл только на этапе очистки.
При выходе из try Java сначала запоминает исключение или результат управления. Затем выполняется finally. Если finally завершается нормально, сохранённое исключение продолжает распространяться. Если же finally выбрасывает новое исключение, именно оно становится результатом всего оператора.
Минимальный пример:
В этом примере вызывающий код получит IllegalArgumentException. Объект IllegalStateException не будет автоматически добавлен в список подавленных исключений и не станет причиной нового исключения.
Проблема возникает не только при явном throw. Команда return в finally также может подавить исключение из try, заменив его обычным результатом. Поэтому return, break и continue в finally считаются опасными: они скрывают как ошибки, так и исходное управление потоком.
Если очистка может завершиться ошибкой, безопаснее использовать try-with-resources. Этот механизм при наличии исключения из тела сохраняет ошибку очистки как подавленное исключение, а основной ошибкой оставляет исключение из тела try. При ручном управлении ресурсом такую связь нужно создавать самостоятельно, например через причинное исключение или механизм подавленных исключений.
Главное ограничение: finally не является абсолютной гарантией выполнения. Он обычно выполняется при выходе из try, но процесс может завершиться аварийно, например из-за остановки виртуальной машины или внешнего принудительного завершения процесса.
В сервисе вручную закрывали сетевое соединение в finally. При ошибке запроса соединение иногда также выбрасывало исключение при закрытии. В результате в журнале фиксировалась только ошибка закрытия, а исходная причина — например, ответ с некорректным форматом — исчезала.
Рассматривались три варианта. Оставить ручной finally было просто, но это приводило к потере причины. Подавить ошибку закрытия полностью сохраняло основную ошибку, но скрывало полезный диагностический сигнал. Перейти на try-with-resources требовало изменить структуру метода, зато обеспечивало стандартное сохранение основной ошибки и доступ к ошибке очистки через getSuppressed().
Выбрали try-with-resources. В результате вызывающий код стал получать исходную ошибку операции, а дополнительная ошибка закрытия сохранялась для диагностики без подмены основной причины.
Что произойдёт, если finally содержит return, а try выбросил исключение?
Исключение будет подавлено, а метод вернёт значение из finally. Это относится и к исключению, и к другим формам управления потоком из try: завершающий переход из finally получает приоритет. Такой код особенно опасен тем, что внешне выглядит как корректная обработка ресурса, но скрывает реальные сбои.
Станет ли исключение из try подавленным автоматически, если новое исключение выброшено из обычного finally?
Нет. Автоматическое добавление подавленного исключения характерно для механизма try-with-resources, а не для произвольного try–finally. При обычном finally исходное исключение может быть полностью потеряно, если разработчик сам не сохранит его и не добавит к новому исключению.
Как сохранить ошибку основной операции, если ручной finally всё же необходим?
Нужно явно разделить основное исключение и ошибку очистки: сначала сохранить основное исключение, затем выполнить очистку, а при её сбое добавить ошибку очистки как подавленную к основной. Если основной ошибки не было, ошибку очистки можно выбросить как самостоятельную. На практике предпочтительнее try-with-resources, поскольку он делает это управление состояниями стандартным и менее подверженным ошибкам.