Il linguaggio di programmazione Java, come molti altri, dispone di strumenti integrati per gestire gli errori, ovvero situazioni eccezionali (eccezioni) in cui un errore del programma viene gestito tramite un codice specifico, separato dall’algoritmo principale.
Grazie alle eccezioni, uno sviluppatore può prevedere i punti deboli del codice ed evitare errori fatali durante l’esecuzione.
Pertanto, gestire le eccezioni in Java è una buona pratica che migliora l’affidabilità complessiva del codice.
Lo scopo di questo articolo è esplorare i principi della cattura e della gestione delle eccezioni, nonché analizzare le strutture sintattiche del linguaggio progettate a questo scopo.
Tutti gli esempi in questa guida sono stati eseguiti su Ubuntu 22.04, installato su un server cloud di Hostman.
Gli esempi mostrati in questa guida sono stati eseguiti utilizzando OpenJDK. L’installazione è semplice.
Per prima cosa, aggiorna l’elenco dei repository disponibili:
sudo apt update
Successivamente, elenca le versioni di OpenJDK disponibili per il download:
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
Vedrai un breve elenco nel terminale:
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
Utilizzeremo la versione openjdk-21-jdk:
sudo apt install openjdk-21-jdk
Puoi quindi verificare che Java sia stato installato correttamente controllando la versione:
java --version
L’output del terminale sarà simile a questo:
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
Come si vede, la versione esatta di OpenJDK è 21.0.5.
Tutti gli esempi di questa guida devono essere salvati in un file separato con estensione .java:
nano App.java
Poi, inserisci nel file creato un codice di esempio come questo:
class App {
public static void main(String[] args) {
System.out.println("This text is printed to the console");
}
}
Nota che il nome della classe deve corrispondere al nome del file.
Successivamente, compila il file:
javac App.java
E infine eseguilo:
java App
Il terminale mostrerà quanto segue:
This text is printed to the console
Tutte le eccezioni in Java hanno un tipo specifico associato al motivo per cui l’eccezione si è verificata — il tipo particolare di errore del programma.
Esistono due tipi fondamentali di eccezioni:
Il tipo Error è considerato un’eccezione solo in modo condizionale — è un vero e proprio errore che inevitabilmente causa l’arresto del programma.
Le eccezioni che possono essere gestite tramite codice personalizzato e consentono al programma di continuare l’esecuzione sono Checked Exceptions e Unchecked Exceptions.
Pertanto, errori ed eccezioni in Java sono entità diverse. Tuttavia, sia gli Error che le Exception (Checked e Unchecked) sono tipi con ulteriori sottotipi che chiariscono la causa del problema.
Ecco un esempio di codice che genera un’eccezione in fase di compilazione:
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // crea un riferimento al file
Scanner scanner = new Scanner(someFile); // analizza il contenuto del file
}
}
La compilazione verrà interrotta e vedrai il seguente errore nel terminale:
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
Se catturi e gestisci questa eccezione, il codice verrà compilato e potrà essere eseguito.
Ecco un altro esempio di codice che genera un’eccezione solo durante l’esecuzione:
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // crea un array con 5 elementi
System.out.println(someArray[10]); // tenta di accedere a un elemento inesistente
}
}
Nessuna eccezione si verificherà durante la compilazione, ma dopo l’esecuzione del codice compilato, vedrai questo errore nel terminale:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
Ciò significa che un’eccezione di questo tipo può essere gestita tramite codice definito dall’utente, consentendo al programma di continuare l’esecuzione.
Infine, ecco un esempio di codice che causa un errore in fase di esecuzione:
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // genera un errore di stack overflow
}
}
La compilazione avrà successo, ma durante l’esecuzione il terminale mostrerà un StackOverflowError:
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
In questo caso, l’errore non può essere gestito; può solo essere corretto nel codice.
Internamente, tutte le eccezioni (ed errori) in Java sono rappresentate come un insieme di classi, alcune delle quali ereditano da altre.
La classe di base per tutti gli errori e le eccezioni è Throwable. Da essa derivano due classi principali: Error e Exception, che fungono da classi base per un’ampia gamma di sottoclassi associate a tipi specifici di eccezioni.
La classe Error descrive errori veri e propri, come accennato in precedenza, mentre la classe Exception descrive le eccezioni controllate.
Inoltre, la classe RuntimeException eredita da Exception e rappresenta le eccezioni non controllate.
Una gerarchia semplificata delle eccezioni Java può essere rappresentata come segue:
Ogni classe di eccezione include metodi per ottenere informazioni aggiuntive sull’errore.
Puoi trovare la classificazione completa delle eccezioni Java, comprese quelle dei pacchetti aggiuntivi, in una guida di riferimento dedicata.
Tutte le eccezioni vengono gestite utilizzando blocchi speciali try e catch, standard nella maggior parte dei linguaggi di programmazione, incluso Java.
All’interno del blocco try, si scrive il codice che potrebbe contenere un errore capace di generare un’eccezione.
All’interno del blocco catch, si scrive il codice che gestisce l’eccezione verificatasi nel blocco try precedentemente definito.
Ecco un esempio di struttura try-catch:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
System.out.println("Who said you can’t divide by zero?");
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
}
}
}
L’output di questo codice nella console sarà:
Actually, you can't divide by zero...
Questo esempio si basa su una divisione illegale per zero, racchiusa in un blocco try che genera un’ArithmeticException.
Nel blocco catch, questa eccezione viene gestita stampando un messaggio di errore nella console.
Grazie a questa struttura, il programma può continuare l’esecuzione anche dopo un errore di divisione per zero.
A differenza di molti altri linguaggi di programmazione, Java include un blocco speciale finally come parte del suo meccanismo di gestione delle eccezioni. Viene sempre eseguito, indipendentemente dal fatto che si verifichi o meno un’eccezione.
Possiamo quindi estendere la struttura precedente:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
} finally {
// code that always executes
System.out.println("Who cares if you can divide by zero or not? This message will appear anyway!");
}
}
}
Dopo l’esecuzione di questo codice, la console mostrerà:
Actually, you can't divide by zero...
Who cares if you can divide by zero or not? This message will appear anyway!
Per comprendere l’utilità pratica del blocco finally, considera il seguente esempio:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
}
// a function that hides the loading indicator
hideLoaderUI();
In un programma con questa struttura, la funzione hideLoaderUI() non verrà mai eseguita se si verifica un’eccezione.
Si potrebbe provare a chiamare hideLoaderUI() sia all’interno del gestore di eccezioni che successivamente:
try {
parseJson(response.json);
} catch (JSONException someException) {
hideLoaderUI(); // duplicate
System.out.println("Looks like there’s something wrong with the JSON...");
}
hideLoaderUI(); // duplicato
Tuttavia, questo comporta una duplicazione indesiderata della chiamata della funzione. Inoltre, il codice duplicato è considerato una cattiva pratica.
Per garantire l’esecuzione di hideLoaderUI() in ogni caso senza duplicazione, si può utilizzare un blocco finally:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
} finally {
// the loading indicator will be hidden in any case
hideLoaderUI();
}
Java consente di creare (lanciare) manualmente eccezioni utilizzando l’operatore speciale throw:
public class App {
public static void main(String[] args) {
throw new Exception("Something strange seems to have happened...");
}
}
È anche possibile creare una variabile per l’eccezione in anticipo e poi lanciarla:
public class App {
public static void main(String[] args) {
var someException = new Exception("Something strange seems to have happened...");
throw someException;
}
}
Un’altra parola chiave importante, throws (con la “s” finale), consente di dichiarare esplicitamente i tipi di eccezioni (sotto forma di nomi di classe) che un metodo può generare.
Se un metodo di questo tipo genera un’eccezione, essa verrà propagata al codice chiamante, che dovrà gestirla:
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someException) {
System.out.println("Dividing by zero again? Do you even know what insanity is?");
}
}
}
L’output della console sarà:
Dividing by zero again? Do you even know what insanity is?
La struttura gerarchica delle eccezioni consente naturalmente di creare classi di eccezioni personalizzate che ereditano da quelle di base.
Grazie alle eccezioni personalizzate, Java consente di implementare percorsi di gestione degli errori specifici per l’applicazione.
Quindi, oltre alle eccezioni standard di Java, è possibile aggiungere le proprie.
Ogni eccezione personalizzata, come qualsiasi eccezione predefinita, può essere gestita utilizzando i blocchi standard try-catch-finally:
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // chiama il costruttore della classe padre
System.out.println("Warning! An exception is about to be thrown!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Just an exception. No explanation. Anyone got a problem?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
Output della console:
Warning! An exception is about to be thrown!
Just an exception. No explanation. Anyone got a problem?
Questo tutorial ha mostrato, tramite esempi, perché le eccezioni sono necessarie in Java, come si verificano (incluso come generarne una manualmente) e come gestirle utilizzando gli strumenti del linguaggio.
Le eccezioni che possono essere catturate e gestite sono di due tipi:
Checked Exceptions: gestite in fase di compilazione.
Unchecked Exceptions: gestite in fase di esecuzione.
Oltre a queste, esistono errori fatali che possono essere risolti solo riscrivendo il codice:
Errors: non possono essere gestiti.
Diverse strutture sintattiche (blocchi) vengono utilizzate per gestire le eccezioni:
try: codice che può generare un’eccezione.
catch: codice che gestisce l’eccezione.
finally: codice che viene sempre eseguito, indipendentemente dal verificarsi o meno di un’eccezione.
Inoltre, esistono parole chiave per controllare il processo di generazione delle eccezioni:
throw: genera manualmente un’eccezione.
throws: elenca le eccezioni che un metodo può generare.
L’elenco completo dei metodi della classe padre Exception è disponibile nella documentazione ufficiale Oracle.