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-винятки:
|
Виклик цього методу можна обробити трьома способами:
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.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ