Java throw e throws
Lancia eccezioni manualmente in Java con throw e dichiarale nelle firme dei metodi con throws.
Finora abbiamo catturato eccezioni lanciate da altro codice. Ora vedrai come lanciarne di tue. Due parole chiave svolgono la maggior parte del lavoro — ed è facile confonderle perché differiscono di una sola lettera.
throw— un'istruzione che solleva un'eccezione a runtime. Una parola, nel codice che viene eseguito.throws— una dichiarazione nella firma di un metodo che indica "questo metodo potrebbe lanciare questi tipi di eccezione." Viene verificata dal compilatore, non viene mai eseguita.
throw accade. throws avverte. Tieni a mente questa coppia.
Lanciare un'eccezione
throw prende un'espressione di tipo Throwable (o qualsiasi suo sottotipo) e la solleva. Il metodo corrente esce immediatamente, lo stack inizia a srotolarsi e l'eccezione comincia la ricerca di un catch corrispondente.
if (amount < 0) {
throw new IllegalArgumentException("amount must be non-negative, got " + amount);
}Tre dettagli:
- Puoi lanciare solo un
Throwable. Il compilatore lo impone —throw "oops";non compila. - Lanci sempre un'istanza, non una classe.
throw new X(...), maithrow X. - L'istanza può essere una costruita inline (comune), oppure un oggetto preesistente (raro — le eccezioni portano stack trace dalla loro costruzione, quindi riutilizzarne una congela il trace sbagliato).
Quando lanciare
Lancia un'eccezione quando il metodo corrente non riesce a rispettare il suo contratto. Alcuni casi chiari:
- Argomenti non validi —
IllegalArgumentExceptionper "mi hai chiamato in modo errato." - Stato errato —
IllegalStateExceptionper "mi hai chiamato nel momento sbagliato" (es.next()su un iteratore vuoto). - Dati mancanti — eccezioni specifiche del dominio come
UserNotFoundException. - Operazioni esterne fallite — errori IO, errori di rete. Di solito queste arrivano dalla chiamata appena effettuata, quindi non le costruisci tu stesso; le lasci propagare o le incapsula in un'eccezione di livello più alto.
Il caso in cui non dovresti lanciare: come scorciatoia per il flusso di controllo con risultati normali. "Lanciare per il flusso di controllo" è lento e confuso. Se "non trovato" è un risultato ordinario, restituisci un Optional<T>, non una NotFoundException.
Scegliere un tipo
Le eccezioni integrate in java.lang coprono la maggior parte dei casi senza cerimonie:
IllegalArgumentException— argomento non validoIllegalStateException— stato erratoNullPointerException— un argomento richiesto era null (usaObjects.requireNonNull)UnsupportedOperationException— operazione non implementata (es.adddi una lista immutabile)ArithmeticException— errore matematico
Quando il fallimento è specifico del tuo dominio — "utente non trovato," "coupon non valido," "configurazione non sincronizzata" — scrivi una piccola classe personalizzata per esso. Tra due capitoli lo faremo esattamente.
La clausola throws
Se il tuo metodo potrebbe lanciare un'eccezione checked che non cattura internamente, devi dichiararla:
public Config loadConfig(Path p) throws IOException, ParseException {
String text = Files.readString(p);
return parser.parse(text);
}La clausola fa parte del contratto del metodo. Dice a ogni chiamante: "se mi chiami, devi o catturare queste eccezioni o dichiararle a tua volta." Il compilatore lo impone — è questo che le rende checked.
Alcune regole:
- Dichiari solo eccezioni checked. Le
RuntimeExceptione le loro sottoclassi sono unchecked — dichiararle è consentito ma non obbligatorio, e di solito non lo si fa. - Puoi dichiarare più tipi di quanti ne lanci effettivamente — utile quando vuoi tenere aperta l'opzione per implementazioni future, anche se è un leggero rumore.
- Un metodo che sovrascrive un altro può dichiarare le stesse o meno eccezioni checked del genitore (e solo sottotipi di quelle dichiarate). Non può aggiungerne di nuove. Questo è il principio di sostituzione di Liskov applicato alle eccezioni.
throw e throws insieme
Un metodo reale di solito fa entrambe le cose:
public User loadUser(String id) throws IOException {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id must be non-blank");
}
String json = httpClient.get("/users/" + id); // may throw IOException
return parser.toUser(json);
}- Il
throws IOExceptiondichiara l'eccezione checked che può provenire dahttpClient.get. - Il
throw new IllegalArgumentException(...)solleva una unchecked per input non valido. Non deve comparire nella clausolathrows.
Incapsulare un'eccezione
Quando un'eccezione di basso livello non è significativa nel tuo layer, incapsulala in una che lo sia. Passa l'originale come causa in modo che il trace rimanga intatto:
try {
return Files.readString(configPath);
} catch (IOException e) {
throw new ConfigLoadException("could not load config from " + configPath, e);
}Il pattern del costruttore con secondo argomento — (message, cause) — è standard in Exception, IOException e tutti i built-in. Quando scrivi la tua classe di eccezione personalizzata, fornisci entrambi i costruttori.
Un esempio pratico
Un piccolo helper in stile bancario che valida l'input con IllegalArgumentException, segnala un account vuoto con IllegalStateException, e lascia risalire al chiamante un'eccezione checked tramite throws. Il driver mostra come appare ognuna quando viene lanciata.
I tre casi runtime — argomento, stato e prelievo riuscito — passano attraverso un unico catch. Il quarto, archive(), compila solo perché main può catturare Exception e perché archive() ha dichiarato throws ArchiveException. Prova a rimuovere la clausola throws e il programma non compilerà.
Cosa c'è dopo
Il compilatore tratta alcune eccezioni in modo rigoroso (devi gestirle) e altre in modo permissivo (non è obbligatorio). Questa distinzione è l'argomento del prossimo capitolo. Continua su Eccezioni checked vs. unchecked in Java.