1. Види виключень

Усі виключення поділяються на 4 види, які насправді є класами, успадкованими один від одного.
Клас Throwable
Найбазовішим класом для всіх виключень є клас Throwable. У класі Throwable зберігається код, який записує поточний стек-трейс викликів функцій у масив. Що таке стек-трейс викликів, ми вивчимо трохи пізніше.
В оператор throw можна передати лише об'єкт класу-нащадка Throwable. І хоча теоретично можна написати код виду throw new Throwable(); — так зазвичай ніхто не робить. Головна мета існування класу Throwable — єдиний клас-предок для всіх виключень.
Клас Error
Наступним класом виключень є клас Error — прямий нащадок класу Throwable. Об'єкти типу Error (і його класів-нащадків) створює Java-машина у випадку якихось серйозних проблем. Наприклад, збій у роботі, нестача пам'яті тощо.
Зазвичай ви як програміст нічого не можете зробити у ситуації, коли в програмі виникла помилка типу Error: занадто серйозна така помилка. Усе, що ви можете зробити — повідомити користувача, що програма аварійно завершує роботу, або записати всю відому інформацію про помилку в лог програми.
Клас Exception
Виключення типу Exception (і RuntimeException) — це звичайні помилки, які виникають під час роботи багатьох методів. Мета кожного виняткового випадку — бути обробленим тим блоком catch, який знає, що потрібно зробити в цій ситуації.
Коли якийсь метод не може виконати свою роботу через якусь причину, він одразу має повідомити про це викликаючий метод, викидаючи виключення відповідного типу.
Іншими словами, якщо якась змінна виявилася рівною null, метод викине NullPointerException, якщо в метод передали невірні аргументи — викине InvalidArgumentException, якщо в методі випадково було ділення на нуль — ArithmeticException.
Клас RuntimeException
RuntimeException — це різновид (підмножина) виключень Exception. Можна навіть сказати, що RuntimeException — це полегшена версія звичайних виключень (Exception): на такі виключення накладається менше вимог і обмежень
Про відмінність Exception і RuntimeException ви дізнаєтесь далі
2. Перевіряємі винятки: throws, checked exceptions
Усі винятки в Java поділяються на 2 категорії — перевіряємі (checked) і неперевіряємі (unchecked).
Усі винятки, успадковані від класів RuntimeException і Error, вважаються unchecked-винятками, усі інші — checked-винятками.
Через 20 років після введення перевіряємих винятків майже всі Java-програмісти вважають це помилкою. 95% усіх винятків у популярних сучасних фреймворках — неперевіряємі. Та сама мова C#, яка майже скопіювала Java, не стала додавати checked-винятки.
У чому ж основна відмінність checked-винятків від unchecked?
До checked-винятків є додаткові вимоги. Вони звучать приблизно так.
Вимога 1
Якщо метод кидає checked-виняток, він повинен містити тип цього винятку у своєму заголовку (сигнатурі методу). Щоб усі методи, які викликають даний метод, знали про те, що в ньому може виникнути таке «важливе виключення».
Вказувати checked-винятки потрібно після параметрів методу після ключового слова throws (не плутати із throw). Виглядає це приблизно так:
тип метод (параметри) throws виняток
Приклад:
| checked-виняток | unchecked-виняток |
|---|---|
|
|
У прикладі праворуч наш код кидає unchecked-виняток — ніяких додаткових дій не потрібно. У прикладі ліворуч метод кидає checked-виняток, тому у сигнатуру методу додали ключове слово throws і вказали тип винятку.
Якщо метод планує кидати кілька checked-винятків, усі їх потрібно вказати після ключового слова throws через кому. Порядок не важливий. Приклад:
public void calculate(int n) throws Exception, IOException
{
if (n == 0)
throw new Exception("n дорівнює нулю!");
if (n == 1)
throw new IOException("n дорівнює одиниці");
}
Вимога 2
Якщо ви викликаєте метод, у якого у сигнатурі прописані checked-винятки, то ви не можете проігнорувати цей факт.
Ви повинні або перехопити всі ці винятки, додавши блоки catch для кожного з них, або додати їх у throws свого методу.
Ми ніби кажемо собі: ці винятки настільки важливі, що ми обов'язково повинні їх перехопити. А якщо ми не знаємо, як їх перехопити, ми повинні повідомити тим, хто буде викликати наш метод, що в ньому можуть виникнути такі винятки.
Приклад:
Уявимо, що ми пишемо метод, який повинен створити світ, населений людьми. Початкова кількість людей передається як параметр. Тоді ми повинні додати винятки, якщо людей занадто мало.
| Створюємо Землю | Примітка |
|---|---|
|
Метод потенційно кидає два checked-винятки: |
- ПустийСвіт
- СамотнійСвіт
Виклик цього методу можна обробити 3 способами:
1. Не перехоплюємо виникаючі винятки
Найчастіше це робиться у випадку, якщо у методі невідомо, як правильно обробити цю ситуацію.
| Код | Примітка |
|---|---|
|
Викликаючий метод не перехоплює винятки і змушений інформувати про них інших: додати їх собі у throws |
2. Перехоплюємо частину винятків
Обробляємо зрозумілі помилки, незрозумілі — перекидаємо у викликаючий метод. Для цього потрібно додати їх назву у throws:
| Код | Примітка |
|---|---|
|
Викликаючий метод перехоплює тільки одне checked-виняток – СамотнійСвіт, друге він повинен додати у свою сигнатуру: вказати після слова throws |
3. Перехоплюємо всі винятки
Якщо метод не перекидає винятки викликаючому методу, викликаючий метод завжди буде впевнений, що все виконалося добре. І не зможе вжити жодних дій, щоб виправити ситуацію.
| Код | Примітка |
|---|---|
|
У цьому методі перехоплюються всі помилки. Викликаючий метод буде впевнений, що все пройшло чудово. |
3. Обгортання виключень
Checked-виключення здавалися класною штукою в теорії, але виявилися повним розчаруванням на практиці.
Припустімо, у вас в проєкті є суперпопулярний метод, який викликається з сотень місць програми. І ви вирішили додати в нього нове checked-виключення. І цілком може виявитися, що це checked-виключення дійсно настільки важливе та особливе, що тільки метод main() знає, що робити у разі перехоплення цього виключення.
Тому вам доведеться додати checked-виключення в throws усіх методів, які викликають ваш суперпопулярний метод. А також у throws усіх методів, які викликають ті методи. І в методи, які викликають ті методи.
У результаті у вас в throws у половини методів проєкту буде додано нове checked-виключення. А потім виявиться, що у вас проект покритий тестами, і тести не компілюються. І вам доведеться правити throws ще й у тестах.
А потім увесь ваш код (зміни в сотнях файлів) мають будуть рев'ювати інші програмісти. І тут ми задаємо собі питання: а заради чого ми вносили в проєкт купу змін? День(дні?) роботи, зламані тести, і все заради додавання одного checked-виключення?
А адже є ще проблеми з наслідуванням і перевизначенням методів. Проблем від checked-виключень значно більше, ніж користі. Зрештою, зараз мало хто їх любить і мало хто використовує.
Проте все одно багато коду (включаючи код стандартних бібліотек Java) містять ті самі checked-виключення. І що ж з ними робити? Ігнорувати не можна, обробляти – невідомо як.
Java-програмісти запропонували «загортати» checked-виключення всередину RuntimeException. Іншими словами, перехоплювати всі checked-виключення, створювати замість них unchecked-виключення (наприклад, RuntimeException) і викидати вже їх. Виглядає це все приблизно так:
try
{
код, де може виникнути checked-виключення
}
catch(Exception exp)
{
throw new RuntimeException(exp);
}
Не дуже красиве рішення, але нічого кримінального: виключення просто поклали всередину виключення RuntimeException.
За бажанням його можна звідти легко дістати. Приклад:
| Код | Примітка |
|---|---|
|
Отримуємо виключення, збережене всередині об'єкта RuntimeException. cause може бути nullВизначаємо його тип і перетворюємо в змінну checked-типу. |
4. Множинне перехоплення винятків
Програмісти дуже не люблять дублювання коду. Навіть придумали такий принцип розробки — DRY: Don’t Repeat Yourself. Проте при обробці винятків часто виникають ситуації, коли після блоку try слідує кілька блоків catch з однаковим кодом.
Або може бути, наприклад, 3 catch-блоки з одним кодом і ще 2 catch-блоки з іншим. Стандартна, в загальному-то, ситуація, коли у вас в проєкті відповідально ставляться до обробки винятків.
Починаючи з 7-ї версії, в мову Java додали можливість вказати кілька типів винятків в одному блоці catch. Це виглядає приблизно так:
try
{
код, де може виникнути помилка
}
catch(ТипВинятку1 | ТипВинятку2 | ТипВинятку3 ім'я)
{
код обробки винятків
}
Блоків catch може бути скільки завгодно. Проте в одному блоці catch не можна вказати винятки, які успадковані один від одного. Тобто не можна написати catch (Exception | RuntimeException e), тому що клас RuntimeException успадкований від Exception.
5. Власні виключення
Ти завжди можеш створити свій власний клас-виключення. Просто успадкувавши клас від, наприклад, RuntimeException. Виглядати це буде приблизно так:
class Ім'яКласу extends RuntimeException
{
}
Деталі ми обговоримо, коли ти вивчиш ООП, наслідування, конструктори та перевизначення методів.
Однак, навіть якщо у тебе є тільки такий простий клас — взагалі без коду, ти все одно можеш кидати його виключення:
| Код | Примітка |
|---|---|
|
Кидаємо unchecked-виключення MyException. |
Детальну роботу зі своїми виключеннями ми розглянемо у квесті Java Multithreading.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ