1. Види винятків

Види винятків у Java

Усі винятки поділяються на 4 види, які насправді є класами, успадкованими один від одного.

Клас Throwable

Головним базовим класом для всіх винятків є клас Throwable. У класі Throwable міститься код, який записує в масив поточну трасу стеку викликів функцій. Що таке траса стеку викликів, ми дізнаємося трохи згодом.

В оператор throw можна передати тільки об'єкт класу-спадкоємця Throwable. І хоча теоретично можна написати код на кшталт throw new Throwable(); — зазвичай так ніхто не робить. Головна мета існування класу Throwable полягає в тому, що це єдиний клас-предок для всіх винятків.

Клас Error

Наступним класом винятків є клас Error — прямий спадкоємець класу Throwable. Об'єкти типу Error (і його класів-спадкоємців) створює Java-машина у разі виникнення серйозних проблем, як-от: збою в роботі, браку пам'яті тощо.

Зазвичай ви як програміст нічого не можете зробити в ситуації, коли в програмі виникла помилка типу Error: вона надто серйозна. Усе, що ви можете зробити, — повідомити користувачу, що програма аварійно завершується, або записати всю відому інформацію про помилку в лог програми.

Клас Exception

Винятки типу ExceptionRuntimeException) — це звичайні помилки, які виникають під час роботи багатьох методів. Мета кожного викинутого винятку — бути захопленим тим блоком catch, який знає, що потрібно зробити в цій ситуації.

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

Інакше кажучи, якщо якась змінна виявилася рівною null, метод кине NullPointerException, якщо в метод передали неправильні аргументи — кине InvalidArgumentException, якщо в методі випадково відбулося ділення на нуль — ArithmeticException.

Клас RuntimeException

RuntimeException — це різновид (підмножина) винятків Exception. Можна навіть сказати, що RuntimeException — це полегшена версія звичайних винятків (Exception): щодо таких винятків застосовується менше вимог і обмежень.

Про різницю між Exception і RuntimeException ви дізнаєтеся далі.


2. Винятки, що перевіряються: throws, checked exceptions

Винятки, що перевіряються: 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-виняток
public void calculate(int n) throws Exception
{
   if (n == 0)
      throw new Exception("n дорівнює нулю!");
}
public void calculate(n)
{
   if (n == 0)
      throw new RuntimeException("n дорівнює нулю!");
}

У прикладі праворуч наш код кидає 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 свого методу.

Ми немовби говоримо собі: ці винятки настільки важливі, що ми обов'язково повинні їх перехопити. А якщо ми не знаємо, як їх перехопити, ми повинні повідомити тим, хто буде викликати наш метод, що в ньому можуть виникнути такі винятки.

Приклад:

Уявімо, що ми пишемо метод, який має створити світ, населений людьми. Початкова кількість людей передається як параметр. Тоді ми повинні додати винятки, якщо людей надто мало.

Створюємо Землю Примітка
public void створитиСвіт(int n) throws ПорожнійСвіт,ОдинокийСвіт
{
   if (n == 0)
      throw new ПорожнійСвіт("Людей взагалі немає!");
   if (n == 1)
      throw new ОдинокийСвіт("Надто мало людей!");
   System.out.println("Створено прекрасний світ. Населення: " + n);
}
Метод потенційно кидає два checked-винятки:

  • ПорожнійСвіт
  • ОдинокийСвіт

Виклик цього методу можна обробити трьома способами:

1. Не перехоплюємо винятки, що виникають

Найчастіше так діють тоді, коли в методі невідомо, як правильно обробити цю ситуацію.

Код Примітка
public void створитиНаселенийСвіт(int population)
throws ПорожнійСвіт, ОдинокийСвіт
{
   створитиСвіт(population);
}
Метод, звідки здійснюється виклик, не перехоплює винятки та змушений інформувати про них інших: додати їх собі у throws

2. Перехоплюємо частину винятків

Обробляємо зрозумілі помилки, незрозумілі — кидаємо в метод, звідки відбувся виклик. Для цього потрібно додати їхні назви в throws:

Код Примітка
public void створити НепорожнійСвіт(int population)
throws ПорожнійСвіт
{
   try
   {
      створитиСвіт(population);
   }
   catch (ОдинокийСвіт e)
   {
      e.printStackTrace();
   }
}
Метод, звідки здійснюється виклик, перехоплює лише один checked-винятокОдинокийСвіт, другий він має додати у свою сигнатуру: вказати після слова throws

3. Перехоплюємо всі винятки

Якщо метод не кидає винятки методу, звідки здійснюється виклик, то останній завжди буде впевнений, що все було виконано нормально. І не зможе вжити ніяких заходів, щоб виправити ситуацію.

Код Примітка
public void створитиБудь-якийСвіт(int population)
{
   try
   {
      створитиСвіт(population);
   }
   catch(ОдинокийСвіт e)
   {
      e.printStackTrace();
   }
   catch(ПорожнійСвіт e)
   {
      e.printStackTrace();
   }
}
У цьому методі перехоплюються всі помилки. Метод, звідки відбувається виклик, буде впевнений, що все пройшло чудово.


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.

Коли є бажання, його можна звідти легко дістати. Приклад:

Код Примітка
try
{
   // код, куди ми запакували checked-виняток
   // у RuntimeException
}
catch(RuntimeException e)
{
   Throwable cause = e.getCause();
   if (cause instanceof Exception)
   {
      Exception exp = (Exception) cause;
      // це код для обробки Exception
   }
}







Отримуємо виняток, збережений усередині об'єкта 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
{
}

Деталі ми обговоримо, коли ви вивчите ООП, успадкування, конструктори й перевизначення методів.

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

Код Примітка
class Solution
{
   public static void main(String[] args)
   {
      throw new MyException();
   }
}

class MyException extends RuntimeException
{
}




Викидаємо unchecked-виняток MyException.

Детальну роботу із власними винятками ми розглянемо у квесті Java Multithreading.