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— це різновид (підмножина) винятків RuntimeException. Можна навіть сказати, що 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-винятки:

  • ПорожнійСвіт
  • СамотнійСвіт

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

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. Множинне перехоплення винятків

Програмісти дуже не люблять дублювання коду. Навіть вигадали такий принцип розробки — 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.