Исключения
Проверяемые и непроверяемые исключения, throw и throws, порядок catch и multi-catch, цепочки причин, try-with-resources и подавленные исключения.
15 вопросов
JuniorТеорияОчень частоКак связаны Throwable, Error, Exception и RuntimeException в Java?
Как связаны Throwable, Error, Exception и RuntimeException в Java?
Throwable — корень всего, что можно бросить или поймать. У него два прямых подкласса: Error, сигналящий о серьёзных сбоях уровня JVM вроде OutOfMemoryError, которые ловить не следует, и Exception, моделирующий проблемы приложения. Собственный подкласс Exception — RuntimeException — непроверяемый, а любое другое Exception проверяемое.
Типичные ошибки
- ✗Считать
ErrorобычнымException, который надо ловить и восстанавливаться после него - ✗Ставить
RuntimeExceptionвышеException, а не ниже него в иерархии - ✗Полагать, что каждый подкласс
Exceptionпроверяемый, забывая, чтоRuntimeExceptionнепроверяемый
Уточняющие вопросы
- →От какого одного класса нужно наследоваться, чтобы создать свой бросаемый тип?
- →Почему перехват
Throwableрискует проглотитьOutOfMemoryError?
JuniorТеорияОчень частоВ чём разница между throw и throws в Java?
В чём разница между throw и throws в Java?
throw — это оператор в теле метода, который прямо сейчас возбуждает один конкретный экземпляр исключения, например throw new IOException(). throws — это часть сигнатуры метода, объявляющая, какие проверяемые исключения метод может пробрасывать вызывающему, чтобы тот знал, что их нужно обработать или переобъявить. Один действует, другой объявляет.
Типичные ошибки
- ✗Писать
throwsв теле метода илиthrowв сигнатуре, путая оператор и часть объявления - ✗Считать, что
throwможет возбудить несколько экземпляров исключения одним оператором - ✗Думать, что
throwsнужен для непроверяемых исключений вродеRuntimeException
Уточняющие вопросы
- →Нужна ли методу
throwsдля непроверяемых исключений, которые он может возбудить? - →Может ли
throwsобъявлять надтип вродеExceptionвместо конкретного подкласса?
MiddleТеорияОчень частоВ чём разница между проверяемыми и непроверяемыми исключениями в Java?
В чём разница между проверяемыми и непроверяемыми исключениями в Java?
Проверяемые исключения — подклассы Exception, кроме RuntimeException; компилятор требует от вызывающего либо обработать их, либо объявить через throws, поэтому они моделируют восстановимые ситуации вроде отсутствующего файла. Непроверяемые — подклассы RuntimeException и Error — не требуют объявления и обычно сигналят об ошибках в коде или фатальных условиях, восстановление от которых не предполагается.
Типичные ошибки
- ✗Думать, что
ErrorиRuntimeExceptionпроверяемые и требуютthrows - ✗Считать, что деление на проверяемые/непроверяемые решается во время выполнения, а не иерархией классов
- ✗Ловить широкое проверяемое
Exceptionлишь чтобы заглушить компилятор, а не обработать
Уточняющие вопросы
- →Почему
RuntimeExceptionиErrorоставлены непроверяемыми разработчиками языка? - →Когда оправдано обернуть проверяемое исключение в непроверяемое и перебросить?
JuniorКодЧастоРасставьте блоки catch, чтобы обработать и отсутствующий файл, и общий сбой ввода-вывода
Расставьте блоки catch, чтобы обработать и отсутствующий файл, и общий сбой ввода-вывода
Java просматривает блоки catch сверху вниз и входит в первый, чей тип совместим с брошенным исключением, поэтому подкласс должен идти первым: сначала catch (FileNotFoundException e) и только потом catch (IOException e). В обратном порядке широкий блок уже обрабатывает подкласс, узкий становится недостижимым, и компилятор его прямо отвергает.
Типичные ошибки
- ✗Ставить
catch (Exception e)первым, а более узкий блок ниже него - ✗Считать, что блоки
catchподбираются по лучшему совпадению, как перегрузка - ✗Думать, что недостижимый
catch— предупреждение, а не ошибка компиляции
Уточняющие вопросы
- →Что меняется, если типы исключений не связаны наследованием, а независимы?
- →Как multi-catch взаимодействует с этими правилами порядка?
JuniorКодЧастоОткройте два ресурса в одном try-with-resources и определите порядок их закрытия
Откройте два ресурса в одном try-with-resources и определите порядок их закрытия
Объявите оба ресурса в одном заголовке try через точку с запятой. Закрываются они в порядке, обратном объявлению, поэтому программа печатает open A, open B, close B, close A. Обратный порядок гарантирован языком: поздний ресурс часто построен поверх раннего. Каждый объявленный ресурс закрывается, даже если предыдущий close() бросил исключение, — этот сбой не теряется, а записывается в пробрасываемое исключение как подавленный.
Типичные ошибки
- ✗Ждать закрытия ресурсов в порядке объявления, а не в обратном
- ✗Считать, что упавший
close()одного ресурса оставит остальные незакрытыми - ✗Добавлять
finallyс ручнымclose()поверх try-with-resources
Уточняющие вопросы
- →Почему язык закрывает поздний ресурс раньше, а не ранний?
- →Куда девается сбой
close(), если телоtryуже бросило исключение?
JuniorТеорияЧастоЧто делает try-with-resources и как он управляет очисткой ресурсов?
Что делает try-with-resources и как он управляет очисткой ресурсов?
Он объявляет один или несколько ресурсов в скобках после try, и JVM закрывает их автоматически в конце блока — в порядке, обратном открытию — вызывая close() на каждом. Ресурс должен реализовывать AutoCloseable. Закрытие происходит и при нормальном выходе, и при исключении, поэтому finally для освобождения файлов или потоков не нужен. Если и тело, и close() бросают исключение, сбой close() становится подавленным исключением.
Типичные ошибки
- ✗Считать, что ресурсу не нужен интерфейс, хотя он должен реализовывать
AutoCloseable(илиCloseable) - ✗Думать, что ресурсы закрываются в порядке открытия, а не в обратном
- ✗Полагать, что сбой
close()перезаписывает исключение тела, а не подавляется им
Уточняющие вопросы
- →Какой интерфейс должен реализовывать ресурс, чтобы использоваться в try-with-resources?
- →Что такое подавленное исключение и как его получить?
MiddleКодЧастоПеребросьте SQLException как доменное исключение, не потеряв исходную причину
Перебросьте SQLException как доменное исключение, не потеряв исходную причину
Дайте своему типу конструктор (String, Throwable), который передаёт аргументы в super(message, cause), и перебросьте через throw new UserLoadException(message, e);. Сам Throwable хранит эту причину, поэтому getCause() вернёт SQLException, а printStackTrace напечатает его кадры под Caused by:. Передача одного лишь e.getMessage() в конструктор только с сообщением сохраняет текст и молча выбрасывает исходный стектрейс.
Типичные ошибки
- ✗Перебрасывать только с
e.getMessage(), теряя исходный стектрейс - ✗Считать, что причина связывается сама, раз
throwстоит внутриcatch - ✗Путать
addSuppressedсinitCause— подавление это не цепочка
Уточняющие вопросы
- →Когда стоит применить
initCause()вместо конструктора, принимающего причину? - →Насколько глубокой бывает цепочка причин и как обойти её программно?
MiddleТеорияЧастоЧто такое подавленное исключение в try-with-resources и как его прочитать?
Что такое подавленное исключение в try-with-resources и как его прочитать?
Если тело try бросило исключение и при раскрутке блока close() тоже бросил, наружу уходит исключение тела, а сбой close() прикрепляется к нему через addSuppressed(), а не подменяет его. Эти второстепенные сбои читаются обратно методом Throwable.getSuppressed(), а printStackTrace печатает их под заголовком Suppressed:. У рукописного try/finally обратная беда: исключение из finally подменяет исходное, и настоящая причина теряется.
Типичные ошибки
- ✗Думать, что побеждает сбой
close(), а подавленным становится исключение тела - ✗Считать, что сбой
close()молча выбрасывается, а не прикрепляется - ✗Путать
getSuppressed()сgetCause()— подавление это не цепочка причин
Уточняющие вопросы
- →Как рукописный
try/finallyможет полностью потерять исходное исключение? - →Можно ли вызывать
addSuppressed()самому вне try-with-resources и когда это полезно?
JuniorТеорияИногдаЧто делает multi-catch и какие ограничения он накладывает?
Что делает multi-catch и какие ограничения он накладывает?
Один блок catch может перечислить несколько типов исключений через | — catch (IOException | SQLException e), — чтобы общее тело обработчика было написано один раз, а не скопировано под каждый тип. Перечисленные типы не должны быть связаны наследованием друг с другом: catch (IOException | FileNotFoundException e) — ошибка компиляции. Параметр неявно final, а его статический тип — ближайший общий надтип перечисленных вариантов.
Типичные ошибки
- ✗Перечислять тип и его же подтип двумя вариантами в одном блоке
- ✗Пытаться присвоить новое значение параметру multi-catch, который неявно
final - ✗Ждать от параметра динамического типа вместо общего надтипа
Уточняющие вопросы
- →Какой статический тип у
eвcatch (IOException | SQLException e)? - →Как точный проброс позволяет методу объявить типы уже, чем
Exception?
SeniorТеорияИногдаСтоит ли когда-нибудь ловить Error и что при этом ломается?
Стоит ли когда-нибудь ловить Error и что при этом ломается?
Практически никогда. Error помечает сбой уровня JVM, от которого приложение не предполагает восстанавливаться: после OutOfMemoryError или StackOverflowError куча или стек уже повреждены, поэтому обработчик, который продолжает работу, опирается на состояние, которому нельзя доверять. Узкие законные случаи — обработчик верхнего уровня, который логирует и завершает процесс, и фреймворк, изолирующий одну задачу. Настоящая опасность — catch (Throwable t) в обычном коде: он молча глотает любой Error.
Типичные ошибки
- ✗Считать
OutOfMemoryErrorвосстановимым условием, повторяемым после сброса кэша - ✗Писать
catch (Throwable t)ради широкого перехвата, глотая заодно любойError - ✗Полагать, что
Errorзаперт в своём потоке и оставляет остальную JVM здоровой
Уточняющие вопросы
- →Почему
catch (Throwable t)опаснееcatch (Exception e)в обработчике запроса? - →Что должен делать пул потоков, когда рабочая задача умирает с
OutOfMemoryError?
SeniorДизайнИногдаВы проводите разбор дизайна исключений в трёхслойном сервисе — JDBC-репозиторий, доменный сервис и REST-контроллер. Сейчас репозиторий выпускает наружу сырое SQLException, сервис ловит его и перебрасывает new RuntimeException(e.getMessage()), а контроллер отображает всё пойманное в HTTP 500. Продукт теперь требует 404 для отсутствующей сущности, 409 для нарушения уникальности и 500 только для настоящих отказов, а поддержке нужна исходная ошибка базы, видимая в логах. Ограничения: новых библиотек добавлять нельзя, репозиторий не должен ничего знать про HTTP, а число типов исключений обязано остаться таким, чтобы команда его помнила. Предложите иерархию исключений и правило перевода на каждой границе и скажите, что слой обязан делать со сбоем, который сам обработать не может.
Вы проводите разбор дизайна исключений в трёхслойном сервисе — JDBC-репозиторий, доменный сервис и REST-контроллер. Сейчас репозиторий выпускает наружу сырое SQLException, сервис ловит его и перебрасывает new RuntimeException(e.getMessage()), а контроллер отображает всё пойманное в HTTP 500. Продукт теперь требует 404 для отсутствующей сущности, 409 для нарушения уникальности и 500 только для настоящих отказов, а поддержке нужна исходная ошибка базы, видимая в логах. Ограничения: новых библиотек добавлять нельзя, репозиторий не должен ничего знать про HTTP, а число типов исключений обязано остаться таким, чтобы команда его помнила. Предложите иерархию исключений и правило перевода на каждой границе и скажите, что слой обязан делать со сбоем, который сам обработать не может.
Дайте домену небольшую непроверяемую иерархию — AppException с NotFoundException и ConflictException — и переводите на каждой границе. Репозиторий ловит SQLException и перебрасывает доменный тип с оригиналом в качестве причины, поэтому ошибка базы всё равно доходит до лога под Caused by:. Контроллер отображает доменные типы в 404/409/500 и никогда не видит SQLException, и HTTP не протекает в нижние слои. Слой, который не может ничего сделать со сбоем, пропускает его дальше, а не глотает.
Типичные ошибки
- ✗Пропускать
SQLExceptionдо контроллера, привязывая веб-слой к драйверу - ✗Перебрасывать только с
e.getMessage(), из-за чего исходный сбой SQL исчезает из лога - ✗Ловить, логировать и глотать на каждом слое, лишая вызывающего возможности отреагировать
Уточняющие вопросы
- →Должны ли доменные исключения здесь быть проверяемыми или непроверяемыми и что это решает?
- →Где вы разместите единственное место, превращающее доменное исключение в HTTP-ответ?
SeniorДизайнРедкоКоманда фиксирует политику исключений для новой публичной Java-библиотеки. Один лагерь хочет объявить каждый восстановимый сбой проверяемым исключением, доказывая, что тогда компилятор заставит каждого вызывающего его признать. Другой лагерь напоминает, что ни один популярный язык, спроектированный после Java, эту идею не перенял — Kotlin, C# и Scala оставляют все исключения непроверяемыми — и хочет так же. Ограничения: методы библиотеки регулярно вызываются из лямбд, переданных в операции Stream; как минимум три внутренних фреймворка оборачивают библиотеку и переиздают её сбои наружу; новая минорная версия выходит ежемесячно и обязана оставаться совместимой с предыдущей на уровне исходников. Приведите доводы обеих сторон именно в этой обстановке, назовите конкретные режимы отказа, которые порождает каждый выбор, и сформулируйте политику, которую примете, и где именно проведёте границу.
Команда фиксирует политику исключений для новой публичной Java-библиотеки. Один лагерь хочет объявить каждый восстановимый сбой проверяемым исключением, доказывая, что тогда компилятор заставит каждого вызывающего его признать. Другой лагерь напоминает, что ни один популярный язык, спроектированный после Java, эту идею не перенял — Kotlin, C# и Scala оставляют все исключения непроверяемыми — и хочет так же. Ограничения: методы библиотеки регулярно вызываются из лямбд, переданных в операции Stream; как минимум три внутренних фреймворка оборачивают библиотеку и переиздают её сбои наружу; новая минорная версия выходит ежемесячно и обязана оставаться совместимой с предыдущей на уровне исходников. Приведите доводы обеих сторон именно в этой обстановке, назовите конкретные режимы отказа, которые порождает каждый выбор, и сформулируйте политику, которую примете, и где именно проведёте границу.
Проверяемые исключения дают контракт, проверяемый компилятором, но они не композируются: проверяемый тип заражает сигнатуру каждого вызывающего, не может покинуть лямбду в Stream.map (функциональные интерфейсы не объявляют throws), а добавление такого типа в минорном релизе ломает совместимость исходников. Это давление и порождает два антипаттерна — пустой catch (Exception e) и огульный throws Exception. Политика: по умолчанию непроверяемые, проверяемые — лишь там, где вызывающий реально может восстановиться.
Типичные ошибки
- ✗Утверждать, что проверяемые исключения дороже в рантайме, — деление чисто компиляторное
- ✗Забывать, что
Function,Supplierи прочие не объявляютthrows, поэтому проверяемый тип не выйдет из лямбды - ✗Считать добавление проверяемого исключения совместимым изменением, хотя оно ломает всех вызывающих
Уточняющие вопросы
- →Как библиотеки всё же делают проверяемое исключение пригодным внутри конвейера
Stream? - →Какие сбои этой библиотеки всё-таки оправдают проверяемый тип по вашей политике?
SeniorТеорияРедкоПочему использовать исключения для обычного потока управления дорого?
Почему использовать исключения для обычного потока управления дорого?
Основная стоимость — fillInStackTrace, выполняемый при конструировании исключения: он обходит стек вызовов, чтобы захватить каждый кадр, что куда дороже ветвления. Поэтому замена if-проверки на null на перехват NullPointerException работает на порядки медленнее под нагрузкой и затемняет намерение. Если трейс реально не нужен, можно переопределить fillInStackTrace или использовать -XX:-StackTraceInThrowable, но настоящее решение — проверять условия явно.
Типичные ошибки
- ✗Приписывать стоимость блоку
try, а не захвату стектрейса при throw - ✗Считать, что стектрейс захватывается лениво, а не при конструировании
- ✗Считать исключения бесплатной идиоматичной заменой явной проверки условия
Уточняющие вопросы
- →Как переопределение
fillInStackTraceменяет стоимость часто бросаемого исключения? - →Когда заранее созданное синглтон-исключение (без трейса) — оправданная оптимизация?
SeniorТеорияРедкоЧем был метод finalize() и почему он не рекомендуется в современной Java?
Чем был метод finalize() и почему он не рекомендуется в современной Java?
finalize() был хуком в Object, который сборщик мусора вызывал перед освобождением объекта для последней очистки. Он не рекомендуется, потому что сборка мусора запускает его недетерминированно — возможно, никогда, — замедляет сборку, может воскресить объект, утекая this, и скрывает выброшенные им исключения. С Java 9 он устарел и заменён try-with-resources для детерминированного освобождения и Cleaner для подстраховки.
Типичные ошибки
- ✗Считать
finalize()детерминированной очисткой как деструктор C++, срабатывающий в известный момент - ✗Освобождать файлы или сокеты в
finalize()вместо try-with-resources илиCleaner - ✗Полагать, что
finalize()всегда вызывается; сборка мусора может не запустить его до выхода
Уточняющие вопросы
- →Как воскрешение объекта в
finalize()нарушает жизненный цикл сборщика мусора? - →Как
Cleanerизбегает воскрешения и сокрытия исключений, свойственныхfinalize()?
SeniorДизайнРедкоВам достался биллинговый модуль двенадцати лет от роду. Каждый метод в нём объявляет throws Exception; там сорок блоков catch (Exception e) { e.printStackTrace(); } и несколько пустых блоков catch с одним лишь комментарием // TODO; в одном месте catch (SQLException e) перебрасывает BillingException, собранное из голого сообщения без всякой причины; а каждый поток освобождается рукописным finally, который прямо вызывает close(). Тестами модуль покрыт слабо, а вызывают его восемь других модулей, и все они обязаны продолжать компилироваться. У вас два спринта. Опишите, как вы проведёте разбор и чистку: что поменяете первым, что намеренно оставите как есть, как уберёте throws Exception из сигнатур, не изменив поведения, и как потом докажете, что чистка не проглотила сбой, который раньше выходил наружу.
Вам достался биллинговый модуль двенадцати лет от роду. Каждый метод в нём объявляет throws Exception; там сорок блоков catch (Exception e) { e.printStackTrace(); } и несколько пустых блоков catch с одним лишь комментарием // TODO; в одном месте catch (SQLException e) перебрасывает BillingException, собранное из голого сообщения без всякой причины; а каждый поток освобождается рукописным finally, который прямо вызывает close(). Тестами модуль покрыт слабо, а вызывают его восемь других модулей, и все они обязаны продолжать компилироваться. У вас два спринта. Опишите, как вы проведёте разбор и чистку: что поменяете первым, что намеренно оставите как есть, как уберёте throws Exception из сигнатур, не изменив поведения, и как потом докажете, что чистка не проглотила сбой, который раньше выходил наружу.
Сначала чините молчаливые сбои: пустой catch и голый printStackTrace одинаково теряют ошибку, поэтому каждый становится пробросом с логированием или прокомментированным решением проигнорировать. Затем восстанавливайте причины — передавайте пойманное исключение в конструктор BillingException: без причины инцидент нераскрываем. И только потом сужайте throws Exception по одному методу, чтобы вызывающие продолжали компилироваться. Доказывайте тестами на тип исключения и его причину, а не на факт падения вызова.
Типичные ошибки
- ✗Переписывать сигнатуры первыми, до проглоченных сбоев, которые и прячут баги
- ✗Считать
printStackTraceдостаточной обработкой ошибки, раз текст куда-то попадает - ✗Заявлять, что зелёная сборка доказывает отсутствие проглатывания, без теста на тип и причину
Уточняющие вопросы
- →Какие из сорока блоков
catchвы намеренно оставите нетронутыми и почему? - →Как сузить
throws Exceptionу метода, который уже вызывают восемь других модулей?