Думаю, тобі вже доводилося стикатися з ситуацією, коли запускаєш код, а в результаті отримуєш щось на зразок NullPointerException, ClassCastException або навіть гірше. Потім — довге налагодження, розбирання, гуглення тощо. Власне винятки — відмінна штука: вони вказують, де виникла проблема і якого роду. Якщо хочеш освіжити пам’ять та й просто дізнатися докладніше, заглянь у статтю Винятки: checked, unchecked та свої власні

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

Тут, звісно, ще залежить від вимог замовника та архітектури системи, але давай припустимо, що надійшло завдання зробити перевірку номера телефону, і якщо він вже використовується — викидати виняток.

Створюємо виняток:


public class PhoneNumberIsExistException extends Exception {

   public PhoneNumberIsExistException(String message) {
       super(message);
   }
}
    

Потім використовуємо його для перевірки:


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberIsExistException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberIsExistException("Вказаний номер телефону вже використовується іншим клієнтом!");
       }
   }
}
    

Для спрощення завдання ми «захардкодимо» кілька номерів телефонів — нехай це буде наша база даних. Ну і, нарешті, спробуємо застосувати наш виняток:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberIsExistException e) {
           // тут можна зробити запис в логи або виведення стеку викликів
		e.printStackTrace();
       }
   }
}
    

Ну що, настав час натиснути Shift+F10 (якщо використовуєш IDEA), тобто запустити проєкт. І ось що ти побачиш у консолі:

exception.CreditCardIssue
exception.PhoneNumberIsExistException: Вказаний номер телефону вже використовується іншим клієнтом!
at exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Ну ось, ти створив свій виняток і навіть трохи його протестував. Вітаю тебе із цим досягненням! Рекомендую трохи поекспериментувати із ним, щоб краще розібратися, як це працює.

Додай ще одне: наприклад, для перевірки наявності літер. Як ти, напевно, знаєш, у США часто використовують літери для спрощення запам’ятовування номера, наприклад, 1-800-MY-APPLE. Тобто тобі треба перевірити, щоб у номері були лише цифри.

Отже, ми створили те, що перевіряється, тобто, checked, виняток. І все б добре, але…

Спільнота програмістів розділилася на два табори — ті, хто за винятки, що перевіряються, і ті, хто проти. Обидві сторони наводять вагомі аргументи. Серед тих та інших є розробники екстра-класу: Брюс Еккель критикує концепцію винятків, що перевіряються, Джеймс Гослінг захищає. Схоже, це питання ніколи не буде остаточно закрите. Тим не менш, давайте розглянемо основні мінуси використання винятків, що перевіряються.

Головний мінус винятків, що перевіряються — їх треба обробляти. І тут у нас два варіанти: або обробляти на місці, використовуючи try-catch, або, якщо у нас один і той самий виняток використовується в багатьох місцях, прокидати за допомогою throws вище і в класах верхнього рівня їх обробляти.

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

Проблеми починаються у досить великих програмах: винятків, що обробляються, стає багато, і метод верхнього шару легко може обрости списком throws із десятком винятків.

public OurCoolClass() throws FirstException, SecondException, ThirdException, ApplicationNameException...

Часто розробникам це не подобається, і вони йдуть на хитрість: успадковують усі свої винятки, що перевіряються, від одного предка — ApplicationNameException. Тепер вони повинні ловити в обробнику ще і його (checked же!):


catch(FirstException e){
    // todo
}
catch(SecondException e){
    // todo
}
catch(ThirdException e){
    // todo
}
catch(ApplicationNameException e){
    // todo
}
    

Тут на нас чекає ще одна проблема: а що робити в останньому catch? Вище ми вже обробили всі штатні ситуації, які передбачили, але тут ApplicationNameException для нас означає не більше ніж Exception: «якась незрозуміла помилка». Так і обробляємо:


catch(ApplicationNameException e){
    LOGGER.error("Unknown error", e.getMessage());
}
    

І ми знаємо що відбулося.

Але, здавалося б, можна ж все прокинути одним рухом руки:


public void ourCoolMethod() throws Exception{
// do some
}
    

Так, можна. Але яку інформацію несе “throws Exception”? Щось зламалося. Доведеться перевіряти все від і до, і ти надовго потоваришуєш із дебагером, щоб зрозуміти причину.

Ще ти можеш зустріти конструкцію, яку іноді називають «спіймав — мовчи»:


try{
// some code
} catch(Exception e){
   throw new ApplicationNameException("Error");
}
    

Тут не потрібно зайвих слів — за кодом все зрозуміло, а точніше — нічого не зрозуміло.

Звісно, ти можеш заперечити, що таке не зустрінеш у реальному коді. Добре, давай подивимося таку штуку, як клас URL із пакету java.net, і заглянемо в її надра, в її код. Follow me if you want to know!

Ось один із конструкторів URL:


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

Як бачиш, тут є цікавий виняток, що перевіряється, — MalformedURLException. І він може бути викинутий у разі, цитую:
if no protocol is specified, or an unknown protocol is found, or spec is null, or the parsed URL fails to comply with the specific syntax of the associated protocol.

Тобто

  1. Якщо протокол не вказано.
  2. Знайдено невідомий протокол.
  3. Специфікація має значення null.
  4. URL-адреса не відповідає конкретному синтаксису зв’язаного протоколу.

Давай створимо метод, який буде створювати об’єкт класу URL:


public URL createURL() {
   URL url = new URL("https://javarush.com");
   return url;
}
    

Щойно ти напишеш ці рядки в IDE (я пишу в IDEA, але навіть в Eclipse та NetBeans це спрацює), побачиш ось це:

Це говорить про те, що нам треба або прокинути виняток, або «обгорнути» в try-catch. Пропоную поки обрати другий варіант, щоб наочно побачити, що вийде:


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://javarush.com");
   } catch(MalformedURLException e){
  e.printStackTrace();
   }
   return url;
}
    

Як бачиш, вийшло вже досить багатослівно. Власне, про це вже сказано вище. Це одна з найбільш очевидних причин використовувати винятки, що не перевіряються.

Ми можемо створити такий виняток, розширивши RuntimeException в Java.

Винятки, що не перевіряються, успадковуються від класу Error або класу RuntimeException. Багато програмістів вважають, що ми не можемо обробляти ці винятки в наших програмах, оскільки вони є типом помилок, від яких не можна очікувати відновлення виконання коду під час роботи програми.

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

Отже, давай писати код:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

Зверни увагу: ми зробили кілька конструкторів для різних цілей. Це дозволяє нам розширити можливості нашого винятку. Ось, наприклад, ми можемо зробити так, щоб виняток міг видати нам код помилки. Спочатку сформуємо enum, в якому, власне, наші коди помилок і будуть лежати:


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

А тепер додамо ще один конструктор у клас нашого винятку:


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

І, так, не забудемо додати поле, мало не забули:


private Integer errorCode;
    

Ну і, звісно, метод для отримання цього коду:


public Integer getErrorCode() {
   return errorCode;
}
    

Поглянемо на весь клас цілком, щоб можна було перевірити та порівняти:

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

Ось наш виняток і готовий! Як бачиш, нічого особливо складного немає. Перевіримо його у роботі:


   public static void main(String[] args) {
       getException();
   }
   public static void getException(){
       throw new OurCoolUncheckedException("Наше круте виключення!");
   }
    

Запустимо нашу невелику програму і побачимо в консолі приблизно таке:

А тепер давай скористаємось додатковою функціональністю, яку ми додали. Трохи допишемо попередній код:


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode){
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Наш крутий виняток! Ми отримали помилку: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Наш крутий виняток! Ми отримали помилку: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // зтут ми підхопимо трійку та всі інші коди, які ми ще не додали, тобто це дія за замовчуванням. Докладніше можеш дізнатися
тут оператор switch в Java
       return new OurCoolUncheckedException("Наш крутий виняток! Ми отримали помилку: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

З винятками можна працювати як з об’єктами, хоча, впевнений, ти і так знаєш, що в Java все є об’єктом.

І дивись, що ми зробили. Спочатку ми змінили метод, який тепер не кидає, а просто створює виняток залежно від того, який нам прилетів параметр. Далі за допомогою switch-case генеруємо виняток із потрібним нам кодом помилки та повідомленням. А в основному методі ми створений виняток отримали, дістали код помилки та кинули його.

Давай запустимо і подивимося, що потрапить у консоль:

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

Ну як? Сподіваюся, у тебе все вийшло!

Взагалі, тема винятків досить велика і не така вже однозначна. Тут буде ще багато суперечок та буде зламано чимало списів. Наприклад, лише в Java є винятки, що перевіряються. Із найпоширеніших мов я не бачив жодної, яка б їх використовувала.

Дуже добре про винятки загалом написав Брюс Еккель у своїй книзі «Філософія Java», у главі 12, рекомендую ознайомитись! Також заглянь у перший том «Java. Бібліотека професіонала» Хорстманна, у главу 7 — там теж багато цікавого.

Невеликі підсумки

  1. Пиши все в лог! Логуй повідомлення, які може видати виняток. У більшості випадків це дуже допоможе у налагодженні та дозволить зрозуміти, що сталося. Не залишай порожнім блок catch, інакше він буде просто «з’їдати» виняток, і у тебе не буде даних для пошуку проблем.

  2. Погана практика щодо винятку — зловити їх всі одночасно (як сказав один мій колега, це не покемон — це Java), тому уникай catch або, того гірше, catch(Throwable t).

  3. Кидай виняток якомога раніше. Це гарна практика програмування в Java. Коли ти будеш вивчати фреймворки типу Spring, побачиш, що він працює за принципом Failed First. Тобто «впасти» якомога раніше, щоб швидше знайти помилку. Це, звісно, несе певні незручності. Проте такий підхід допомагає створювати надійніший код.
  4. У разі виклику інших частин коду найкраще ловити певні винятки. Якщо код, що викликається, генерує кілька винятків, погана практика програмування — перехоплювати тільки батьківський клас цих винятків. Наприклад, якщо код, що викликається, видає FileNotFoundExceptionза участю IOException. І в твій код, який викликає цей модуль, краще написати два блоки catch для захоплення кожного з винятків замість одного catch для перехоплення Exception.
  5. Лови винятки тільки тоді, коли зможеш ефективно для користувача та налагодження їх обробити.

  6. Не соромся писати свої винятки. Звісно, в Java дуже багато готових, на будь-який смак і колір, але іноді доведеться все ж таки створювати свій «велосипед». Але ти маєш чітко розуміти, навіщо ти це робиш, і бути впевненим, що серед штатних немає потрібного тобі.
  7. Коли ти створюєш свої класи винятків, стеж за іменуванням! Ти, швидше за все, вже знаєш, що правильне іменування класів, змінних, методів та пакетів — це дуже важливо. Винятки — не винятки (вибач за тавтологію)! Наприкінці завжди став слово Exception, а назва винятку має чітко говорити про помилку, яку він ловить. Приклад — FileNotFoundException.
  8. Документуй винятки. Бажано писати javadoc @throws для винятків. Це буде особливо корисним у тих випадках, коли твої розробки надають будь-які інтерфейси. Та й самому потім буде простіше розібратися у своєму ж коді. Ось як ти думаєш, звідки можна дізнатися, що робить MalformedURLException? Із javadoc! Так, перспектива писати документацію не дуже тішить, але, повір, ти собі скажеш спасибі, коли через пів року повернешся до свого ж коду.
  9. Звільняй ресурси і не нехтуй конструкцією try-with-resources. Якщо ти ще не знаєш, що це таке, то ознайомся з ними тут — Java 7 try-with-resources.

  10. Це, імовірніше, загальний підсумок: використовуй винятки розумно. Кинути виняток — це досить «дорога» за ресурсами операція. Не виключено, що в багатьох випадках буде простіше не кидати винятки, а повернути, скажімо, логічну змінну, яка вкаже, як пройшла операція, використовуючи простий і більш «дешевий» if-else.

    Також може з’явитися спокуса зав’язати на винятки логіку програми, чого робити точно не варто. Винятки, як ми вже говорили на початку статті, це виняткова ситуація, а не штатна, і для їхньої «профілактики» є різні інструменти. Зокрема, є Optional, щоб запобігти NullPointerException, або Scanner.hasNext і йому подібні для недопущення винятку IOException, який може кинути метод read().