Исключения в Java
Метод из двух строк: в try бросается IllegalStateException, в finally стоит return. Запустил - метод спокойно вернул строку из finally, а исключение исчезло без следа. Ни лога, ни стектрейса, ничего.
По исключениям интервьюер отличает «писал код» от «сопровождал код». Иерархию расскажет любой, а вот что делать с ошибкой - где ловить, что логировать, как не потерять первопричину - знают те, кто дежурил по инцидентам.
// Формулировки: «checked и unchecked - что используешь и почему?», «что не так с return в finally?», «как правильно перебросить исключение?»
Иерархия и вечный спор про checked
Дерево простое: Throwable делится на Error и Exception. Error - это беды виртуальной машины вроде нехватки памяти или переполнения стека, их не ловят. Exception делится дальше: checked компилятор заставляет обработать или объявить в throws, а unchecked (RuntimeException и наследники) летят свободно.
Замысел был такой: checked - для ожидаемых восстановимых ситуаций (файла нет, сеть моргнула), unchecked - для ошибок программирования (null, кривой индекс). На практике checked-объявления расползаются вверх по всем слоям и захламляют сигнатуры: метод репозитория объявил IOException, и о нём знает уже контроллер.
// Поэтому современный тренд - предпочитать unchecked, а чужие checked оборачивать в свои на границе слоя. Kotlin и Spring выбрали эту сторону целиком. На собеседовании ценится не «правильный» ответ, а то, что ты знаешь обе позиции и умеешь аргументировать свою.
- checked
- компилятор требует обработать или объявить
- unchecked
- RuntimeException и наследники, летят свободно
try-with-resources и предательский finally
Проверил порядок на двух ресурсах. Открылись A и B, тело бросило исключение, закрылись B и A - в обратном порядке открытия. Причём close у B тоже бросил своё исключение, и оно не потерялось: наружу вылетело исключение из тела, а ошибка закрытия приехала к нему прицепом, в списке подавленных.
Это и есть аргумент за try-with-resources: всё, что реализует AutoCloseable, закрывается само, в правильном порядке и во всех ветках. Ручное закрытие в finally с проверками на null - код до Java 7, сегодня это красный флаг на ревью.
// А finally показывает тёмную сторону, когда в нём стоит return или throw. Он выполняется всегда, поэтому перетирает исключение из try - тот замер из начала темы именно про это. Метод возвращает значение, настоящая ошибка испаряется, и найти её потом нельзя ничем.
try { throw new IllegalStateException("настоящая ошибка"); }
finally { return "значение из finally"; }
// метод вернул строку, исключение исчезло- try-with-resources
- автозакрытие в обратном порядке во всех ветках
- подавленное исключение
- ошибка close, прицепленная к основному исключению
Культура обработки: ловить узко, причину сохранять
Ловить надо узко и осмысленно. Конструкция catch (Exception e) с пустым телом проглатывает и NPE (NullPointerException), и всё, что прилетело от соседнего кода; отладка через неделю идёт вслепую. Обработать нечем - не лови вовсе, пусть летит туда, где решат.
Перебрасываешь - сохраняй причину, и это тоже проверяется замером. Обернул исходную ошибку с передачей cause - getCause показывает исходное IOException с его текстом. Обернул без cause - getCause возвращает null, и первопричина потеряна навсегда вместе со стектрейсом.
// Правило «логируй или пробрасывай, но не одновременно». Логируешь и пробрасываешь на каждом слое - одна ошибка размножается в пять стектрейсов, и дежурный ищет пять багов вместо одного. И ещё: исключения не годятся как поток управления, создание каждого заполняет стектрейс, в горячем цикле это дорого.
- cause
- исходное исключение внутри обёртки, хранит стектрейс
- стектрейс
- снимок стека на момент создания исключения
Как отвечать: «Checked и unchecked - в чём разница и что предпочитаешь?»
Checked компилятор заставляет обработать или объявить в throws, unchecked - это RuntimeException и его наследники, они летят свободно. По изначальному замыслу checked предназначались для восстановимых ситуаций вроде отсутствия файла, unchecked - для ошибок программирования. На практике я предпочитаю unchecked, потому что checked расползаются по всем слоям: объявил IOException в репозитории - и о нём знает контроллер, которому это не нужно. Поэтому чужие checked оборачиваю в доменные unchecked прямо на границе слоя, и обязательно с передачей cause. Это не формальность: я проверял, обёртка без cause даёт getCause равный null, то есть первопричина и её стектрейс потеряны навсегда. Ловлю узко и только там, где могу осмысленно обработать, остальное пусть летит к общему обработчику. И держу правило «логируй или пробрасывай, но не оба сразу», иначе одна ошибка превращается в пять стектрейсов в логе.
Сильный ответ: есть учебное различие, собственная позиция с аргументом и практика - оборачивание на границе слоя, обязательный cause, узкий catch. Звучит как опыт, а не пересказ главы.
На чём валят
- −Ставят return или throw в finally. Исключение из try исчезает молча, и спрашивают это кодом «что вернёт метод».
- −Пишут catch (Exception e) с пустым телом. На собеседовании это маркер человека, который прод не сопровождал.
- −Оборачивают без cause. Стектрейс первопричины теряется, getCause возвращает null.
- −Логируют и пробрасывают на каждом слое. Одна ошибка даёт пять стектрейсов, разбор превращается в квест.
- −Ставят catch родителя раньше наследника. Это ошибка компиляции: exception RuntimeException has already been caught.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 18, остальные разбираются в тренажёре.
- Выполнится ли блок finally, если в try есть return?A)Нет: return немедленно выходит из метода, и до finally управление уже не доходитB)Finally выполнится лишь в том случае, если внутри try действительно возникло исключение, а при обычном return этот блок просто пропускаетсяC)Да, finally выполняется при return и при исключении; не выполнится лишь при System.exit или падении JVMD)Да, но лишь когда метод возвращает void; при return со значением finally пропускается
показать ответ и разбор
+C)Да, finally выполняется при return и при исключении; не выполнится лишь при System.exit или падении JVM// разбор: finally выполняется практически всегда: при нормальном завершении try, при return из try (return «придерживается», пока не отработает finally) и при выбросе исключения. Не выполнится он лишь в экстренных случаях: System.exit() (принудительно гасит JVM), фатальный сбой/убийство процесса, бесконечный цикл или демон-поток, чей процесс завершился. Именно поэтому в finally освобождают ресурсы. Отдельная ловушка — return внутри самого finally: он перекрывает результат/исключение из try.
- Зачем нужен try-with-resources?A)Чтобы повторить try при сбое: блок автоматически перезапускается, пока ресурс не откроетсяB)Чтобы ускорить работу с файлами за счёт кэширования открытых потоков между вызовами методаC)Чтобы одним блоком catch поймать сразу больше разных типов исключений, не перечисляя их все через вертикальную черту вручнуюD)Чтобы автоматически закрыть ресурсы (AutoCloseable) в обратном порядке даже при исключении — без утечек
показать ответ и разбор
+D)Чтобы автоматически закрыть ресурсы (AutoCloseable) в обратном порядке даже при исключении — без утечек// разбор: try-with-resources объявляет ресурсы в скобках try(...); каждый должен реализовывать AutoCloseable. По выходу из блока — нормальному ИЛИ через исключение — JVM автоматически вызывает close() для них в порядке, обратном открытию. Это устраняет классические утечки, когда close забыли или он не сработал из-за исключения до finally. Плюс исключение из close не затирает основное — оно добавляется как suppressed. Заметно чище ручного try/finally.
- Чем отличаются throw и throws?A)Throw объявляет прямо в сигнатуре метода, что тот способен бросить исключение, а throws уже фактически выбрасывает его в телеB)throw бросает исключение (оператор в теле); throws объявляет в сигнатуре, что метод может его броситьC)Throw — для checked-исключений, throws — для unchecked; в остальном они взаимозаменяемыD)Это две формы записи одного и того же; какую выбрать — вопрос стиля и версии Java
показать ответ и разбор
+B)throw бросает исключение (оператор в теле); throws объявляет в сигнатуре, что метод может его бросить// разбор: throw — оператор, который в конкретной точке кода ВЫБРАСЫВАЕТ экземпляр исключения: throw new IllegalArgumentException(...). throws — часть СИГНАТУРЫ метода, объявляющая, какие checked-исключения он может пробросить наружу: void read() throws IOException. То есть throw — действие, throws — контракт. Путать их — классическая оговорка джуна. Для unchecked throws писать не обязательно, но иногда добавляют для документирования.
- Метод бросает исключение в try, но в finally стоит return. Что вернётся наружу?A)Исключение из try сильнее return и прервёт выполнение методаB)Значение из return в finally; исключение из try при этом молча проглатывается — это антипаттернC)Оба: сначала метод вернёт значение, а затем повторно выбросит отложенное исключениеD)Ошибка компиляции: делать return внутри finally, когда в try возможно исключение, прямо запрещено спецификацией самого языка
показать ответ и разбор
+B)Значение из return в finally; исключение из try при этом молча проглатывается — это антипаттерн// разбор: return (или throw) в finally перекрывает всё, что «недосказал» try: если try выбросил исключение, а finally делает return — наружу уходит именно это значение, а исключение теряется БЕЗ следа (не логируется, не пробрасывается). Компилятор такое разрешает, поэтому баг тихий и коварный — потерянные ошибки крайне трудно ловить. Отсюда правило: не делать return/throw в finally; finally оставляют только под освобождение ресурсов.
- Чем плох пустой catch: catch (Exception e) {}?A)Он ловит слишком мало — Exception не перехватывает RuntimeException, и реальные баги пролетают мимоB)Он молча глотает любые исключения, включая ошибки логики (NPE и пр.) — теряется вся диагностикаC)Пустой catch не компилируется: блок обработки обязан содержать хотя бы логирование или throwD)Он замедляет метод, потому что JVM на каждый вызов проверяет, не пуст ли блок обработки
показать ответ и разбор
+B)Он молча глотает любые исключения, включая ошибки логики (NPE и пр.) — теряется вся диагностика// разбор: catch (Exception e) {} перехватывает почти всё (включая RuntimeException — NPE, IllegalState и т.п.) и НИЧЕГО с этим не делает: ошибка исчезает без лога и без проброса. Программа продолжает работу в некорректном состоянии, а причину потом не найти — это одна из худших практик. Как минимум исключение логируют с контекстом и/или пробрасывают; ловить стоит конкретные типы, а не Exception «на всё». Компилятор пустой catch разрешает, скорость ни при чём.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.