Исключения Java. Java: Операторы throw и throws

Эта статья посвящается очень важному вопросу программирования - исключительным ситуациям и ошибкам (exceptions and errors).

В языке Java исключения (Exceptions) и ошибки (Errors) являются объектами. Когда метод вызывает, еще говорят "бросает" от слова "throws", исключительную ситуацию, он на самом деле работает с объектом. Но такое происходит не с любыми объектами, а только с теми, которые наследуются от Throwable.

Упрощенную диаграмму классов ошибок и исключительний вы можете увидеть на следующем рисунке:

RuntimeException, Error и их наследников еще называют unchecked exception , а всех остальных наследников класса Exception - checked exception .

Checked Exception обязывает пользователя обработать ее (использую конструкцию try-catch) или же отдать на откуп обрамляющим методам, в таком случае к декларации метода, который бросает проверяемое (checked) исключение, дописывают конструкцию throws , например:

Public Date parse(String source) throws ParseException { ... }

К unchecked исключениям относятся, например, NullPointerException, ArrayIndexOutOfBoundsException, ClassCastExcpetion и так далее. Это те ошибки, которые могут возникнут практически в любом методе. Несомненно, описывать каждый метод как тот, который бросает все эти исключения, было бы глупо.

1. Так когда же нужно бросать ошибки? . На этот вопрос можно ответить просто: если в методе возможна ситуация, которую метод не в состоянии обработать самостоятельно, он должен "бросать" ошибку. Но ни в коем случае нельзя использовать исключительные ситуации для управления ходом выполнения программы.

Чаще всего Exceptions бросаются при нарушении контракта метода. Контракт (contract) - это негласное соглашение между создателем метода (метод сделает и/или вернет именно то, что надо) и пользователем метода (на вход метода будут передаваться значения из множества допустимых).

Нарушение контракта со стороны создателя метода - это, например, что-нибудь на подобии MethodNotImplementedYetException:).

Пользователь метода может нарушить контракт, например, таким способом: на вход Integer.parseInt(String) подать строку с буквами и по заслугам получить NumberFormatException.

Часто при реализации веб-сервисов первыми строками методов я пишу конструкции вида:

Public Contract getContractById(String id) { if (id == null) throw new NullPointerException("id is null"); ... }

Это помогает на вызывающей стороне понять, что они нарушают контракт метода, причиной чего часто может быть ошибка в логике их же приложения.

2. А что собственно бросать? . Выбор не то чтобы сильно велик, но и не однозначен: checked, unchecked (runtime), unchecked (error).

Сразу скажу, в подавляющем большинстве случаев Error вам не понадобится. Это в основном критические ошибки (например, StackOverflowError), с которыми пусть работает JVM.

Checked Exceptions, как было написано выше, заставляет программиста-пользователя написать код для ее обработки или же описать метод как "вызывающий исключительную ситуацию".

С unchecked exception можно поступить по-разному. В случае с такими ошибками, пользователь сам решает, будет он обрабатывать эту ошибку, или же нет (компилятор не заставляет это делать).

Можно написать следующее простое правило: если некоторый набор входящих в метод данных может привести к нарушению контракта, и вы считаете, что программисту-пользователю важно разобраться с этим (и что он сможет это сделать), описывайте метод с конструкцией throws, иначе бросайте unchecked exception.

3. Ну и как это обрабатывать? Обрабатывать ошибку лучше там, где она возникла. Если в данном фрагменте кода нет возможности принять решение, что делать с исключением, его нужно бросать дальше, пока не найдется нужный обработчик, либо поток выполнения программы не вылетит совсем.

Вы наверняка знаете, что обработка исключений происходит с помощью блока try-catch-finally. Сразу скажу вам такую вещь: никогда не используйте пустой catch блок! . Выглядит этот ужас так:

Try { ... } catch(Exception e) { }

Если Вы уверены, что исключения в блоке try не возникнет никогда, напишите комментарий, как например в этом фрагменте кода:

StringReader reader = new StringReader("qwerty"); try { reader.read(); } catch (IOException e) { /* cannot happen */ }

Если же исключение в принципе может возникнуть, но только действительно в "исключительной ситуации" когда с ним ничего уже сделать будет нельзя, лучше оберните его в RuntimeException и пробросьте наверх, например:

String siteUrl = ...; ... URL url; try { url = new URL(siteUrl); } catch (MalformedURLException e) { throw new RuntimeException(e); }

Скорее всего ошибка здесь может возникнуть только при неправильной конфигурации приложения, например, siteUrl читается из конфигурационного файла и кто-то допустил опечатку при указании адреса сайта. Без исправления конфига и перезапуска приложения тут ничего поделать нельзя, так что RuntimeException вполне оправдан.

4. Зачем нужно все это делать? А почему бы и нет:). Если серьезно - правильное использование Exceptions и корректная их обработка сделают код более понятным, гибким, структурированным и возможным для повторного использования.

Updated 28.08.2009: Хочу показать вам несколько интересных моментов, которые касаются исключений и блоков try-catch-finally.

Можно ли сделать так, чтобы блок finally не выполнился? Можно:

Public class Test1 { public static void main(String args) { try { throw new RuntimeException(); } catch (Exception e) { System.exit(0); } finally { System.out.println("Please, let me print this."); } } }

Ну и еще одна интересная вещь. Какое исключение будет выброшено из метода:

Public class Test { public static void main(String args) { try { throw new NullPointerException(); } catch (NullPointerException e) { throw e; } finally { throw new IllegalStateException(); } } }

Правильный ответ - IllegalStateException. Не смотря на то, что в блоке catch происходит повторный "выброс" NPE, после него выполняется блок finally, который перехватывает ход выполнения программы и бросает исключение IllegalStateException.

Жду ваших вопросов и комментариев.



Комментариев: 2

Но ни в коем случае нельзя использовать исключительные ситуации для реализации частей бизнесс-логики.

Я бы сказал не стоит использовать исключения для управления flow алгоритма (типа выхода из вложенного цикла и все такое). А вот для реализации частей бизнес-логики как раз таки иногда и нужно, но только те которые checked. Классический пример - снятие денег со счета, когда выкидывается исключение, говорящее о том что денюшок то не хватает. Вот тут как раз клиенту метода снятия надо реализовать бизнес-логику по обработке такой ситуации.

Ну а в тех случаях когда речь идет о нарушении контракта метода (не бизнес-ограничений) не надо и задумываться - только unchecked, и ловить их должен код, находящийся в самом начале потока, и ничего кроме логирования информации об ошибки с ними сделать он не может, да и не должен.

P.S. Ну конечно бывают ситуации исключительные. Но это не более 20%.

lucker wrote: Ну а в тех случаях когда речь идет о нарушении контракта метода (не бизнес-ограничений) не надо и задумываться - только unchecked, и ловить их должен код, находящийся в самом начале потока, и ничего кроме логирования информации об ошибки с ними сделать он не может, да и не должен.

такой подход приведет к тому что код в начале потока будет в 80% случаев получать исключение неизвестно по какой причине и как возникшее, например стэк трэйс на вэб странице, ни один из модулей который будет использовать такой метод и понятия не будет иметь о том что гдето в глубине ктото выбросил анчекед исключение и результат его может быть самым удручающим, например: если такой метод кто-то начнет использовать в цикле то из-за одноко неправильного объекта переданого в метод, который выкинет анчекед из-за нарушения контракта, вместо скажем ожидаемого списка из нескольких строк на экране клиент не увидит ни одного, а мог бы просто увидеть на один меньше (тот который послужил причиной исключения) А произойдет это потому что девелопер который будет писать вывод понятия не будет иметь что данный конкретный метод может просто выкинуть тот или иной рантайм, если такой метод в случае нарушения контракта выбросит чекед исключение то пользователь метода будет иметь информацию и решит что ему делать с данным исключением, код будет иметь более предсказуемое поведение а следственно станет более професиональным.

Использование анчекед исключений где попало как раз и делает код мало пригодным к использованию, поскольку как раз скрывает тот самый контракт метода, когда пользователь метода понятия не имеет какие исключения могут вылететь из данного метода. А вылетать такие исключения как раз любят на продакшене во время демонстрации системы заказчику.

Статья кстати выглядит немного поверхностной, основная проблема в написании комерческого софта именно в том чтобы сделать его реюзабельным и легко изменяемым к постоянно меняющимся требованиям при этом надежным, предсказуемым и устойчивым. Поэтому приведенная цитата о том что 90% кода обрабатывает исключения а 10% делает работу - это цитата человека который врядли писал что-то что потом используют другие… поскольку работа програмы включает в себя и надежность и предсказуемость и повторяемость результатов и масштабируемость и возможность использовать код еще кем-то, все то что отличает комерческий код профи от кода студента.

Привет, Хабр! Представляю вашему вниманию перевод статьи Fixing 7 Common Java Exception Handling Mistakes автора Thorben Janssen.

Обработка исключения является одной из наиболее распространенных, но не обязательно одной из самых простых задач. Это все еще одна из часто обсуждаемых тем в опытных командах, и есть несколько передовых методов и распространенных ошибок, о которых вы должны знать.

Вот несколько вещей, которые следует избегать при обработке исключений в вашем приложении.

Ошибка 1: объявление java.lang.Exception или java.lang.Throwable

Как вы уже знаете, вам нужно либо объявить, либо обработать проверяемое исключение. Но проверяемые исключения - это не единственные, которые вы можете указать. Вы можете использовать любой подкласс java.lang.Throwable в предложении throws. Таким образом, вместо указания двух разных исключений, которые выбрасывает следующий фрагмент кода, вы можете просто использовать исключение java.lang.Exception в предложении throws.

Public void doNotSpecifyException() throws Exception { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }
Но это не значит, что вы должны это сделать. Указание Exeption или Throwable делает почти невозможным правильное обращение с ними при вызове вашего метода.Единственная информация, которую получает вызывающий вами метод, заключается в том, что что-то может пойти не так. Но вы не делитесь какой-либо информацией о каких-либо исключительных событиях, которые могут произойти. Вы скрываете эту информацию за обобщенными причинами выброса исключений.Становится еще хуже, когда ваше приложение меняется со временем. Выброс обобщенных исключений скрывает все изменения исключений, которые вызывающий должен ожидать и обрабатывать. Это может привести к нескольким непредвиденным ошибкам, которые необходимо найти в тестовом примере вместо ошибки компилятора.

Используйте конкретные классы

Гораздо лучше указать наиболее конкретные классы исключений, даже если вам приходится использовать несколько из них. Это сообщает вызывающему устройству, какие исключительные событий нужно обрабатывать. Это также позволяет вам обновить предложение throw, когда ваш метод выдает дополнительное исключение. Таким образом, ваши клиенты знают об изменениях и даже получают ошибку, если вы изменяете выбрасываемые исключения. Такое исключение намного проще найти и обработать, чем исключение, которое появляется только при запуске конкретного тестового примера.

Public void specifySpecificExceptions() throws NumberFormatException, IllegalArgumentException { doSomething(); }

Ошибка 2: перехват обобщенных исключений

Серьезность этой ошибки зависит от того, какой программный компонент вы реализуете, и где вы обнаруживаете исключение. Возможно, было бы хорошо поймать java.lang.Exception в основном методе вашего приложения Java SE. Но вы должны предпочесть поймать определенные исключения, если вы реализуете библиотеку или работаете над более глубокими слоями вашего приложения.

Это дает несколько преимуществ. Такой подход позволяет обрабатывать каждый класс исключений по-разному и не позволяет вам перехватывать исключения, которых вы не ожидали.

Но имейте в виду, что первый блок catch, который обрабатывает класс исключения или один из его супер-классов, поймает его. Поэтому сначала обязательно поймайте наиболее специфический класс. В противном случае ваши IDE покажут сообщение об ошибке или предупреждении о недостижимом блоке кода.

Try { doSomething(); } catch (NumberFormatException e) { // handle the NumberFormatException log.error(e); } catch (IllegalArgumentException e) { // handle the IllegalArgumentException log.error(e); }

Ошибка 3: Логирование и проброс исключений

Это одна из самых популярных ошибок при обработке исключений Java. Может показаться логичным регистрировать исключение там, где оно было брошено, а затем пробросить его вызывающему объекту, который может реализовать конкретную обработку для конкретного случая использования. Но вы не должны делать это по трем причинам:

1. У вас недостаточно информации о прецеденте, который хочет реализовать вызывающий объект вашего метода. Исключение может быть частью ожидаемого поведения и обрабатываться клиентом. В этом случае нет необходимости регистрировать его. Это добавит ложное сообщение об ошибке в файл журнала, который должен быть отфильтрован вашей операционной группой.

2. Сообщение журнала не предоставляет никакой информации, которая еще не является частью самого исключения. Его трассировка и трассировка стека должны содержать всю необходимую информацию об исключительном событии. Сообщение описывает это, а трассировка стека содержит подробную информацию о классе, методе и строке, в которой она произошла.

3. Вы можете регистрировать одно и то же исключение несколько раз, когда вы регистрируете его в каждом блоке catch, который его ловит. Это испортит статистику в вашем инструменте мониторинга и затрудняет чтение файла журнала для ваших операций и команды разработчиков.

Регистрируйте исключение там, где вы его обрабатываете

Таким образом, лучше всего регистрировать исключение тогда, когда вы его обрабатываете. Как в следующем фрагменте кода. Метод doSomething генерирует исключение. Метод doMore просто указывает его, потому что у разработчика недостаточно информации для его обработки. Затем он обрабатывается в методе doEvenMore, который также записывает сообщение журнала.

Public void doEvenMore() { try { doMore(); } catch (NumberFormatException e) { // handle the NumberFormatException } catch (IllegalArgumentException e) { // handle the IllegalArgumentException } } public void doMore() throws NumberFormatException, IllegalArgumentException { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }

Ошибка 4: использование исключений для управления потоком

Использование исключений для управления потоком вашего приложения считается анти-шаблоном по двум основным причинам:

Они в основном работают как оператор Go To, потому что они отменяют выполнение блока кода и переходят к первому блоку catch, который обрабатывает исключение. Это делает код очень трудным для чтения.

Они не так эффективны, как общие структуры управления Java. Как видно из названия, вы должны использовать их только для исключительных событий, а JVM не оптимизирует их так же, как и другой код.Таким образом, лучше использовать правильные условия, чтобы разбить свои циклы или инструкции if-else, чтобы решить, какие блоки кода должны быть выполнены.

Ошибка 5: удалить причину возникновения исключения

Иногда вам может понадобиться обернуть одно исключение в другое. Возможно, ваша команда решила использовать специальное исключение для бизнеса с кодами ошибок и единой обработкой. Нет ничего плохого в этом подходе, если вы не устраните причину.

Когда вы создаете новое исключение, вы всегда должны устанавливать первоначальное исключение в качестве причины. В противном случае вы потеряете трассировку сообщения и стека, которые описывают исключительное событие, вызвавшее ваше исключение. Класс Exception и все его подклассы предоставляют несколько методов-конструкторов, которые принимают исходное исключение в качестве параметра и задают его как причину.

Ошибка 6: Обобщение исключений

Когда вы обобщаете исключение, вы ловите конкретный, например, NumberFormatException, и вместо этого генерируете неспецифическое java.lang.Exception. Это похоже, но даже хуже, чем первая ошибка, которую я описал в этой статье. Он не только скрывает информацию о конкретном случае ошибки на вашем API, но также затрудняет доступ.

Public void doNotGeneralizeException() throws Exception { try { doSomething(); } catch (NumberFormatException e) { throw new Exception(e); } catch (IllegalArgumentException e) { throw new Exception(e); } }
Как вы можете видеть в следующем фрагменте кода, даже если вы знаете, какие исключения может вызвать метод, вы не можете просто их поймать. Вам нужно поймать общий класс Exception и затем проверить тип его причины. Этот код не только громоздкий для реализации, но его также трудно читать. Становится еще хуже, если вы сочетаете этот подход с ошибкой 5. Это удаляет всю информацию об исключительном событии.

Try { doNotGeneralizeException(); } catch (Exception e) { if (e.getCause() instanceof NumberFormatException) { log.error("NumberFormatException: " + e); } else if (e.getCause() instanceof IllegalArgumentException) { log.error("IllegalArgumentException: " + e); } else { log.error("Unexpected exception: " + e); } }
Итак, какой подход лучший?

Будьте конкретны и сохраняйте причину возникновения исключения.

Исключения, которые вы бросаете, должны всегда быть максимально конкретными. И если вы оборачиваете исключение, вы также должны установить исходный исключение в качестве причины, чтобы не потерять трассировку стека и другую информацию, описывающую исключительное событие.

Try { doSomething(); } catch (NumberFormatException e) { throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR); } catch (IllegalArgumentException e) { throw new MyBusinessException(e, ErrorCode.UNEXPECTED); }

Ошибка 7: добавление ненужных преобразований исключений

Как я уже объяснял ранее, может быть полезно обернуть исключения в пользовательские, если вы установите исходное исключение в качестве причины. Но некоторые архитекторы переусердствуют и вводят специальный класс исключений для каждого архитектурного уровня. Таким образом, они улавливают исключение в уровне персистентности и переносят его в MyPersistenceException. Бизнес-уровень ловит и обертывает его в MyBusinessException, и это продолжается до тех пор, пока оно не достигнет уровня API или не будет обработано.

Public void persistCustomer(Customer c) throws MyPersistenceException { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer try { persistCustomer(c); } catch (MyPersistenceException e) { throw new MyBusinessException(e, e.getCode()); } } public void createCustomer(Customer c) throws MyApiException { // create a Customer try { manageCustomer(c); } catch (MyBusinessException e) { throw new MyApiException(e, e.getCode()); } }
Легко видеть, что эти дополнительные классы исключений не дают никаких преимуществ. Они просто вводят дополнительные слои, которые оборачивают исключение. И хотя было бы забавно обернуть подарок во множестве красочной бумаги, это не очень хороший подход к разработке программного обеспечения.

Обязательно добавьте информацию

Просто подумайте о коде, который должен обрабатывать исключение или о самом себе, когда вам нужно найти проблему, вызвавшую исключение. Сначала вам нужно прорваться через несколько уровней исключений, чтобы найти исходную причину. И до сегодняшнего дня я никогда не видел приложение, которое использовало этот подход, и добавляло полезную информацию с каждым слоем исключения. Они либо обобщают сообщение об ошибке и код, либо предоставляют избыточную информацию.

Поэтому будьте осторожны с количеством настраиваемых классов исключений, которые вы вводите. Вы всегда должны спрашивать себя, дает ли новый класс исключений дополнительную информацию или другие преимущества. В большинстве случаев для достижения этого вам не требуется более одного уровня пользовательских исключений.

Public void persistCustomer(Customer c) { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer throw new MyBusinessException(e, e.getCode()); } public void createCustomer(Customer c) throws MyBusinessException { // create a Customer manageCustomer(c); }

Привет, Хабр! Представляю вашему вниманию перевод статьи Fixing 7 Common Java Exception Handling Mistakes автора Thorben Janssen.

Обработка исключения является одной из наиболее распространенных, но не обязательно одной из самых простых задач. Это все еще одна из часто обсуждаемых тем в опытных командах, и есть несколько передовых методов и распространенных ошибок, о которых вы должны знать.

Вот несколько вещей, которые следует избегать при обработке исключений в вашем приложении.

Ошибка 1: объявление java.lang.Exception или java.lang.Throwable

Как вы уже знаете, вам нужно либо объявить, либо обработать проверяемое исключение. Но проверяемые исключения - это не единственные, которые вы можете указать. Вы можете использовать любой подкласс java.lang.Throwable в предложении throws. Таким образом, вместо указания двух разных исключений, которые выбрасывает следующий фрагмент кода, вы можете просто использовать исключение java.lang.Exception в предложении throws.

Public void doNotSpecifyException() throws Exception { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }
Но это не значит, что вы должны это сделать. Указание Exeption или Throwable делает почти невозможным правильное обращение с ними при вызове вашего метода.Единственная информация, которую получает вызывающий вами метод, заключается в том, что что-то может пойти не так. Но вы не делитесь какой-либо информацией о каких-либо исключительных событиях, которые могут произойти. Вы скрываете эту информацию за обобщенными причинами выброса исключений.Становится еще хуже, когда ваше приложение меняется со временем. Выброс обобщенных исключений скрывает все изменения исключений, которые вызывающий должен ожидать и обрабатывать. Это может привести к нескольким непредвиденным ошибкам, которые необходимо найти в тестовом примере вместо ошибки компилятора.

Используйте конкретные классы

Гораздо лучше указать наиболее конкретные классы исключений, даже если вам приходится использовать несколько из них. Это сообщает вызывающему устройству, какие исключительные событий нужно обрабатывать. Это также позволяет вам обновить предложение throw, когда ваш метод выдает дополнительное исключение. Таким образом, ваши клиенты знают об изменениях и даже получают ошибку, если вы изменяете выбрасываемые исключения. Такое исключение намного проще найти и обработать, чем исключение, которое появляется только при запуске конкретного тестового примера.

Public void specifySpecificExceptions() throws NumberFormatException, IllegalArgumentException { doSomething(); }

Ошибка 2: перехват обобщенных исключений

Серьезность этой ошибки зависит от того, какой программный компонент вы реализуете, и где вы обнаруживаете исключение. Возможно, было бы хорошо поймать java.lang.Exception в основном методе вашего приложения Java SE. Но вы должны предпочесть поймать определенные исключения, если вы реализуете библиотеку или работаете над более глубокими слоями вашего приложения.

Это дает несколько преимуществ. Такой подход позволяет обрабатывать каждый класс исключений по-разному и не позволяет вам перехватывать исключения, которых вы не ожидали.

Но имейте в виду, что первый блок catch, который обрабатывает класс исключения или один из его супер-классов, поймает его. Поэтому сначала обязательно поймайте наиболее специфический класс. В противном случае ваши IDE покажут сообщение об ошибке или предупреждении о недостижимом блоке кода.

Try { doSomething(); } catch (NumberFormatException e) { // handle the NumberFormatException log.error(e); } catch (IllegalArgumentException e) { // handle the IllegalArgumentException log.error(e); }

Ошибка 3: Логирование и проброс исключений

Это одна из самых популярных ошибок при обработке исключений Java. Может показаться логичным регистрировать исключение там, где оно было брошено, а затем пробросить его вызывающему объекту, который может реализовать конкретную обработку для конкретного случая использования. Но вы не должны делать это по трем причинам:

1. У вас недостаточно информации о прецеденте, который хочет реализовать вызывающий объект вашего метода. Исключение может быть частью ожидаемого поведения и обрабатываться клиентом. В этом случае нет необходимости регистрировать его. Это добавит ложное сообщение об ошибке в файл журнала, который должен быть отфильтрован вашей операционной группой.

2. Сообщение журнала не предоставляет никакой информации, которая еще не является частью самого исключения. Его трассировка и трассировка стека должны содержать всю необходимую информацию об исключительном событии. Сообщение описывает это, а трассировка стека содержит подробную информацию о классе, методе и строке, в которой она произошла.

3. Вы можете регистрировать одно и то же исключение несколько раз, когда вы регистрируете его в каждом блоке catch, который его ловит. Это испортит статистику в вашем инструменте мониторинга и затрудняет чтение файла журнала для ваших операций и команды разработчиков.

Регистрируйте исключение там, где вы его обрабатываете

Таким образом, лучше всего регистрировать исключение тогда, когда вы его обрабатываете. Как в следующем фрагменте кода. Метод doSomething генерирует исключение. Метод doMore просто указывает его, потому что у разработчика недостаточно информации для его обработки. Затем он обрабатывается в методе doEvenMore, который также записывает сообщение журнала.

Public void doEvenMore() { try { doMore(); } catch (NumberFormatException e) { // handle the NumberFormatException } catch (IllegalArgumentException e) { // handle the IllegalArgumentException } } public void doMore() throws NumberFormatException, IllegalArgumentException { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }

Ошибка 4: использование исключений для управления потоком

Использование исключений для управления потоком вашего приложения считается анти-шаблоном по двум основным причинам:

Они в основном работают как оператор Go To, потому что они отменяют выполнение блока кода и переходят к первому блоку catch, который обрабатывает исключение. Это делает код очень трудным для чтения.

Они не так эффективны, как общие структуры управления Java. Как видно из названия, вы должны использовать их только для исключительных событий, а JVM не оптимизирует их так же, как и другой код.Таким образом, лучше использовать правильные условия, чтобы разбить свои циклы или инструкции if-else, чтобы решить, какие блоки кода должны быть выполнены.

Ошибка 5: удалить причину возникновения исключения

Иногда вам может понадобиться обернуть одно исключение в другое. Возможно, ваша команда решила использовать специальное исключение для бизнеса с кодами ошибок и единой обработкой. Нет ничего плохого в этом подходе, если вы не устраните причину.

Когда вы создаете новое исключение, вы всегда должны устанавливать первоначальное исключение в качестве причины. В противном случае вы потеряете трассировку сообщения и стека, которые описывают исключительное событие, вызвавшее ваше исключение. Класс Exception и все его подклассы предоставляют несколько методов-конструкторов, которые принимают исходное исключение в качестве параметра и задают его как причину.

Ошибка 6: Обобщение исключений

Когда вы обобщаете исключение, вы ловите конкретный, например, NumberFormatException, и вместо этого генерируете неспецифическое java.lang.Exception. Это похоже, но даже хуже, чем первая ошибка, которую я описал в этой статье. Он не только скрывает информацию о конкретном случае ошибки на вашем API, но также затрудняет доступ.

Public void doNotGeneralizeException() throws Exception { try { doSomething(); } catch (NumberFormatException e) { throw new Exception(e); } catch (IllegalArgumentException e) { throw new Exception(e); } }
Как вы можете видеть в следующем фрагменте кода, даже если вы знаете, какие исключения может вызвать метод, вы не можете просто их поймать. Вам нужно поймать общий класс Exception и затем проверить тип его причины. Этот код не только громоздкий для реализации, но его также трудно читать. Становится еще хуже, если вы сочетаете этот подход с ошибкой 5. Это удаляет всю информацию об исключительном событии.

Try { doNotGeneralizeException(); } catch (Exception e) { if (e.getCause() instanceof NumberFormatException) { log.error("NumberFormatException: " + e); } else if (e.getCause() instanceof IllegalArgumentException) { log.error("IllegalArgumentException: " + e); } else { log.error("Unexpected exception: " + e); } }
Итак, какой подход лучший?

Будьте конкретны и сохраняйте причину возникновения исключения.

Исключения, которые вы бросаете, должны всегда быть максимально конкретными. И если вы оборачиваете исключение, вы также должны установить исходный исключение в качестве причины, чтобы не потерять трассировку стека и другую информацию, описывающую исключительное событие.

Try { doSomething(); } catch (NumberFormatException e) { throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR); } catch (IllegalArgumentException e) { throw new MyBusinessException(e, ErrorCode.UNEXPECTED); }

Ошибка 7: добавление ненужных преобразований исключений

Как я уже объяснял ранее, может быть полезно обернуть исключения в пользовательские, если вы установите исходное исключение в качестве причины. Но некоторые архитекторы переусердствуют и вводят специальный класс исключений для каждого архитектурного уровня. Таким образом, они улавливают исключение в уровне персистентности и переносят его в MyPersistenceException. Бизнес-уровень ловит и обертывает его в MyBusinessException, и это продолжается до тех пор, пока оно не достигнет уровня API или не будет обработано.

Public void persistCustomer(Customer c) throws MyPersistenceException { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer try { persistCustomer(c); } catch (MyPersistenceException e) { throw new MyBusinessException(e, e.getCode()); } } public void createCustomer(Customer c) throws MyApiException { // create a Customer try { manageCustomer(c); } catch (MyBusinessException e) { throw new MyApiException(e, e.getCode()); } }
Легко видеть, что эти дополнительные классы исключений не дают никаких преимуществ. Они просто вводят дополнительные слои, которые оборачивают исключение. И хотя было бы забавно обернуть подарок во множестве красочной бумаги, это не очень хороший подход к разработке программного обеспечения.

Обязательно добавьте информацию

Просто подумайте о коде, который должен обрабатывать исключение или о самом себе, когда вам нужно найти проблему, вызвавшую исключение. Сначала вам нужно прорваться через несколько уровней исключений, чтобы найти исходную причину. И до сегодняшнего дня я никогда не видел приложение, которое использовало этот подход, и добавляло полезную информацию с каждым слоем исключения. Они либо обобщают сообщение об ошибке и код, либо предоставляют избыточную информацию.

Поэтому будьте осторожны с количеством настраиваемых классов исключений, которые вы вводите. Вы всегда должны спрашивать себя, дает ли новый класс исключений дополнительную информацию или другие преимущества. В большинстве случаев для достижения этого вам не требуется более одного уровня пользовательских исключений.

Public void persistCustomer(Customer c) { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer throw new MyBusinessException(e, e.getCode()); } public void createCustomer(Customer c) throws MyBusinessException { // create a Customer manageCustomer(c); }

Как известно, исключения - это события, возникающие при выполнении программы. Эти события нарушают нормальный ход вещей.

Блок try, блок catch

Часть кода, где возможно возникновение исключительной ситуации, охватывают блоком try.

Обработчики исключений объявляются в блоке catch, который следует сразу за блоком try.

Блоков catch может быть несколько для одного блока try.

В блоке catch идёт обработка исключений, а его аргумент указывает тип исключения, который может обработать данный блок, т.е. тип аргумента есть тип исключения. Код, записанный в блоке catch есть обработчик исключения.

Блок finally

После блока(ов) ислючения может располагаться блок finally. Приемлем случай, когда нет блоков catch, но есть блок finally. Код блока finally выполняется всегда, а не только при возникновении исключения. В нем можно выполнять обязательные завершающие действия, очистку, например, закрыть поток, а не в блоке try. После finally выполнение программы продолжается со следующей строки кода.

Схематично всё выглядет так:

Public void aFunc() { try { //код } catch(TypeException e) { //код обработчика исключения типа //TypeException; } catch(SecondTypeException e) { //код обработчика исключения типа //SecondTypeException; } finally { //обязательные завершающие действия. //Код блока finally //выполняется всегда, а не только при //возникновении исключения. } }

а может быть без блоков catch, вот так:

Public void aFunc() { try { //код } finally { //обязательные завершающие действия. //Код блока finally //выполняется всегда, а не только при //возникновении исключения. } }

или без блока finally:

Public void aFunc() { try { //код } catch(TypeException e) { //код обработчика исключения типа //TypeException; } }

Рассмотрим подробнее, что происходит при возникновении исключительной ситуации.

При возникновении исключительной ситуации, формируется объект исключения, содержащий информацию об этом исключении, который передаётся системе. В таких случаях говорят, что соотвестсвующая часть программы выбрасывает исключение, т.е. формирует объект исключения и передает его системе. Далее начинается работа системы, но прежде чем мы её разберём, надо понять что такое стек вызовов.

Врезка

Стек вызовов(call stack)

Стек вызовов(call stack) – это пследовательность вызовов методов, начиная от метода main до интересующего нас метода. Рассмотрим условный пример. Есть класс:

Public class AClass { public AClass() {} public void methodC() { } public void methodB() { methodC(); } public void methodA() { try { methodB(); } catch(AnException e) { System.out.println(e.getMessage()); } } public static void main(String args) { AClass ac = new Aclass(); ac.methodA(); } }

Здесь стек вызовов такой: main вызывает methodA(), methodA() вызывает methodB(), methodB() вызывает methodС().

Конец врезки

Продолжим изучение действий системы после получения ею объекта исключения (предположим, что тип объекта исключения - AnException), выброшенного каким-то методом. Пусть это будет метод methodС(), см. врезку «Стек вызовов » выше. Система начинает просматривать стек вызовов для methodС() в обратном порядке, т.е, стартуя с самого метода methodС(), перемещается в напралении main. Цель этого просмотра – найти обработчик исключения, возникшего в methodС(). Сначала система просматривает methodС(), если в нём нет нужного обработчика исключения, то переходит к methodB(), если и в нём нет, то переходит к methodA(). methodA() содержит обработчик типа AnException, вот этому-то обработчику и передаёт система объект исключения на обработку. Говорят, что обработчик захватывает (catch) исключение. Если система не нашла бы обработчик ислючения нужного типа, то выполнение программы было бы завершено.

Если бы у methodA() было несколько блоков catch, то система выбрала бы первый подходящий блок, т.е. такой блок, тип аргумента которого совпадает с типом исключения.

Три категории исключений

Контролируемые (checked) исключения . Например, ошибки ввода. Такие исключения рекомендуется обрабатывать;

Ошибка (error) . Это исключения, вызванные внешними, по отношению к приложению, причинами и приложение их, как правило, не обрабатывает. Такие исключения определяются классом Error или его подклассами;

Исключения времени выполнения (runtime exception) . Например, ошибки в коде. Такие исключения определяются классом RuntimeException или его подклассами. Эти исключения могут обрабатываться.

Исключения второго и третьего типа называют неконтролируемые (unchecked) исключения .

Метод может не обрабатывать исключение, но, предполагая, что оно может возникнуть, передать его на обработку в стек вызовов. Тогда в объявлении метода пишут оператор throws и в нём через запятую перечень возможных типов исключений. Пример:

Public void methodC(int a) throws AnException { //code; }

Метод может сам выбросить исключение. Для этого используют оператор throw и объект исключения в нём. Пример:

Public void methodC(int a) throws AnException { if(a < 10) { throw new AnException("Error in methodC"); } }

Если этом примере a < 10, то будет выброшено исключение AnException, так, как если бы здесь произошла соответствующая ошибка.

Выбросить исключение оператором throw можно и из тела оработчика другого исключения. Так возникает цепочка исключений (chained exceptions) . Пример:

Public void methodA() { try { methodB(); } catch(AnException e) { System.out.println(e.toString()); throw new NextException("methodA throws NextException."); } }

Все классы исключений восходят к классу Throwable . Непосредственными наследниками его являются классы Error и Exception . От Exception происходят RuntimeException и все классы контролируемых исключений.

Мы уже знаем, что исключения типа Error обрабатывает система, мы их обычно не касаемся.

Исключения типа RuntimeException говорят об ошибках в коде. Такие исключения могут обрабатываться, но лучше просто исправить ошибки кода.

Исключения типа Exception говорят о наличии проблем, но они не являются серьёзными системными. Такие ошибки следует обрабатывать.

Можно создавать свои классы исключений как прямых или нет наследков Exception . Принято к названиям таких классов добавлять слово Exception .

Если тип аргумента оператора catch есть Exception , то соотвестсвующий обработчик исключений сможет захватить все контролируемые исключения и исключения типа RuntimeException . Так можно поступить, если нет особых требований к обработчику или если могут возникнуть непредвиденные исключения. Но если требуется особый подход, то лучше использовать наследников Exception .

Как самому создать класс исключения?

Так как мы должны обрабатывать контролируемые исключения, то унаследуем наш класс от Exception .

Пример

В заключении привожу полный код примера, который рассматривался на протяжении этого раздела. Главный класс:

Package exceptionTest.test; public class TestClass { public TestClass() { } public void methodC(int a) throws AnException { if(a < 10) { throw new AnException("Error in methodC"); } } public void methodB() throws AnException { methodC(1); } public void methodA() { try { methodB(); } catch(AnException e) { System.out.println(e.toString()); throw new NextException("methodA throws NextException."); } } public static void main(String args) { TestClass tc = new TestClass(); try { tc.methodA(); } catch(NextException ex) { System.out.println(ex.toString()); } finally { System.out.print("Message from finally: The end."); } } }

Класс исключения AnException:

Public class AnException extends Exception { private static final long serialVersionUID = 1L; private String exceptionMessage; AnException() { exceptionMessage = null; } AnException(String exceptionMessage) { this.exceptionMessage = exceptionMessage; } public String toString() { return "Message from AnException: " + exceptionMessage; } }

Класс исключения NextException:

Package exceptionTest.test; public class NextException extends RuntimeException { private static final long serialVersionUID = 1L; private String exceptionMessage; NextException() { exceptionMessage = null; } NextException(String exceptionMessage) { this.exceptionMessage = exceptionMessage; } public String toString() { return "Message from NextException: " + exceptionMessage; } }

Оператор throw используется для возбуждения исключения «вручную». Для того чтобы сделать это, нужно иметь объект подкласса класса Throwable, который можно либо получить как параметр оператора catch, либо создать с помощью оператора new. Ниже приведена общая форма оператораthrow ,

throw ОбъектТипа Throwable ;

При достижении этого оператора нормальное выполнение кода немедленно прекращается, так что следующий за ним оператор не выполняется. Ближайший окружающий блок try проверяется на наличие соответствующего возбужденному исключению обработчика catch. Если такой отыщется, управление передается ему. Если нет, то проверяется следующий из вложенных операторов try и так до тех пор, пока либо не будет найден подходящий раздел catch, либо обработчик исключений исполняющей системы Java не остановит программу, выведя при этом состояние стека вызовов. Ниже приведен пример, в котором сначала создается объект-исключение, затем оператор throw возбуждает исключительную ситуацию, после чего то же исключение возбуждается повторно - на этот раз уже кодом перехватившего его в первый раз раздела catch.

class ThrowDemo {
static void demoproc() {
{
throw new NullPointerException("demo");
}
catch (NullPointerException e) {
System.out.println("caught inside demoproc");
throw e;
}
}

try {
demoproc();
}
catch(NulPointerException e) {
System.out.println("recaught:" + e);
}
}
}

В этом примере обработка исключения проводится в два приема. Метод main создает контекст для исключения и вызывает demoproc. Метод demoproc также устанавливает контекст для обработки исключения, создает новый объект класса NullPointerException и с помощью оператора throw возбуждает это исключение. Исключение перехватывается в следующей строке внутри метода demoproc, причем объект-исключение доступен коду обработчика через параметр «е». Код обработчика выводит сообщение о том, что возбуждено исключение, а затем снова возбуждает его с помощью оператора throw, в результате чего оно передается обработчику исключений в методе main. Ниже приведен результат, полученный при запуске этого примера.

С:\> java ThrowDemo
caught inside demoproc
recaught: java.lang.NullPointerException: demo

9.8. Оператор throws

Если метод способен возбуждать исключения, которые он сам не обрабатывает, он должен объявить о таком поведении, чтобы вызывающие методы могли защитить себя от этих исключений. Для задания списка исключений, которые могут возбуждаться методом, используется оператор throws. Если метод в явном виде (т.е. с помощью оператора throw) возбуждает исключение соответствующего класса, тип класса исключений должен быть указан в операторе throws в объявлении этого метода. С учетом этого наш прежний синтаксис определения метода должен быть расширен следующим образом:

тип имя_метода(список аргументов) throws список исюпочений {}

Ниже приведен пример программы, в которой метод procedure пытается возбудить исключение, не обеспечивая ни программного кода для его перехвата, ни объявления этого исключения в заголовке метода. Такой программный код не будет оттранслирован.

class ThrowsDemo 1 {
static void procedure ()
{
System.out.println("inside procedure");
}
public static void main(String args) {
procedure ();
}
}

Для того чтобы мы смогли оттранслировать этот пример, нам придется сообщить транслятору, что procedure может возбуждать исключения типа IllegalAccessException и в методе main добавить код для обработки этого типа исключений:

class ThrowsDemo {
static void procedure() throws IllegalAccessException
{
System.out.println(" inside procedure");
throw new IllegalAccessException("demo");
}
public static void main(String aigs) {
try {
procedure();
}
catch (IllegalAccessException e) {
System.out.println("caught" + e);
}
}
}

Ниже приведен результат выполнения этой программы.

С :\> java ThrowsDemo
inside procedure
caught javaJangIllegalAccessException: demo