Установка виключення в параметрах методу, dev64

Припустимо, у нас є метод класу, за певних умов кидає виняток. Чи можна в параметрах методу задати виключення, яке цей метод буде кидати? Питання це, здавалося б, простий, однак зробити це не так просто, як здається.

Наприклад, наведений нижче код, який демонструє установку виключення, реалізує відразу два антипаттерн обробки винятків:

По-перше, цей метод викидає безпосередньо клас Exception. що повністю ламає нормальну ієрархію обробки винятків. По-друге, він втрачає root cause, через якого, власне, і кинуто виключення. Іншими словами, в даному випадку першопричиною є IOException e. однак якщо на верхньому рівні виняток буде оброблено і буде роздрукований його стектрейс, то IOException -а в стек-Трейсі не буде. Це друга проблема. Вирішити обидві проблеми можна застосувавши підхід, продемонстрований нижче:

В даному випадку, щоб викидалося не просто Exception. а конкретний переданий тип виключення, застосовується generic-метод. Друга проблема з root cause вирішується за допомогою методу initCause () класу Throwable. initCause () виставляє root cause у виключення. initCause () влаштований так, що виставити root cause можна лише один раз. Його не можна викликати повторно. Тому, мабуть, він і називається initCause (). а не setCause (). Нижче наведено юніт-тест, що демонструє використання отриманого методу:

В результаті роздруковується такий стектрейс:


exceptions.MyException: This is my exception
at exceptions.MyBusinessClassTest.test (MyBusinessClassTest.java:11)
.
Caused by: java.io.IOException: IOException cause
at exceptions.MyBusinessClass.doOrThrow (MyBusinessClass.java:9)
. 27 more

Тобто видно, що в стек-Трейсі є root cause «IOException cause».

Корисною модифікацією вищенаведеного коду буде заміна Throwable на Exception. тому що з двох підкласів Throwable тільки один, Exception. призначений безпосередньо для використання програмістами, і явне застосування Throwable є code smell.
Іншими словами T extends Exception, а не Throwable:

Чим такий підхід може бути незручний? Згідно best practices по використанню винятків (див. Наприклад, Джош Блох «Effective Java»), разом з виключенням необхідно зберігати якомога більше корисної інформації, яка може вказати на причину помилки. Наприклад, буває корисно скласти деяке повідомлення у виключенні, причому це повідомлення містить потенційно мінливі частини. наприклад:

Тут ми складаємо вміст повідомлення у ValidationException всередині catch-блоку. У цьому коді немає нічого поганого, він абсолютно правильний. Однак якщо створювати таким чином виняток із застосуванням підходу, описаного вище, наприклад, так:

то вийде, що ми будемо створювати об'єкт виключення кожен раз, коли цей код виконається. незалежно від того, чи відбудеться помилка насправді чи ні. Саме створення об'єкта не так вже й погано (це дуже дешева операція в Java), але ось форматування повідомлення може бути потенційно дорогим. У більш потужних мовах, таких, як Scala, проблема вирішується дуже просто.

Різницю становить лише наявність => в списку параметрів у другому методі. Цей параметр e в другому варіанті є call by name-параметрів, значення якого буде вирахувано тільки тоді, коли його використання буде зустрінуте всередині тіла методу, причому потенційно кілька разів. Використання ж цих методів не відрізняється нічим:

Різниця тут проявиться при виконанні. У першому випадку об'єкт ValidationException буде створений в будь-якому випадку, у другому - тільки тоді, коли він знадобиться, тобто при виникненні помилки.

На жаль, Java не надає такого зручного механізму відкладених обчислень. Але ж Java і Scala - Тьюринг-повні, так що в Java можна реалізувати той же, що і в Scala 🙂 Для цього скористаємося вельми популярним патерном - provider. Оголосимо інтерфейс:

і зробимо так, щоб наш метод orThrow брав в якості аргументу саме ExceptionProvider. а не голий Exception:

Тепер створення об'єктів винятків відбувається тільки тоді, коли це дійсно необхідно. Використовувати метод orThrow () можна, наприклад, так:

Виглядає дуже громіздко, але це не є проблемою, принаймні, з двох причин. По-перше, в наступній версії Java (Java 8) в мову будуть додані довгоочікувані лямбда, що дозволить скоротити код вище до такого:

Не сильно довше, ніж Scala. А по-друге, провайдери можна абстрагувати, наприклад, із застосуванням відображення:

Цей провайдер створить виняток заданого класу з зазначеним повідомленням, відформатованим за допомогою переданих аргументів:

Зрозуміло, що для того, щоб це працювало, у класу винятку повинен бути конструктор з одним аргументом-рядком.

Використовується це цілком очевидним способом:

Тепер код став досить легкочитаємий і не несе проблем із зайвими операціями, і при цьому дуже гнучким.

Інші статті по Java дивіться в розділі Java