Cred că probabil ați experimentat o situație în care rulați cod și ajungeți cu ceva de genul NullPointerException , ClassCastException sau mai rău... Acesta este urmat de un proces lung de depanare, analiză, căutare pe Google și așa mai departe. Excepțiile sunt minunate așa cum sunt: ​​ele indică natura problemei și unde a apărut. Dacă doriți să vă reîmprospătați memoria și să aflați doar puțin mai multe, aruncați o privire la acest articol: Excepții: bifate, nebifate și personalizate .

Acestea fiind spuse, pot exista situații în care trebuie să vă creați propria excepție. De exemplu, să presupunem că codul dvs. trebuie să solicite informații de la un serviciu de la distanță care nu este disponibil dintr-un motiv oarecare. Sau să presupunem că cineva completează o cerere pentru un card bancar și oferă un număr de telefon care, din întâmplare sau nu, este deja asociat cu un alt utilizator din sistem.

Desigur, comportamentul corect aici depinde încă de cerințele clientului și de arhitectura sistemului, dar să presupunem că ați fost însărcinat să verificați dacă numărul de telefon este deja în uz și să aruncați o excepție dacă este.

Să creăm o excepție:


public class PhoneNumberAlreadyExistsException extends Exception {

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

În continuare, îl vom folosi când vom efectua verificarea:


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

Pentru a simplifica exemplul nostru, vom folosi mai multe numere de telefon codificate pentru a reprezenta o bază de date. Și, în sfârșit, să încercăm să folosim excepția noastră:


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

Și acum este timpul să apăsați Shift+F10 (dacă utilizați IDEA), adică rulați proiectul. Iată ce veți vedea în consolă:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Numărul de telefon specificat este deja utilizat de un alt client!
la exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Uită-te la tine! Ți-ai creat propria excepție și chiar ai testat-o ​​puțin. Felicitări pentru această realizare! Recomand să experimentați puțin codul pentru a înțelege mai bine cum funcționează.

Adăugați o altă verificare — de exemplu, verificați dacă numărul de telefon include litere. După cum probabil știți, literele sunt adesea folosite în Statele Unite pentru a face numerele de telefon mai ușor de reținut, de exemplu 1-800-MY-APPLE. Verificarea dvs. ar putea asigura că numărul de telefon conține numai numere.

Bine, deci am creat o excepție bifată. Totul ar fi bine și bine, dar...

Comunitatea de programare este împărțită în două tabere — cei care sunt în favoarea excepțiilor verificate și cei care li se opun. Ambele părți au argumente puternice. Ambele includ dezvoltatori de top: Bruce Eckel critică excepțiile bifate, în timp ce James Gosling le apără. Se pare că această problemă nu va fi niciodată rezolvată definitiv. Acestea fiind spuse, să ne uităm la principalele dezavantaje ale utilizării excepțiilor verificate.

Principalul dezavantaj al excepțiilor verificate este că acestea trebuie gestionate. Și aici avem două opțiuni: fie o gestionăm în loc folosind un try-catch , fie, dacă folosim aceeași excepție în multe locuri, folosim throws pentru a arunca excepțiile și le procesăm în clase de nivel superior.

De asemenea, s-ar putea să ajungem la un cod „boilerplate”, adică un cod care ocupă mult spațiu, dar nu face prea multe sarcini grele.

Probleme apar în aplicații destul de mari, cu o mulțime de excepții tratate: lista de aruncări pe o metodă de nivel superior poate crește cu ușurință pentru a include o duzină de excepții.

public OurCoolClass() aruncă FirstException, SecondException, ThirdException, ApplicationNameException...

De obicei, dezvoltatorilor nu le place acest lucru și, în schimb, optează pentru un truc: fac ca toate excepțiile lor verificate să moștenească un strămoș comun - ApplicationNameException . Acum trebuie să prindă și acea excepție ( bifată !) într-un handler:


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

Aici ne confruntăm cu o altă problemă - ce ar trebui să facem în ultimul bloc de captură ? Mai sus, am procesat deja toate situațiile așteptate, așa că în acest moment ApplicationNameException nu înseamnă nimic mai mult pentru noi decât „ Excepție : a apărut o eroare de neînțeles”. Iată cum o gestionăm:


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

Și până la urmă, nu știm ce s-a întâmplat.

Dar nu am putea să aruncăm toate excepțiile dintr-o dată, așa?


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

Da, am putea. Dar ce ne spune „aruncă excepție”? Acel ceva este rupt. Va trebui să investigați totul de sus până jos și să vă relaxați mult timp cu depanatorul pentru a înțelege motivul.

De asemenea, puteți întâlni o construcție care se numește uneori „înghițire de excepții”:


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

Nu sunt multe de adăugat aici cu titlu de explicație - codul face totul clar sau, mai degrabă, face totul neclar.

Desigur, puteți spune că nu veți vedea acest lucru în codul real. Ei bine, haideți să privim în măruntaiele (codul) clasei URL din pachetul java.net . Urmărește-mă dacă vrei să știi!

Iată unul dintre constructele din clasa URL :


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

După cum puteți vedea, avem o excepție bifată interesantă - MalformedURLException . Iată când poate fi aruncat (și citez):
„dacă nu este specificat niciun protocol, sau este găsit un protocol necunoscut, sau specificația este nulă, sau URL-ul analizat nu respectă sintaxa specifică a protocolului asociat”.

Acesta este:

  1. Dacă nu este specificat niciun protocol.
  2. A fost găsit un protocol necunoscut.
  3. Specificația este nulă .
  4. URL-ul nu respectă sintaxa specifică a protocolului asociat.

Să creăm o metodă care creează un obiect URL :


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

De îndată ce scrieți aceste linii în IDE (codez în IDEA, dar funcționează chiar și în Eclipse și NetBeans), veți vedea asta:

Aceasta înseamnă că trebuie fie să aruncăm o excepție, fie să împachetăm codul într-un bloc try-catch . Deocamdată, vă sugerez să alegeți a doua opțiune pentru a vizualiza ce se întâmplă:


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

După cum puteți vedea, codul este deja destul de pronunțat. Și am făcut aluzie la asta mai sus. Acesta este unul dintre cele mai evidente motive pentru a utiliza excepții neverificate.

Putem crea o excepție neverificată prin extinderea RuntimeException în Java.

Excepțiile neverificate sunt moștenite din clasa Error sau din clasa RuntimeException . Mulți programatori consideră că aceste excepții pot fi gestionate în programele noastre, deoarece reprezintă erori de la care nu ne putem aștepta să le recuperăm în timp ce programul rulează.

Când apare o excepție neverificată, aceasta este de obicei cauzată de utilizarea incorectă a codului, transmiterea unui argument care este nul sau altfel invalid.

Ei bine, hai să scriem codul:


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

Rețineți că am creat mai mulți constructori în scopuri diferite. Acest lucru ne permite să oferim excepției noastre mai multe capacități. De exemplu, putem face astfel încât o excepție să ne dea un cod de eroare. Pentru început, să facem o enumerare pentru a reprezenta codurile noastre de eroare:


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

Acum să adăugăm un alt constructor la clasa noastră de excepții:


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

Și să nu uităm să adăugăm un câmp (aproape am uitat):


private Integer errorCode;
    

Și, desigur, o metodă pentru a obține acest cod:


public Integer getErrorCode() {
   return errorCode;
}
    

Să ne uităm la întreaga clasă, astfel încât să o putem verifica și să comparăm:

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

Ta-da! Excepția noastră este făcută! După cum puteți vedea, nu este nimic deosebit de complicat aici. Să vedem în acțiune:


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

Când rulăm aplicația noastră mică, vom vedea ceva de genul următor în consolă:

Acum să profităm de funcționalitatea suplimentară pe care am adăugat-o. Vom adăuga puțin la codul anterior:


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

}
    

Puteți lucra cu excepții în același mod în care lucrați cu obiecte. Desigur, sunt sigur că știți deja că totul în Java este un obiect.

Și uite ce am făcut. În primul rând, am schimbat metoda, care acum nu aruncă, ci pur și simplu creează o excepție, în funcție de parametrul de intrare. Apoi, folosind o instrucțiune switch-case , generăm o excepție cu codul de eroare și mesajul dorit. Și în metoda principală, obținem excepția creată, obținem codul de eroare și îl aruncăm.

Să rulăm asta și să vedem ce obținem pe consolă:

Uite - am imprimat codul de eroare pe care l-am primit de la excepție și apoi am aruncat excepția în sine. În plus, putem chiar să urmărim exact unde a fost aruncată excepția. După cum este necesar, puteți adăuga toate informațiile relevante la mesaj, puteți crea coduri de eroare suplimentare și puteți adăuga noi funcții la excepțiile dvs.

Ei bine, ce crezi despre asta? Sper că totul a ieșit pentru tine!

În general, excepțiile este un subiect destul de extins și nu este clar. Vor mai fi multe dispute cu privire la asta. De exemplu, numai Java a verificat excepțiile. Printre cele mai populare limbi, nu am văzut una care să le folosească.

Bruce Eckel a scris foarte bine despre excepții în capitolul 12 al cărții sale „Thinking in Java” — vă recomand să o citiți! Aruncă o privire, de asemenea, la primul volum din „Core Java” al lui Horstmann — Are și o mulțime de lucruri interesante în capitolul 7.

Un mic rezumat

  1. Scrie totul într-un jurnal! Înregistrați mesajele în excepții aruncate. Acest lucru va ajuta de obicei foarte mult la depanare și vă va permite să înțelegeți ce sa întâmplat. Nu lăsați un bloc de captură gol, altfel va „înghiți” excepția și nu veți avea nicio informație care să vă ajute să găsiți probleme.

  2. Când vine vorba de excepții, este o practică proastă să le prindeți pe toate deodată (cum a spus un coleg de-al meu, „nu este Pokemon, este Java”), așa că evitați catch (Excepția e) sau mai rău, catch (Throwable t) .

  3. Aruncă excepții cât mai curând posibil. Aceasta este o bună practică de programare Java. Când studiezi cadre precum Spring, vei vedea că urmează principiul „eșuează rapid”. Adică „eșuează” cât mai devreme posibil pentru a face posibilă găsirea rapidă a erorii. Desigur, acest lucru aduce anumite inconveniente. Dar această abordare ajută la crearea unui cod mai robust.

  4. Când apelați alte părți ale codului, cel mai bine este să prindeți anumite excepții. Dacă codul apelat aruncă mai multe excepții, este o practică slabă de programare să prindeți doar clasa părinte a acestor excepții. De exemplu, să presupunem că apelați cod care aruncă FileNotFoundException și IOException . În codul dvs. care apelează acest modul, este mai bine să scrieți două blocuri catch pentru a prinde fiecare dintre excepții, în loc de un singur catch pentru a captura Exception .

  5. Obțineți excepții numai atunci când le puteți gestiona eficient pentru utilizatori și pentru depanare.

  6. Nu ezitați să scrieți propriile excepții. Desigur, Java are o mulțime de gata făcute, câte ceva pentru fiecare ocazie, dar uneori mai trebuie să-ți inventezi propria „roată”. Dar ar trebui să înțelegeți clar de ce faceți acest lucru și să vă asigurați că setul standard de excepții nu are deja ceea ce aveți nevoie.

  7. Când vă creați propriile clase de excepție, aveți grijă la denumire! Probabil știi deja că este extrem de important să denumești corect clase, variabile, metode și pachete. Excepțiile nu fac excepție! :) Încheiați întotdeauna cu cuvântul Excepție , iar numele excepției ar trebui să indice clar tipul de eroare pe care îl reprezintă. De exemplu, FileNotFoundException .

  8. Documentează-ți excepțiile. Vă recomandăm să scrieți o etichetă @throws Javadoc pentru excepții. Acest lucru va fi util în special atunci când codul dvs. oferă interfețe de orice fel. Și, de asemenea, veți găsi mai ușor să înțelegeți propriul cod mai târziu. Ce părere aveți, cum puteți determina despre ce este vorba despre MalformedURLException ? De la Javadoc! Da, gândul de a scrie documentație nu este foarte atrăgător, dar credeți-mă, vă veți mulțumi atunci când veți reveni la propriul cod șase luni mai târziu.

  9. Eliberați resurse și nu neglijați construcția încercare cu resurse .

  10. Iată rezumatul general: utilizați excepțiile cu înțelepciune. Aruncarea unei excepții este o operațiune destul de „costisitoare” din punct de vedere al resurselor. În multe cazuri, poate fi mai ușor să evitați aruncarea de excepții și să returnați, de exemplu, o variabilă booleană care indică dacă operația a reușit, folosind un simplu și „mai puțin costisitor” if-else .

    De asemenea, poate fi tentant să legați logica aplicației de excepții, ceea ce în mod clar nu ar trebui să faceți. După cum spuneam la începutul articolului, excepțiile sunt pentru situații excepționale, nu cele așteptate, și există diverse instrumente pentru prevenirea acestora. În special, există Opțional pentru a preveni o NullPointerException , sau Scanner.hasNext și altele asemenea pentru a preveni o IOException , pe care metoda read() o poate arunca.