Объясните механизм: почему параметр multi-catch нельзя переназначить в этом фрагменте?
import java.io.IOException;
class Demo {
static void read(boolean fail) throws IOException {
if (fail) throw new IOException();
throw new IllegalStateException();
}
static void run() {
try {
read(true);
} catch (IOException | IllegalStateException e) {
e = new IllegalStateException(e);
}
}
}
Параметр обработчика multi-catch неявно объявлен как final, поэтому присваивание e = ... запрещено компилятором. Это ограничение действует именно для параметра catch (A | B e) и позволяет сохранять точную информацию о возможных типах исключения при анализе кода, включая механизм точного повторного выброса.
Ограничение относится только к переназначению ссылки. Сам объект исключения не становится неизменяемым: его методы можно вызывать, а исходное исключение можно передать как причину новому исключению.
Синтаксис multi-catch появился в Java 7. До него одинаковую обработку нескольких независимых типов приходилось дублировать в нескольких блоках catch либо перехватывать их общим предком.
Multi-catch уменьшил дублирование и сохранил информацию о каждом варианте исключения. Для этого Java определяет специальное правило: его параметр является не обычной переназначаемой локальной переменной, а неявно финальной переменной обработчика.
В примере методы read могут завершиться IOException или IllegalStateException, а обработка для обоих вариантов одинакова. Переназначение e попыталось бы заменить исходную причину другим объектом исключения.
Если бы такое переназначение разрешалось без специальных правил, после него было бы сложнее определить, какое исключение фактически поступило в обработчик и какие типы допустимо повторно выбросить. Кроме того, замена ссылки могла бы скрыть исходную причину ошибки.
Каждая альтернатива multi-catch должна быть независимой, а параметр e имеет специальный составной тип, представляющий допустимые варианты. В обычных выражениях доступны только операции, общие для типов альтернатив, а анализ потока управления сохраняет сведения о самих альтернативах.
Параметр такого обработчика неявно final. Поэтому следующий код не компилируется:
Правильный способ добавить контекст — создать новый объект, передав исходное исключение в качестве причины:
Здесь e не заменяется. Новый объект содержит исходное исключение в цепочке причин, поэтому диагностика сохраняет исходный тип и стек вызовов.
Это отличается от обычного обработчика:
Параметр обычного catch можно переназначить, если он не объявлен final. Однако такое присваивание влияет на анализ повторного throw: компилятор уже не может считать, что выбрасывается первоначальное исключение, и обычно применяет более широкий статический тип параметра.
Если требуется просто повторно выбросить исходную ошибку, параметр multi-catch можно передать в throw без присваивания. Проверяемые альтернативы должны быть учтены в сигнатуре метода, а непроверяемые исключения объявлять не требуется.
Главный компромисс — небольшая негибкость параметра взамен более точного статического анализа и сохранения исходной причины. Если для разных типов требуется изменить тип или сообщение по-разному, следует использовать отдельные блоки catch, а не пытаться переназначить общий параметр.
HTTP-клиент может выбросить IOException при проблеме сети или IllegalStateException при нарушении состояния соединения. Для обоих случаев сервису нужно вернуть единое доменное исключение ExternalServiceException.
Первый вариант — два одинаковых блока catch. Он явно показывает каждый тип, но дублирует код и со временем может привести к расхождению логирования или формирования сообщения.
Второй вариант — catch (Exception e). Он короче, но перехватывает также неожиданные исключения, например ошибку программирования, и может скрыть дефект под видом штатной ошибки внешнего сервиса.
Выбранный вариант — multi-catch с созданием нового исключения:
Он ограничивает набор перехватываемых типов, не дублирует обработку и сохраняет исходное исключение как причину. Переназначение параметра здесь было бы хуже: оно удалило бы прямую ссылку на первоначальный объект, если новый объект не сохранить отдельно.
Да. Запрет касается только присваивания новой ссылки переменной e. Вызов методов объекта или передача e в конструктор другого исключения разрешены, хотя изменение состояния самого исключения обычно не рекомендуется из-за ухудшения диагностируемости и потокобезопасности.
throws?Для непроверяемых исключений объявление не требуется. Для проверяемых исключений компилятор анализирует альтернативы и требует объявить те из них, которые не обработаны окончательно. Например, при IOException | SQLException метод должен объявить оба типа либо их допустимый общий контракт; одно лишь наличие multi-catch не превращает проверяемое исключение в непроверяемое.
catch (Exception e) при обращении к параметру?В catch (Exception e) статический тип параметра — Exception, поэтому доступны члены этого типа, а переназначение разрешено, если параметр не объявлен final. В multi-catch параметр представляет объединение альтернатив: доступны операции, корректные для всех вариантов, сам параметр не переназначается, а компилятор дополнительно использует сведения об исходных типах для проверки повторного выброса.