какие данные есть в исключении
Правильное использование исключений в Java
Доброго времени суток, уважаемый Хабр.
Я хотел бы рассказать, как правильно нужно использовать исключения в Java. Частично этот материал рассматривается на просторах интернета, а также рассматривается немного в книге J.Bloch Effective Java. Речь пойдет о использовании проверенных и непроверенных (checked/unchecked) исключениях. Статья будет полезна новичкам, т.к. вначале не всегда ясно, как правильно нужно пользоваться исключениями.
Иерархия исключений в Java представлена следующим образом: родительский класс для всех Throwable. От него унаследовано 2 класса: Exception и Error. От класса Exception унаследован еще RuntimeException.
Error – критические ошибки, который могут возникнуть в системе (например, StackOverflowError ). Как правило обрабатывает их система. Если они возникают, то приложение закрывается, так как при данной ситуации работа не может быть продолжена.
Exception – это проверенные исключения. Это значит, что если метод бросает исключение, которое унаследовано от Exception (напр. IOException), то этот метод должен быть обязательно заключен в блок try-catch. Сам метод, который бросает исключение, должен в сигнатуре содержать конструкцию throws. Проверенные (checked) исключения означают, что исключение можно было предвидеть и, соответственно, оно должно быть обработано, работа приложения должна быть продолжена. Пример такого исключения — это попытка создать новый файл, который уже существует (IOException). В данному случае, работа приложения должна быть продолжена и пользователь должен получить уведомление, по какой причине файл не может быть создан.
Например:
В данном примере можно увидеть, что метод createTempFile может выбрасывать IOException, когда файл не может быть создан. И это исключение должно быть обработано соответственно. Если попытаться вызвать этот метод вне блока try-catch, то компилятор выдаст ошибку и будет предложено 2 варианта исправления: окружить метод блоком try-catch или метод, внутри которого вызывается File.createTempFile, должен выбрасывать исключение IOException (чтобы передать его на верхний уровень для обработки).
RuntimeException – это непроверенные исключения. Они возникают во время выполнения приложения. К таким исключениям относится, например, NullPointerException. Они не требуют обязательного заключения в блок try-catch. Когда RuntimeException возникает, это свидетельствует о ошибке, допущенной программистом (неинициализированный объект, выход за пределы массива и т.д.). Поэтому данное исключение не нужно обрабатывать, а нужно исправлять ошибку в коде, чтобы исключение вновь не возникало.
Ниже приведен пример, как правильно работать с RuntimeException:
В данном примере метод принимает объект класса Rectangle. В описании метода содержится строка @throws, которая описывает исключение, которое может быть выброшено и при каких условиях. Однако, сигнатура метода не содержит конструкции throws. Это значит, что при вызове метода его не нужно оборачивать блоком try-catch. А программист должен не допустить передачи в метод неинициализированного объекта.
Надеюсь, такие небольшие разъяснения могут немного разобраться, когда же нужно обрабатывать исключения, а когда нужно просто исправлять ошибки в коде.
Профессионально обрабатываем исключения в Python
Одним из недостатков гибких языков, таких как Python, является предположение, что если что-то работает, то скорее всего оно сделано правильно. Я хочу написать скромное руководство по эффективному использованию исключений в Python, правильной их обработке и логировании.
Эффективная обработка исключений
Введение
Давайте рассмотрим следующую систему. У нас есть микросервис, который отвечает за:
· Прослушивание событий о новом заказе;
· Получение заказа из базы данных;
· Проверку состояния принтера;
· Отправка квитанции в налоговую систему (IRS).
В любой момент может сломаться что угодно. У вас могут возникнуть проблемы с объектом заказа, в котором может не быть нужной информации, или в принтере может закончиться бумага, или же сервис налоговой не будет работать, и вы не сможете синхронизировать с ними квитанцию об оплате, а может быть ваша база данных окажется недоступна.
Ваша задача правильно и проактивно реагировать на любую ситуацию, чтобы избежать ошибок при обработке новых заказов.
И примерно вот такой код на этот случай пишут люди (он, конечно, работает, но плохо и неэффективно):
Сначала я сосредоточусь на том, что OrderService слишком много знает, и все эти данные делают его чем-то вроде blob, а чуть позже расскажу о правильной обработке и правильном логировании исключений.
Почему этот сервис — blob?
Этот сервис знает слишком много. Кто-то может сказать, что он знает только то, что ему нужно (то есть все шаги, связанные с формированием чека), но на самом деле он знает куда больше.
Он сосредоточен на создании ошибок (например, база данных, печать, статус заказа), а не на том, что он делает (например, извлекает, проверяет статус, генерирует, отправляет) и на том, как следует реагировать в случае сбоев.
В этом смысле мне кажется, что клиент учит сервис тому, какие исключения он может выдать. Если мы решим переиспользовать его на любом другом этапе (например, клиент захочет получить еще одну печатную копию более старого чека по заказу), мы скопируем большую часть этого кода.
Несмотря на то, что сервис работает нормально, поддерживать его трудно, и неясно, как один шаг соотносится с другим из-за повторяющихся блоков except между шагами, которые отвлекают наше внимание на вопрос «как» вместо того, чтобы думать о «когда».
Первое улучшение: делайте исключения конкретными
Давайте сначала сделаем исключения более точными и конкретными. Преимущества не видны сразу, поэтому я не буду тратить слишком много времени на объяснение этого прямо сейчас. Однако обратите внимание на то, как изменяется код.
Я выделю только то, что мы поменяли:
Второе улучшение: не лезьте не в свое дело
Теперь, когда у нас есть кастомные исключения, мы можем перейти к мысли «не учите классы тому, что может пойти не так» — они сами скажут, если это случится!
Кроме того, пожалуйста, обратите внимание на то, что я сохранил инструкции raise без объявления объекта исключения. Это не опечатка. На самом деле, это правильный способ повторного вызова исключения: простой и немногословный.
Но это еще не все. Логирование продолжает меня раздражать.
Третье улучшение: улучшение логирования
Этот шаг напоминает мне принцип «говори, а не спрашивай», хотя это все же не совсем он. Вместо того, чтобы запрашивать подробности исключения и на их основе выдавать полезные сообщения, исключения должны выдавать их сами – в конце концов, я их конкретизировал!
Вот пример вывода ваших логов:
Последнее улучшение: упрощение
После более детального рассмотрения нашего окончательного кода, он кажется лучше, теперь его легко читать и поддерживать.
Но управляет ли OrderService бизнес-логикой? Я не думаю, что это сервис в общем смысле. Он больше похож на координацию вызовов настоящих сервисов обеспечивающих бизнес-логику, которая выглядит получше, чем паттерн facade.
Перейдем к упрощению.
Хорошо, а теперь скажите, насколько легче теперь стало это читать?
Я могу понять все return при беглом просмотре. Я знаю, что происходит, когда все идет хорошо, и как крайние случаи могут привести к разным результатам. И все это без прокрутки взад-вперед.
Теперь этот код такой же простой, каким (в основном) должен быть любой код.
Теперь давайте разберемся с некоторыми хитростями, которые помогут вам сделать код понятным для чтения и простым в обслуживании.
Эффективное создание исключений
Всегда классифицируйте свои исключения через базовое и расширяйте все конкретные исключения от него. С помощью этой полезной практики вы можете переиспользовать логику для связанного кода.
Исключения – это объекты, которые несут в себе информацию, поэтому не стесняйтесь добавлять кастомные атрибуты, которые могут помочь вам понять, что происходит. Не позволяйте своему бизнес-коду учить вас тому, как он должен быть построен, ведь с таким количеством сообщений и деталей потерять себя становится трудно.
В тестировании также будет больше смысла, поскольку я могу сделать assert order_id через строку.
Ловим и создаем исключения эффективно
Еще одна вещь, которую люди часто делают неправильно – это отлавливают и повторно создают исключения.
Согласно PEP 3134 Python, делать нужно следующим образом.
Повторное создание исключения
Обычной инструкции raise более чем достаточно.
Создание одного исключения из другого
Этот вариант особо актуален, поскольку он сохраняет всю трассировку стека и помогает вашей команде отлаживать основные проблемы.
Эффективное логирование исключений
Еще один совет, который не позволит вам быть слишком многословным.
Используйте logger.exception
Вам не нужно логировать объект исключения. Функция exception логгера предназначена для использования внутри блоков except . Она уже обрабатывает трассировку стека с информацией о выполнении и отображает, какое исключение вызвало ее, с сообщением, установленном на уровне ошибки!
А что, если это не ошибка?
Если по какой-то причине вы не хотите логировать исключение как ошибку, то возможно, это предупреждение или просто информация, как было показано выше.
Источники
Принципы и качество кода:
Всех желающих приглашаем на онлайн-интенсив «Быстрая разработка JSON API приложений на Flask». На этом интенсиве мы:
— Познакомимся со спецификацией JSON API;
— Узнаем, что такое сериализация/десериализация данных;
— Узнаем, что такое marshmallow и marshmallow-jsonapi;
— Познакомимся со Swagger;
— Посмотрим на обработку и выдачу связей.
Лучшие практики обработки исключений в C#
В рамках скорого старта курса «C# Developer. Professional» подготовили для вас перевод материала.
Приглашаем также всех желающих на бесплатный демо-урок «DI-контейнеры для C#». На этом занятии мы:
1) Разберемся с тем, что такое принцип DI и зачем он нужен;
2) Научимся применять DI без использования контейнеров;
3) Рассмотрим два популярных DI-контейнеры для C#: Windsor и Autofac, разберем их плюсы и минусы;
4) Научимся регистрировать зависимости, управлять их жизненным циклом, применять инъекцию зависимостей.
Я плавно приближаюсь к своему двадцатилетнему юбилею в технической индустрии. На протяжении этих лет я своими глазами повидал почти все анти-паттерны обработки исключений (да что уж там, и я сам тоже совершал ошибки). В этой статье я собрал собственные лучшие практики работы с исключениями в C#.
Не генерируйте исключения повторно
Я натыкаюсь на это снова и снова. Люди оказываются сбиты с толку тем, что исходный стек трейс «волшебным образом» исчезает при обработке ошибок. Чаще всего это вызвано повторной генерацией исключений. Давайте посмотрим на пример, в котором у нас есть вложенные try/catch :
Как вы, наверное, уже догадались, внутренний try/catch перехватывает, регистрирует и проглатывает исключение. Чтобы пробросить SpecificException в глобальный блок catch для его обработки, вам нужно пробросить его в стек. Вы можете сделать следующее:
Декорируйте исключения
Я достаточно редко вижу реализацию этой рекомендации на практике. Все исключения расширяют Exception, в котором есть словарь Data. Словарь можно использовать для включения дополнительной информации об ошибке. Отображается ли эта информация в вашем логе, зависит от того, какой фреймворк логирования и хранилище вы используете. В elmah.io записи Data отображаются на вкладке Data.
Информацию в словарь Data вносится посредством добавьте пар ключ/значение:
В этом примере я добавляю ключ с именем user с потенциальным именем пользователя, хранящимся в потоке.
Вы также можете декорировать исключения, сгенерированные сторонним кодом. Добавьте try/catch :
Перехватывайте в первую очередь наиболее специфические исключения
Вероятнее всего, у вас есть где-то код, похожий на этот:
В следующем примере я четко демонстрирую понимание, какие исключения следует ожидать и как поступать с каждым конкретным типом:
Обратите внимание, что, хотя приведенный выше код служит для объяснения порядка обработки исключений, реализация потока управления, используя исключения подобным образом — практика не очень хорошая. Это прекрасная подводка к следующему совету:
Старайтесь избегать исключений
Может показаться очевидным, что нужно избегать исключений. Но многих методов, генерирующих исключение, можно избежать с помощью защитного программирования.
Доступ к a выбрасывает исключение. Хорошо, но представьте, что a предоставляется в качестве параметра.
Если вы хотите разрешить city с нулевым значением, вы можете избежать исключения, используя null-condition оператор:
Другой распространенный пример исключений — это анализ чисел или логических значений. В следующем примере будет сгенерировано FormatException :
Создавайте пользовательские исключения
Класс MyVerySpecializedException (возможно, это не то имя класса, которое вы должны использовать в качестве примера :D) реализует три конструктора, которые должен иметь каждый класс исключения. Кроме того, я добавил свойство Status в качестве примера дополнительных данных. Это позволит нам написать такой код:
Логируйте исключения
Это кажется таким очевидным. Но я видел слишком много ошибок в коде в следующих строках при использовании этого шаблона:
Логирование как неперехваченных, так и перехваченных исключений — это меньшее, что вы можете сделать для своих пользователей. Нет ничего хуже, чем когда пользователи обращаются в вашу службу поддержки, и вы даже не подозреваете, какие были ошибки и что произошло. В этом вам поможет ведение логов.
9 лучших практик для обработки исключений в Java
Независимо от того, новичок вы или профессионал, всегда полезно освежить в памяти методы обработки исключений, чтобы убедиться, что вы и ваша команда можете справиться с проблемами.
Вот почему у большинства команд разработчиков есть собственный набор правил их использования. И если вы новичок в команде, вас может удивить, насколько эти правила могут отличаться от тех, которые вы использовали раньше.
Тем не менее, есть несколько передовых практик, которые используются большинством команд. Вот 9 самых важных из них, которые помогут вам начать работу или улучшить обработку исключений.
1. Освободите ресурсы в блоке finally или используйте инструкцию «Try-With-Resource»
Довольно часто вы используете ресурс в своем блоке try, например InputStream, который вам нужно закрыть позже. Распространенной ошибкой в таких ситуациях является закрытие ресурса в конце блока try.
Проблема в том, что этот подход работает отлично до тех пор, пока не генерируется исключение. Все операторы в блоке try будут выполнены, и ресурс будет закрыт.
Но вы не зря добавили блок try. Вы вызываете один или несколько методов, которые могут вызвать исключение, или, может быть, вы сами вызываете исключение. Это означает, что вы можете не дойти до конца блока try. И как следствие, вы не закроете ресурсы.
Поэтому вам следует поместить весь код очистки в блок finally или использовать оператор try-with-resource.
Используйте блок Finally
В отличие от последних нескольких строк вашего блока try, блок finally всегда выполняется. Это происходит либо после успешного выполнения блока try, либо после обработки исключения в блоке catch. Благодаря этому вы можете быть уверены, что освободите все захваченные ресурсы.
Оператор Java 7 «Try-With-Resource»
Вы можете использовать его, если ваш ресурс реализует интерфейс AutoCloseable. Это то, что делает большинство стандартных ресурсов Java. Когда вы открываете ресурс в предложении try, он автоматически закрывается после выполнения блока try или обработки исключения.
2. Конкретные исключения предпочтительнее
Чем конкретнее исключение, которое вы генерируете, тем лучше. Всегда помните, что коллеге, который не знает вашего кода, а может быть, и вам через несколько месяцев, необходимо вызвать ваш метод и обработать исключение.
Поэтому постарайтесь предоставить им как можно больше информации. Это упрощает понимание вашего API. В результате вызывающий ваш метод сможет лучше обработать исключение или избежать его с помощью дополнительной проверки.
Поэтому всегда старайтесь найти класс, который лучше всего подходит для вашего исключительного события, например, генерируйте NumberFormatException вместо IllegalArgumentException. И избегайте создания неспецифического исключения.
3. Документируйте определенные вами исключения
Каждый раз, когда вы определяете исключение в сигнатуре вашего метода, вы также должны задокументировать его в своем Javadoc. Это преследует ту же цель, что и предыдущая передовая практика: предоставить вызывающему как можно больше информации, чтобы он мог избежать или обработать исключение.
Итак, не забудьте добавить объявление @throws в свой Javadoc и описать ситуации, которые могут вызвать исключение.
4. Генерирование исключений с описательными сообщениями
Идея, лежащая в основе этой передовой практики, аналогична двум предыдущим. Но на этот раз вы не предоставляете информацию вызывающей стороне вашего метода. Сообщение об исключении читают все, кто должен понимать, что произошло, когда исключение было зарегистрировано в файле журнала или в вашем инструменте мониторинга.
Следовательно, он должен как можно точнее описать проблему и предоставить наиболее актуальную информацию для понимания исключительного события.
Не поймите меня неправильно; вы не должны писать абзац текста. Но вам следует объяснить причину исключения в 1-2 коротких предложениях. Это помогает вашей группе эксплуатации понять серьезность проблемы, а также упрощает анализ любых инцидентов, связанных с обслуживанием.
Если вы выберете конкретное исключение, его имя класса, скорее всего, уже будет описывать тип ошибки. Таким образом, вам не нужно предоставлять много дополнительной информации. Хорошим примером этого является NumberFormatException. Оно вызывается конструктором класса java.lang.Long, когда вы предоставляете String в неправильном формате.
Название класса NumberFormatException уже говорит вам о типе проблемы. Его сообщение должно содержать только строку ввода, которая вызвала проблему. Если имя класса исключения не так выразительно, вам необходимо предоставить необходимую информацию в сообщении.
5. Сначала перехватите наиболее конкретное исключение
Большинство IDE помогут вам в этой лучшей практике. Они сообщают о недостижимом блоке кода, когда вы сначала пытаетесь перехватить менее конкретное исключение.
Проблема в том, что выполняется только первый блок catch, соответствующий исключению. Итак, если вы сначала поймаете IllegalArgumentException, вы никогда не достигнете блока catch, который должен обрабатывать более конкретное NumberFormatException, потому что это подкласс IllegalArgumentException.
Всегда сначала перехватывайте наиболее конкретный класс исключения и добавляйте менее конкретные блоки перехвата в конец вашего списка.
6. Не перехватывайте Throwable
Если вы используете Throwable в предложении catch, он не только перехватит все исключения; он также перехватит все ошибки. JVM выдает ошибки, чтобы указать на серьезные проблемы, которые не предназначены для обработки приложением. Типичными примерами этого являются OutOfMemoryError или StackOverflowError. И то, и другое вызвано ситуациями, которые находятся вне контроля приложения и не могут быть обработаны.
Итак, лучше не перехватывайте Throwable, если вы не абсолютно уверены, что находитесь в исключительной ситуации, в которой вы можете или обязаны обрабатывать ошибку.
7. Не игнорируйте исключения
Вы когда-нибудь анализировали отчет об ошибке, в котором выполнялась только первая часть вашего сценария использования?
Часто это вызвано игнорируемым исключением. Разработчик, вероятно, был уверен, что оно никогда не будет вызвано, и добавил блок catch, который не обрабатывает и не регистрирует его. И когда вы найдете этот блок, вы, скорее всего, даже найдете один из известных комментариев «Этого никогда не будет».
Что ж, возможно, вы анализируете проблему, в которой произошло невозможное.
Поэтому, пожалуйста, никогда не игнорируйте исключения. Вы не знаете, как код изменится в будущем. Кто-то может удалить проверку, которая предотвратила исключительное событие, не осознавая, что это создает проблему. Или код, который генерирует исключение, изменяется и теперь генерирует несколько исключений одного и того же класса, а вызывающий код не предотвращает их все.
Вы должны хотя бы написать сообщение в журнале, сообщающее всем, что произошло немыслимое и что кто-то должен это проверить.
8. Не пишите в лог сгенерированные исключения
Это, вероятно, наиболее часто игнорируемая передовая практика в списке. Вы можете найти множество фрагментов кода и даже библиотек, в которых исключение перехватывается, регистрируется и повторно генерируется.
Может показаться интуитивно понятным регистрировать исключение, когда оно произошло, а затем повторно генерировать его, чтобы вызывающий мог обработать его соответствующим образом. Но, в таком случае, приложение будет писать в лог несколько сообщений об ошибках для одного и того же исключения.
Повторные сообщения также не добавляют никакой информации. Как объясняется в лучшей практике №4, сообщение об исключении должно описывать исключительное событие. А трассировка стека сообщает вам, в каком классе, методе и строке было сгенерировано исключение.
Если вам нужно добавить дополнительную информацию, вы должны перехватить исключение и обернуть его в пользовательское. Но обязательно следуйте передовой практике номер 9.
Итак, перехватывайте исключение, только если вы хотите его обработать. В противном случае укажите это в сигнатуре метода и позвольте вызывающей стороне позаботиться об этом.
9. Оберните исключение, не обрабатывая его
Иногда лучше поймать стандартное исключение и превратить его в настраиваемое. Типичным примером такого исключения является бизнес-исключение для конкретного приложения или платформы. Это позволяет вам добавлять дополнительную информацию, а также вы можете реализовать специальную обработку для вашего класса исключения.
Когда вы это сделаете, обязательно установите исходное исключение в качестве причины. Класс Exception предоставляет определенные методы конструктора, которые принимают Throwable в качестве параметра. В противном случае вы потеряете трассировку стека и сообщение об исходном исключении, что затруднит анализ исключительного события, вызвавшего ваше исключение.
Резюме
Как вы видели, есть много разных вещей, которые вы должны учитывать, когда генерируете или перехватываете исключение. Большинство из них имеют цель улучшить читаемость вашего кода или удобство использования вашего API.
Чаще всего исключения являются одновременно механизмом обработки ошибок и средством связи. Поэтому вам следует обязательно обсудить передовые практики и правила, которые вы хотите применять, со своими коллегами, чтобы все понимали общие концепции и использовали их одинаково.
Исключения в Java, Часть I (try-catch-finally)
Это первая часть статьи, посвященной такому языковому механизму Java как исключения (вторая (checked/unchecked) вот). Она имеет вводный характер и рассчитана на начинающих разработчиков или тех, кто только приступает к изучению языка.
Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).
1. Ключевые слова: try, catch, finally, throw, throws
«Магия» (т.е. некоторое поведение никак не отраженное в исходном коде и потому неповторяемое пользователем) исключений #1 заключается в том, что catch, throw, throws можно использовать исключительно с java.lang.Throwable или его потомками.
throws:
Годится
catch:
Годится
throw:
Годится
Кроме того, throw требуется не-null аргумент, иначе NullPointerException в момент выполнения
throw и new — это две независимых операции. В следующем коде мы независимо создаем объект исключения и «бросаем» его
Однако, попробуйте проанализировать вот это
2. Почему используем System.err, а не System.out
System.out — buffered-поток вывода, а System.err — нет. Таким образом вывод может быть как таким
Так и вот таким (err обогнало out при выводе в консоль)
Давайте это нарисуем
когда Вы пишете в System.err — ваше сообщение тут же выводится на консоль, но когда пишете в System.out, то оно может на какое-то время быть буферизированно. Stacktrace необработанного исключение выводится через System.err, что позволяет им обгонять «обычные» сообщения.
3. Компилятор требует вернуть результат (или требует молчать)
Если в объявлении метода сказано, что он возвращает НЕ void, то компилятор зорко следит, что бы мы вернули экземпляр требуемого типа или экземпляр типа, который можно неявно привести к требуемому
вот так не пройдет (другой тип)
Вот так не выйдет — нет возврата
и вот так не пройдет (компилятор не может удостовериться, что возврат будет)
Компилятор отслеживает, что бы мы что-то вернули, так как иначе непонятно, что должна была бы напечатать данная программа
Из-забавного, можно ничего не возвращать, а «повесить метод»
Тут в d никогда ничего не будет присвоено, так как метод sqr повисает
Компилятор пропустит «вилку» (таки берем в квадрат ИЛИ висим)
Но механизм исключений позволяет НИЧЕГО НЕ ВОЗВРАЩАТЬ!
Итак, у нас есть ТРИ варианта для компилятора
Но КАКОЙ ЖЕ double вернет функция, бросающая RuntimeException?
А НИКАКОЙ!
Подытожим: бросаемое исключение — это дополнительный возвращаемый тип. Если ваш метод объявил, что возвращает double, но у вас нет double — можете бросить исключение. Если ваш метод объявил, что ничего не возвращает (void), но у вам таки есть что сказать — можете бросить исключение.
Давайте рассмотрим некоторый пример из практики.
Задача: реализовать функцию, вычисляющую площадь прямоугольника
важно, что задание звучит именно так, в терминах предметной области — «вычислить площадь прямоугольника», а не в терминах решения «перемножить два числа»:
Мы не можем ничего не вернуть
Можно, конечно, отписаться в консоль, но кто ее будет читать и как определить где была поломка. При чем, вычисление то продолжится с неправильными данными
Можно вернуть специальное значение, показывающее, что что-то не так (error code), но кто гарантирует, что его прочитают, а не просто воспользуются им?
Можем, конечно, целиком остановить виртуальную машину
Но «правильный путь» таков: если обнаружили возможное некорректное поведение, то
1. Вычисления остановить, сгенерировать сообщение-поломку, которое трудно игнорировать, предоставить пользователю информацию о причине, предоставить пользователю возможность все починить (загрузить белье назад и повторно нажать кнопку старт)
4. Нелокальная передача управления (nonlocal control transfer)
Механизм исключительных ситуация (исключений) — это механизм НЕЛОКАЛЬНОЙ ПЕРЕДАЧИ УПРАВЛЕНИЯ.
Что под этим имеется в виду?
Программа, в ходе своего выполнения (точнее исполнения инструкций в рамках отдельного потока), оперирует стеком («стопкой») фреймов. Передача управления осуществляется либо в рамках одного фрейма
и другие операторы.
return — выходим из ОДНОГО фрейма (из фрейма #4(метод h()))
throw — выходим из ВСЕХ фреймов
При помощи catch мы можем остановить летящее исключение (причина, по которой мы автоматически покидаем фреймы).
Останавливаем через 3 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g()) + фрейм #2(метод f())
Обратите внимание, стандартный сценарий работы был восстановлен в методе main() (фрейм #1)
Останавливаем через 2 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g())
Останавливаем через 1 фрейм (фактически аналог return, просто покинули фрейм «другим образом»)
Итак, давайте сведем все на одну картинку
5. try + catch (catch — полиморфен)
Напомним иерархию исключений
То, что исключения являются объектами важно для нас в двух моментах
1. Они образуют иерархию с корнем java.lang.Throwable (java.lang.Object — предок java.lang.Throwable, но Object — уже не исключение)
2. Они могут иметь поля и методы (в этой статье это не будем использовать)
По первому пункту: catch — полиморфная конструкция, т.е. catch по типу Parent перехватывает летящие экземпляры любого типа, который является Parent-ом (т.е. экземпляры непосредственно Parent-а или любого потомка Parent-а)
Даже так: в блоке catch мы будем иметь ссылку типа Exception на объект типа RuntimeException
catch по потомку не может поймать предка
catch по одному «брату» не может поймать другого «брата» (Error и Exception не находятся в отношении предок-потомок, они из параллельных веток наследования от Throwable)
По предыдущим примерам — надеюсь вы обратили внимание, что если исключение перехвачено, то JVM выполняет операторы идущие ПОСЛЕ последних скобок try+catch.
Но если не перехвачено, то мы
1. не заходим в блок catch
2. покидаем фрейм метода с летящим исключением
А что будет, если мы зашли в catch, и потом бросили исключение ИЗ catch?
В таком случае выполнение метода тоже прерывается (не печатаем «3»). Новое исключение не имеет никакого отношения к try-catch
Мы можем даже кинуть тот объект, что у нас есть «на руках»
И мы не попадем в другие секции catch, если они есть
Обратите внимание, мы не напечатали «3», хотя у нас летит Error а «ниже» расположен catch по Error. Но важный момент в том, что catch имеет отношение исключительно к try-секции, но не к другим catch-секциям.
Как покажем ниже — можно строить вложенные конструкции, но вот пример, «исправляющий» эту ситуацию
Как вы видели, мы можем расположить несколько catch после одного try.
Но есть такое правило — нельзя ставить потомка после предка! (RuntimeException после Exception)
Ставить брата после брата — можно (RuntimeException после Error)
Как происходит выбор «правильного» catch? Да очень просто — JVM идет сверху-вниз до тех пор, пока не найдет такой catch что в нем указано ваше исключение или его предок — туда и заходит. Ниже — не идет.
Выбор catch осуществляется в runtime (а не в compile-time), значит учитывается не тип ССЫЛКИ (Throwable), а тип ССЫЛАЕМОГО (Exception)
7. try + finally
finally-секция получает управление, если try-блок завершился успешно
finally-секция получает управление, даже если try-блок завершился исключением
finally-секция получает управление, даже если try-блок завершился директивой выхода из метода
finally-секция НЕ вызывается только если мы «прибили» JVM
System.exit(42) и Runtime.getRuntime().exit(42) — это синонимы
И при Runtime.getRuntime().halt(42) — тоже не успевает зайти в finally
exit() vs halt()
javadoc: java.lang.Runtime#halt(int status)
… Unlike the exit method, this method does not cause shutdown hooks to be started and does not run uninvoked finalizers if finalization-on-exit has been enabled. If the shutdown sequence has already been initiated then this method does not wait for any running shutdown hooks or finalizers to finish their work.
Однако finally-секция не может «починить» try-блок завершившийся исключение (заметьте, «more» — не выводится в консоль)
Трюк с «if (true) <. >» требуется, так как иначе компилятор обнаруживает недостижимый код (последняя строка) и отказывается его компилировать
И finally-секция не может «предотвратить» выход из метода, если try-блок вызвал return («more» — не выводится в консоль)
Однако finally-секция может «перебить» throw/return при помощи другого throw/return
finally-секция может быть использована для завершающего действия, которое гарантированно будет вызвано (даже если было брошено исключение или автор использовал return) по окончании работы
Например для освобождения захваченной блокировки
Или для закрытия открытого файлового потока
Специально для этих целей в Java 7 появилась конструкция try-with-resources, ее мы изучим позже.
Вообще говоря, в finally-секция нельзя стандартно узнать было ли исключение.
Конечно, можно постараться написать свой «велосипед»
Не рекомендуемые практики
— return из finally-секции (можем затереть исключение из try-блока)
— действия в finally-секции, которые могут бросить исключение (можем затереть исключение из try-блока)
8. try + catch + finally
Не заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение и есть подходящий catch
Заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение но нет подходящего catch
Не заходим в catch, заходим в finally, не продолжаем после оператора — вылетаем с неперехваченным исключением
9. Вложенные try + catch + finally
Операторы обычно допускают неограниченное вложение.
Пример с if
Суть в том, что try-cacth-finally тоже допускает неограниченное вложение.
Например вот так
Ну что же, давайте исследуем как это работает.
Вложенный try-catch-finally без исключения
Мы НЕ заходим в обе catch-секции (нет исключения), заходим в обе finally-секции и выполняем обе строки ПОСЛЕ finally.
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНУТРЕННИЙ catch
Мы заходим в ПЕРВУЮ catch-секцию (печатаем «3»), но НЕ заходим во ВТОРУЮ catch-секцию (НЕ печатаем «6», так как исключение УЖЕ перехвачено первым catch), заходим в обе finally-секции (печатаем «4» и «7»), в обоих случаях выполняем код после finally (печатаем «5»и «8», так как исключение остановлено еще первым catch).
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНЕШНИЙ catch
Мы НЕ заходим в ПЕРВУЮ catch-секцию (не печатаем «3»), но заходим в ВТОРУЮ catch-секцию (печатаем «6»), заходим в обе finally-секции (печатаем «4» и «7»), в ПЕРВОМ случае НЕ выполняем код ПОСЛЕ finally (не печатаем «5», так как исключение НЕ остановлено), во ВТОРОМ случае выполняем код после finally (печатаем «8», так как исключение остановлено).
Вложенный try-catch-finally с исключением, которое НИКТО НЕ ПЕРЕХВАТИТ
Мы НЕ заходим в ОБЕ catch-секции (не печатаем «3» и «6»), заходим в обе finally-секции (печатаем «4» и «7») и в обоих случаях НЕ выполняем код ПОСЛЕ finally (не печатаем «5» и «8», так как исключение НЕ остановлено), выполнение метода прерывается по исключению.
Контакты
Я занимаюсь онлайн обучением Java (вот курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно, видео канала лучше систематизировано в этой статье.