Gerarchia delle eccezioni in Java
Come Throwable, Error, Exception e RuntimeException si relazionano nella gerarchia delle eccezioni di Java.
Ogni eccezione in Java fa parte di un piccolo albero di classi con radice in java.lang.Throwable. Conoscere la forma di quell'albero ripaga costantemente: spiega perché catch (Exception e) non cattura OutOfMemoryError, perché RuntimeException è speciale e perché alcune eccezioni ti obbligano a gestirle mentre altre no. L'intera struttura entra in un solo diagramma.
Questa pagina mappa quell'albero: la radice Throwable, i rami Error ed Exception, dove cade la linea tra checked e unchecked, e come tutto ciò determina cosa catturano effettivamente i tuoi blocchi catch.
L'albero completo
Throwable
├── Error (unchecked — JVM-level)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── VirtualMachineError
│ └── ...
└── Exception (checked by default)
├── IOException (checked)
├── SQLException (checked)
├── ClassNotFoundException (checked)
├── ...
└── RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
├── IndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
├── IllegalStateException
└── ...L'intero albero è una sola gerarchia di classi. Ecco perché un catch per un supertipo cattura i suoi sottotipi, perché le variabili di eccezione si comportano come riferimenti ordinari e perché puoi tenere un IOException in un campo di tipo Exception.
Throwable — la radice
Throwable è ciò che throw accetta e ciò che catch dichiara. Qualsiasi cosa tu voglia sollevare o gestire è una sottoclasse di Throwable. La classe stessa fornisce:
- Un messaggio (
getMessage()) - Una stack trace catturata alla costruzione (
getStackTrace(),printStackTrace()) - Una causa opzionale — un altro
Throwableche ha scatenato questo (getCause()) - Eccezioni soppresse — errori secondari allegati dal try-with-resources (
getSuppressed())
Quasi mai estendi Throwable direttamente. Il design interessante si trova un livello più in basso.
Error — non catturare
Error e le sue sottoclassi rappresentano fallimenti segnalati dalla JVM: memoria esaurita, stack overflow, un file di classe che non può essere collegato. Per convenzione non li si cattura nel codice applicativo, perché:
- Di solito significano che la JVM non è più affidabile. Continuare dopo un
OutOfMemoryErrorraramente funziona a lungo. - Quasi mai esiste un'azione di recupero sensata che il tuo codice possa intraprendere.
- La JVM stessa potrebbe già occuparsene; intercettare interferisce.
Error è tecnicamente catturabile — Java non te lo impedisce. Ma la convenzione è così forte che "cattura Error" è considerato un segnale d'allarme nelle code review. L'unico caso d'uso legittimo è un supervisore di primo livello (un request handler, un job runner) che registra l'errore ed esce in modo pulito.
Exception — fallimenti applicativi
Tutto ciò che si trova sotto Throwable tranne Error è Exception o uno dei suoi sottotipi. La linea tra checked e unchecked corre all'interno di questo ramo, non sopra di esso:
- Le sottoclassi dirette di
Exceptionche non sonoRuntimeExceptionsono checked. RuntimeExceptione tutti i suoi sottotipi sono unchecked.
Ecco perché catch (Exception e) corrisponde sia a IOException (checked) sia a NullPointerException (unchecked) — sono fratelli sotto la stessa radice. È anche il motivo per cui catturare Exception è così generico: hai unito entrambi i rami insieme.
La distinzione checked/unchecked ha conseguenze reali per le firme dei tuoi metodi — le eccezioni checked devono essere dichiarate con throws o gestite, quelle unchecked no. Questo compromesso viene trattato separatamente in checked vs. unchecked exceptions.
RuntimeException — il ramo dei bug
RuntimeException e i suoi sottotipi sono riservati per convenzione agli errori di programmazione che non dovrebbero accadere nel codice corretto:
NullPointerException— dereferenziazione di nullIllegalArgumentException— argomento non validoIllegalStateException— stato errato per l'operazioneIndexOutOfBoundsException— indice di lista/array oltre il limiteArithmeticException— divisione per zeroClassCastException— cast non validoUnsupportedOperationException— operazione non supportata (es. modifica di una lista non modificabile)
Puoi lanciare queste eccezioni da qualsiasi punto senza cambiare la firma del metodo. I chiamanti possono catturarle, ma il linguaggio non li obbliga. Sono lo strumento giusto quando il fallimento dice "questo è un bug" anziché "questo a volte accade."
Relazioni di tipo nei catch
Un catch (T e) corrisponde a qualsiasi valore lanciato che sia un'istanza di T o un suo sottotipo. Quindi la gerarchia determina direttamente cosa vedono i tuoi catch:
try { ... }
catch (IOException e) { ... } // catches FileNotFoundException too
catch (Exception e) { ... } // catches almost everything below Throwable
catch (Throwable t) { ... } // catches everything, including Error — don'tL'ordine conta: poiché ogni catch corrisponde ai sottotipi, devi elencare i tipi più specifici per primi. Se catch (Exception e) venisse prima di catch (IOException e), il blocco IOException sarebbe irraggiungibile e il compilatore lo rifiuterebbe. Vedi multiple catch blocks per le regole complete.
Questo è anche il motivo per cui un pattern che cattura tutto è pericoloso. catch (Exception) cattura NullPointerException (un bug), IOException (un fallimento recuperabile) e IllegalStateException (probabilmente un bug) — tutto in un unico blocco, senza modo di gestirli diversamente. La gerarchia ti chiede di essere più specifico.
Ricercare i tipi
Quando incontri una nuova eccezione in una stack trace e vuoi sapere dove si trova:
- Si trova in
java.langse è un errore fondamentale (NullPointerException,ArithmeticException). - Si trova in
java.io,java.sql,java.netse è legata al dominio di quel pacchetto. - Una classe che termina con
Errorè quasi certamente sottoError. - Una classe che termina con
Exceptionè quasi certamente sottoException— ma verifica se estendeRuntimeExceptionper sapere se è checked.
Il Javadoc mostra la catena di ereditarietà in cima a ogni pagina. In caso di dubbio, consultalo.
Un esempio pratico
Un piccolo programma che percorre la gerarchia con controlli instanceof. Cattura una sequenza di lanci come Throwable, poi riporta dove si trova ciascuno nell'albero.
Il metodo helper isChecked codifica la regola in una sola riga: il sottoinsieme checked è Exception meno RuntimeException. Esegui il programma e vedrai esattamente dove si trovano ciascuno dei cinque: IOException è checked, i due RuntimeException non lo sono, OutOfMemoryError è un Error (quindi non è né un Exception né checked), e il semplice Exception è checked.
Cosa fare dopo
L'albero built-in copre la maggior parte dei casi. Quando il tuo dominio ha fallimenti propri — "fattura non trovata," "configurazione non aggiornata" — scrivi le tue classi estendendo il nodo giusto di questo albero. Continua con Java custom exceptions.
Letture correlate:
- Try-catch basics — come
catchseleziona effettivamente un handler. throwethrows— sollevare eccezioni e dichiarare quelle checked.- Checked vs. unchecked — la linea all'interno del ramo
Exception. - Exception best practices — cosa fare con ciò che catturi.