Мисля, че вероятно сте имали ситуация, в която изпълнявате code и накрая получавате нещо като NullPointerException , ClassCastException or по-лошо... Това е последвано от дълъг процес на отстраняване на грешки, анализиране, гугъл и т.н. Изключенията са прекрасни такива, Howвито са: те показват естеството на проблема и къде е възникнал. Ако искате да опресните паметта си и да научите малко повече, погледнете тази статия: Изключения: проверени, непроверени и персонализирани .

Въпреки това може да има ситуации, когато трябва да създадете свое собствено изключение. Да предположим например, че вашият code трябва да поиска информация от отдалечена услуга, която е недостъпна по няHowва причина. Или да предположим, че някой попълни заявление за банкова карта и предостави телефонен номер, който случайно or не, вече е свързан с друг потребител в системата.

Разбира се, правилното поведение тук все още зависи от изискванията на клиента и архитектурата на системата, но нека приемем, че сте получor задача да проверите дали телефонният номер вече се използва и да хвърлите изключение, ако е.

Нека създадем изключение:


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(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 PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

За да опростим нашия пример, ще използваме няколко твърдо codeирани телефонни номера, за да представим база данни. И накрая, нека се опитаме да използваме нашето изключение:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

И сега е време да натиснете Shift+F10 (ако използвате IDEA), т.е. стартирайте проекта. Ето Howво ще видите в конзолата:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Посоченият телефонен номер вече се използва от друг клиент!
при изключение.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Виж се! Създадохте свое собствено изключение и дори го тествахте малко. Поздравления за това постижение! Препоръчвам да експериментирате малко с codeа, за да разберете по-добре How работи.

Добавете друга проверка — например проверете дали телефонният номер включва букви. Както вероятно знаете, буквите често се използват в Съединените щати, за да направят телефонните номера по-лесни за запомняне, напр. 1-800-MY-APPLE. Вашата проверка може да гарантира, че телефонният номер съдържа само числа.

Добре, създадохме проверено изключение. Всичко би било добре, но...

Програмистката общност е разделена на два лагера - тези, които подкрепят проверените изключения, и тези, които им се противопоставят. И двете страни дават силни аргументи. И двамата включват първокласни разработчици: Брус Екел критикува проверените изключения, докато Джеймс Гослинг ги защитава. Изглежда, че този въпрос никога няма да бъде решен окончателно. Въпреки това, нека да разгледаме основните недостатъци на използването на проверени изключения.

Основният недостатък на проверените изключения е, че те трябва да се обработват. И тук имаме две възможности: or да го обработим на място с помощта на try-catch , or, ако използваме едно и също изключение на много места, да използваме хвърляния , за да изхвърлим изключенията и да ги обработим в класове от най-високо ниво.

Също така, може да се окажем с "boilerplate" code, т.е. code, който заема много място, но не върши много тежка работа.

Проблеми възникват в доста големи applications с много обработвани изключения: списъкът с хвърляния на метод от най-високо ниво може лесно да нарасне, за да включва дузина изключения.

public OurCoolClass() хвърля FirstException, SecondException, ThirdException, ApplicationNameException...

Разработчиците обикновено не харесват това и instead of това избират трик: те карат всичките им проверени изключения да наследяват общ предшественик — ApplicationNameException . Сега те също трябва да уловят това ( проверено !) изключение в манипулатор:


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

Тук се сблъскваме с друг проблем — Howво трябва да направим в последния catch блок? По-горе вече обработихме всички очаквани ситуации, така че на този етап ApplicationNameException не означава нищо повече за нас от „ Изключение : възникна няHowва неразбираема грешка“. Ето How се справяме:


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

И накрая не знаем Howво се случи.

Но не бихме ли могли да хвърлим всяко изключение наведнъж, по този начин?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

Да, можехме. Но Howво ни казва "хвърля изключение"? Че нещо е счупено. Ще трябва да проучите всичко от горе до долу и да се почувствате удобно с дебъгера за дълго време, за да разберете причината.

Може също да срещнете конструкция, която понякога се нарича "преглъщане на изключения":


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

Тук няма много Howво да се добави като обяснение — codeът прави всичко ясно or по-скоро прави всичко неясно.

Разбира се, може да кажете, че няма да видите това в реален code. Е, нека надникнем в недрата (codeа) на URL класа от пакета java.net . Последвайте ме, ако искате да знаете!

Ето една от конструкциите в URL класа:


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

Както можете да видите, имаме интересно проверено изключение — MalformedURLException . Ето кога може да бъде хвърлен (и цитирам):
"ако не е посочен протокол or е намерен неизвестен протокол, or спецификацията е нула, or анализираният URL address не отговаря на специфичния синтаксис на свързания протокол."

Това е:

  1. Ако не е посочен протокол.
  2. Намерен е неизвестен протокол.
  3. Спецификацията е нулева .
  4. URL addressът не отговаря на специфичния синтаксис на свързания протокол.

Нека създадем метод, който създава URL обект:


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

Веднага след като напишете тези редове в IDE (аз codeирам в IDEA, но това работи дори в Eclipse и NetBeans), ще видите това:

Това означава, че трябва or да хвърлим изключение, or да увием codeа в блок try-catch . Засега предлагам да изберете втората опция, за да визуализирате Howво се случва:


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

Както можете да видите, codeът вече е доста многословен. И споменахме това по-горе. Това е една от най-очевидните причини да използвате непроверени изключения.

Можем да създадем непроверено изключение, като разширим RuntimeException в Java.

Непроверените изключения са наследени от класа Error or класа RuntimeException . Много програмисти смятат, че тези изключения могат да бъдат обработени в нашите програми, защото представляват грешки, от които не можем да очакваме да се възстановим, докато програмата работи.

Когато възникне непроверено изключение, то обикновено се причинява от неправилно използване на code, предаване на аргумент, който е нулев or невалиден по друг начин.

Е, нека напишем codeа:


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);
   }
}
    

Имайте предвид, че направихме множество конструктори за различни цели. Това ни позволява да дадем на нашето изключение повече възможности. Например, можем да направим така, че изключение да ни дава code за грешка. Като начало, нека направим enum, за да представи нашите codeове за грешки:


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;
    

И разбира се, метод за получаване на този code:


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;
   }
}
    

Та-да! Нашето изключение е напequalsо! Както можете да видите, тук няма нищо особено сложно. Нека го проверим в действие:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

Когато стартираме нашето малко приложение, ще видим нещо като следното в конзолата:

Сега нека се възползваме от допълнителната функционалност, която добавихме. Ще добавим малко към предишния code:


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("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

Можете да работите с изключения по същия начин, по който работите с обекти. Разбира се, сигурен съм, че вече знаете, че всичко в Java е обект.

И вижте Howво направихме. Първо променихме метода, който сега не хвърля, а просто създава изключение в зависимост от входния параметър. След това, използвайки команда за превключване на случай , генерираме изключение с желания code за грешка и съобщение. И в основния метод получаваме създаденото изключение, получаваме codeа на грешката и го хвърляме.

Нека стартираме това и да видим Howво ще получим на конзолата:

Вижте — отпечатахме codeа за грешка, който получихме от изключението, и след това хвърлихме самото изключение. Нещо повече, можем дори да проследим точно къде е хвърлено изключението. Ако е необходимо, можете да добавите цялата подходяща информация към съобщението, да създадете допълнителни codeове за грешка и да добавите нови функции към вашите изключения.

Е, Howво мислиш за това? Надявам се всичко да се е получило при вас!

Като цяло изключенията са доста обширна тема и не е ясна. Ще има още много спорове по него. Например само Java има проверени изключения. Сред най-популярните езици не видях такъв, който ги използва.

Bruce Eckel е написал много добре за изключенията в глава 12 от книгата си „Мислене в Java“ — препоръчвам ви да я прочетете! Разгледайте и първия том на "Core Java" на Хорстман — също има много интересни неща в глава 7.

Малко резюме

  1. Запишете всичко в дневник! Регистрирайте съобщения в хвърлени изключения. Това обикновено ще помогне много при отстраняването на грешки и ще ви позволи да разберете Howво се е случило. Не оставяйте catch блока празен, в противен случай той просто ще "погълне" изключението и няма да имате информация, която да ви помогне да откриете проблеми.

  2. Що се отнася до изключения, лоша практика е да ги хванете всички наведнъж (Howто каза един мой колега, „това не е Pokemon, това е Java“), така че избягвайте catch (Exception e) or по-лошо, catch (Throwable t) .

  3. Хвърлете изключения възможно най-рано. Това е добра практика за програмиране на Java. Когато изучавате рамки като Spring, ще видите, че те следват принципа „fail fast“. Тоест, те се "провалят" възможно най-рано, за да могат бързо да открият грешката. Разбира се, това носи определени неудобства. Но този подход помага за създаването на по-стабилен code.

  4. Когато извиквате други части от codeа, най-добре е да уловите определени изключения. Ако извиканият code хвърля множество изключения, лошата програмна практика е да се улавя само родителският клас на тези изключения. Например, кажете, че извиквате code, който хвърля FileNotFoundException и IOException . Във вашия code, който извиква този модул, е по-добре да напишете два catch блока за улавяне на всяко от изключенията, instead of един catch за улавяне на Exception .

  5. Хващайте изключения само когато можете да ги управлявате ефективно за потребителите и за отстраняване на грешки.

  6. Не се колеbyteе да напишете вашите собствени изключения. Разбира се, Java има много готови, за всеки повод по нещо, но понякога все пак трябва да измислите свое собствено "колело". Но трябва ясно да разберете защо правите това и да сте сигурни, че стандартният набор от изключения вече не съдържа това, от което се нуждаете.

  7. Когато създавате свои собствени класове за изключения, внимавайте с именуването! Вероятно вече знаете, че е изключително важно правилно да именувате класове, променливи, методи и пакети. Изключенията не са изключение! :) Винаги завършвайте с думата Exception , а името на изключението трябва ясно да предава вида на грешката, която представлява. Например FileNotFoundException .

  8. Документирайте изключенията си. Препоръчваме да напишете @throws Javadoc таг за изключения. Това ще бъде особено полезно, когато вашият code предоставя интерфейси от всяHowъв вид. Освен това по-късно ще разберете по-лесно собствения си code. Какво мислите, How можете да определите за Howво е MalformedURLException ? От Javadoc! Да, мисълта за писане на documentация не е много привлекателна, но повярвайте ми, ще си благодарите, когато се върнете към собствения си code шест месеца по-късно.

  9. Освободете ресурси и не пренебрегвайте конструкцията опитване с ресурси .

  10. Ето общото резюме: използвайте изключенията разумно. Хвърлянето на изключение е доста "скъпа" операция от гледна точка на ресурси. В много случаи може да е по-лесно да се избегне хвърлянето на изключения и instead of това да се върне, да речем, булева променлива, която показва дали операцията е успешна, като се използва прост и "по-евтин" if-else .

    Може също да е изкушаващо да обвържете логиката на приложението с изключения, което очевидно не трябва да правите. Както казахме в началото на статията, изключенията са за изключителни ситуации, а не за очаквани, и има различни инструменти за предотвратяването им. По-специално, има Optional за предотвратяване на NullPointerException or Scanner.hasNext и други подобни за предотвратяване на IOException , което методът read() може да хвърли.