Penso che tu abbia probabilmente sperimentato una situazione in cui esegui il codice e finisci con qualcosa come NullPointerException , ClassCastException o peggio ... Questo è seguito da un lungo processo di debug, analisi, googling e così via. Le eccezioni sono meravigliose così come sono: indicano la natura del problema e dove si è verificato. Se vuoi rinfrescarti la memoria e saperne un po' di più, dai un'occhiata a questo articolo: Eccezioni: controllate, non controllate e personalizzate .

Detto questo, potrebbero esserci situazioni in cui è necessario creare la propria eccezione. Ad esempio, supponi che il tuo codice debba richiedere informazioni da un servizio remoto che non è disponibile per qualche motivo. Oppure supponiamo che qualcuno compili una domanda per una carta bancaria e fornisca un numero di telefono che, accidentalmente o meno, è già associato a un altro utente nel sistema.

Naturalmente, il comportamento corretto qui dipende ancora dai requisiti del cliente e dall'architettura del sistema, ma supponiamo che ti sia stato assegnato il compito di controllare se il numero di telefono è già in uso e di lanciare un'eccezione se lo è.

Creiamo un'eccezione:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Successivamente lo useremo quando eseguiremo il nostro controllo:


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

Per semplificare il nostro esempio, utilizzeremo diversi numeri di telefono codificati per rappresentare un database. E infine, proviamo a usare la nostra eccezione:


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

E ora è il momento di premere Maiusc+F10 (se stai usando IDEA), cioè eseguire il progetto. Questo è ciò che vedrai nella console:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: il numero di telefono specificato è già utilizzato da un altro cliente!
in eccezione.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Guardati! Hai creato la tua eccezione e l'hai anche testata un po'. Congratulazioni per questo risultato! Consiglio di sperimentare un po 'con il codice per capire meglio come funziona.

Aggiungi un altro segno di spunta, ad esempio controlla se il numero di telefono include lettere. Come probabilmente saprai, le lettere vengono spesso utilizzate negli Stati Uniti per rendere i numeri di telefono più facili da ricordare, ad esempio 1-800-MY-APPLE. Il tuo assegno potrebbe garantire che il numero di telefono contenga solo numeri.

Ok, quindi abbiamo creato un'eccezione controllata. Andrebbe tutto bene e bene, ma...

La comunità dei programmatori è divisa in due campi: quelli a favore delle eccezioni controllate e quelli contrari. Entrambe le parti fanno argomenti forti. Entrambi includono sviluppatori di prim'ordine: Bruce Eckel critica le eccezioni verificate, mentre James Gosling le difende. Sembra che questa questione non sarà mai risolta definitivamente. Detto questo, esaminiamo i principali svantaggi dell'utilizzo delle eccezioni verificate.

Il principale svantaggio delle eccezioni verificate è che devono essere gestite. E qui abbiamo due opzioni: o gestirlo sul posto usando un try-catch , oppure, se usiamo la stessa eccezione in molti punti, usare i tiri per sollevare le eccezioni ed elaborarle in classi di primo livello.

Inoltre, potremmo finire con il codice "boilerplate", cioè codice che occupa molto spazio, ma non fa molto lavoro pesante.

I problemi emergono in applicazioni abbastanza grandi con molte eccezioni gestite: l' elenco dei lanci su un metodo di primo livello può facilmente crescere fino a includere una dozzina di eccezioni.

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

Agli sviluppatori di solito non piace questo e optano invece per un trucco: fanno ereditare a tutte le loro eccezioni verificate un antenato comune — ApplicationNameException . Ora devono anche rilevare quell'eccezione ( selezionata !) in un gestore:


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

Qui affrontiamo un altro problema: cosa dovremmo fare nell'ultimo blocco di cattura ? Sopra, abbiamo già elaborato tutte le situazioni previste, quindi a questo punto ApplicationNameException non significa altro per noi che " Eccezione : si è verificato un errore incomprensibile". Ecco come lo gestiamo:


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

E alla fine, non sappiamo cosa sia successo.

Ma non potremmo lanciare tutte le eccezioni tutte in una volta, in questo modo?


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

Sì, potremmo. Ma cosa ci dice "genera eccezione"? Che qualcosa è rotto. Dovrai indagare su tutto dall'alto verso il basso e familiarizzare con il debugger per molto tempo per capirne il motivo.

Potresti anche incontrare un costrutto che a volte viene chiamato "deglutizione delle eccezioni":


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

Non c'è molto da aggiungere qui a titolo di spiegazione: il codice rende tutto chiaro, o meglio, rende tutto poco chiaro.

Certo, potresti dire che non lo vedrai nel codice reale. Bene, esaminiamo le viscere (il codice) della classe URL dal pacchetto java.net . Seguimi se vuoi saperlo!

Ecco uno dei costrutti nella classe URL :


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

Come puoi vedere, abbiamo un'interessante eccezione verificata — MalformedURLException . Ecco quando può essere lanciato (e cito):
"se non viene specificato alcun protocollo, o viene trovato un protocollo sconosciuto, o la specifica è nulla, o l'URL analizzato non è conforme alla sintassi specifica del protocollo associato."

Questo è:

  1. Se non è specificato alcun protocollo.
  2. È stato trovato un protocollo sconosciuto.
  3. La specifica è nulla .
  4. L'URL non è conforme alla sintassi specifica del protocollo associato.

Creiamo un metodo che crea un oggetto URL :


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

Non appena scrivi queste righe nell'IDE (sto codificando in IDEA, ma funziona anche in Eclipse e NetBeans), vedrai questo:

Ciò significa che dobbiamo lanciare un'eccezione o avvolgere il codice in un blocco try-catch . Per ora, suggerisco di scegliere la seconda opzione per visualizzare ciò che sta accadendo:


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

Come puoi vedere, il codice è già piuttosto prolisso. E abbiamo accennato a quello sopra. Questo è uno dei motivi più ovvi per utilizzare le eccezioni non controllate.

Possiamo creare un'eccezione non verificata estendendo RuntimeException in Java.

Le eccezioni non controllate vengono ereditate dalla classe Error o dalla classe RuntimeException . Molti programmatori ritengono che queste eccezioni possano essere gestite nei nostri programmi perché rappresentano errori dai quali non possiamo aspettarci di recuperare mentre il programma è in esecuzione.

Quando si verifica un'eccezione non verificata, di solito è causata dall'uso errato del codice, passando un argomento nullo o comunque non valido.

Bene, scriviamo il codice:


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

Si noti che abbiamo creato più costruttori per scopi diversi. Questo ci consente di dare alla nostra eccezione più capacità. Ad esempio, possiamo fare in modo che un'eccezione ci fornisca un codice di errore. Per cominciare, creiamo un'enumerazione per rappresentare i nostri codici di errore:


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

Ora aggiungiamo un altro costruttore alla nostra classe di eccezione:


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

E non dimentichiamo di aggiungere un campo (ci eravamo quasi dimenticati):


private Integer errorCode;
    

E, naturalmente, un metodo per ottenere questo codice:


public Integer getErrorCode() {
   return errorCode;
}
    

Diamo un'occhiata all'intera classe in modo da poterla controllare e confrontare:

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! La nostra eccezione è fatta! Come puoi vedere, non c'è niente di particolarmente complicato qui. Diamo un'occhiata in azione:


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

Quando eseguiamo la nostra piccola applicazione, vedremo qualcosa di simile al seguente nella console:

Ora sfruttiamo le funzionalità extra che abbiamo aggiunto. Aggiungeremo un po' al codice precedente:


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

}
    

Puoi lavorare con le eccezioni nello stesso modo in cui lavori con gli oggetti. Certo, sono sicuro che sai già che tutto in Java è un oggetto.

E guarda cosa abbiamo fatto. Per prima cosa, abbiamo cambiato il metodo, che ora non lancia, ma crea semplicemente un'eccezione, a seconda del parametro di input. Successivamente, utilizzando un'istruzione switch-case , generiamo un'eccezione con il codice di errore e il messaggio desiderati. E nel metodo principale, otteniamo l'eccezione creata, otteniamo il codice di errore e lo lanciamo.

Eseguiamo questo e vediamo cosa otteniamo sulla console:

Guarda: abbiamo stampato il codice di errore che abbiamo ottenuto dall'eccezione e poi abbiamo lanciato l'eccezione stessa. Inoltre, possiamo persino tracciare esattamente dove è stata lanciata l'eccezione. Se necessario, puoi aggiungere tutte le informazioni pertinenti al messaggio, creare ulteriori codici di errore e aggiungere nuove funzionalità alle tue eccezioni.

Beh, cosa ne pensi? Spero che tutto abbia funzionato per te!

In generale, le eccezioni sono un argomento piuttosto vasto e non chiaro. Ci saranno molte più controversie al riguardo. Ad esempio, solo Java ha verificato le eccezioni. Tra le lingue più popolari, non ne ho vista una che le usi.

Bruce Eckel ha scritto molto bene sulle eccezioni nel capitolo 12 del suo libro "Thinking in Java" — vi consiglio di leggerlo! Dai anche un'occhiata al primo volume di "Core Java" di Horstmann: contiene anche molte cose interessanti nel capitolo 7.

Un piccolo riassunto

  1. Scrivi tutto su un registro! Registra i messaggi nelle eccezioni generate. Questo di solito aiuterà molto nel debug e ti permetterà di capire cosa è successo. Non lasciare vuoto un blocco catch , altrimenti semplicemente "inghiottirà" l'eccezione e non avrai alcuna informazione per aiutarti a scovare i problemi.

  2. Quando si tratta di eccezioni, è una cattiva pratica prenderle tutte in una volta (come ha detto un mio collega, "non è Pokemon, è Java"), quindi evita catch (Exception e) o peggio, catch ( Throwable t ) .

  3. Genera eccezioni il prima possibile. Questa è una buona pratica di programmazione Java. Quando studi framework come Spring, vedrai che seguono il principio del "fail fast". Cioè, "falliscono" il prima possibile per consentire di trovare rapidamente l'errore. Naturalmente, questo comporta alcuni inconvenienti. Ma questo approccio aiuta a creare codice più robusto.

  4. Quando si chiamano altre parti del codice, è meglio rilevare alcune eccezioni. Se il codice chiamato genera più eccezioni, è scarsa pratica di programmazione rilevare solo la classe genitore di tali eccezioni. Ad esempio, supponi di chiamare il codice che genera FileNotFoundException e IOException . Nel tuo codice che chiama questo modulo, è meglio scrivere due blocchi catch per catturare ciascuna delle eccezioni, invece di un singolo catch per catturare Exception .

  5. Cattura le eccezioni solo quando puoi gestirle in modo efficace per gli utenti e per il debug.

  6. Non esitate a scrivere le vostre eccezioni. Certo, Java ne ha molti già pronti, qualcosa per ogni occasione, ma a volte devi ancora inventare la tua "ruota". Ma dovresti capire chiaramente perché lo stai facendo ed essere sicuro che il set standard di eccezioni non abbia già ciò di cui hai bisogno.

  7. Quando crei le tue classi di eccezione, fai attenzione alla denominazione! Probabilmente sai già che è estremamente importante nominare correttamente classi, variabili, metodi e pacchetti. Le eccezioni non fanno eccezione! :) Termina sempre con la parola Exception e il nome dell'eccezione dovrebbe indicare chiaramente il tipo di errore che rappresenta. Ad esempio, FileNotFoundException .

  8. Documenta le tue eccezioni. Si consiglia di scrivere un tag Javadoc @throws per le eccezioni. Ciò sarà particolarmente utile quando il tuo codice fornisce interfacce di qualsiasi tipo. E troverai anche più facile capire il tuo codice in seguito. Cosa ne pensi, come puoi determinare di cosa tratta MalformedURLException ? Da Javadoc! Sì, l'idea di scrivere documentazione non è molto allettante, ma credimi, ti ringrazierai quando tornerai al tuo codice sei mesi dopo.

  9. Rilascia le risorse e non trascurare il costrutto try-with-resources .

  10. Ecco il riepilogo generale: usa le eccezioni con saggezza. Lanciare un'eccezione è un'operazione abbastanza "costosa" in termini di risorse. In molti casi, potrebbe essere più semplice evitare di generare eccezioni e invece restituire, ad esempio, una variabile booleana che indica se l'operazione è riuscita, utilizzando un semplice e "meno costoso" if-else .

    Potrebbe anche essere allettante legare la logica dell'applicazione alle eccezioni, cosa che chiaramente non dovresti fare. Come dicevamo all'inizio dell'articolo, le eccezioni sono per situazioni eccezionali, non previste, e ci sono diversi strumenti per prevenirle. In particolare, c'è Optional per prevenire una NullPointerException , o Scanner.hasNext e simili per prevenire una IOException , che il metodo read() potrebbe generare.