Gestione delle eccezioni in Java

Introduzione

Un’ eccezione è un evento di errore che può verificarsi durante l’esecuzione di un programma e interrompe il suo normale svolgimento. Java fornisce un modo robusto e orientato agli oggetti per gestire scenari di eccezione noto come Gestione delle eccezioni di Java.

Le eccezioni in Java possono derivare da diverse situazioni, come dati errati inseriti dall’utente, guasti hardware, interruzioni della connessione di rete o un server di database non disponibile. Il codice che specifica cosa fare in scenari di eccezione specifici viene chiamato gestione delle eccezioni.

Lancio e cattura delle eccezioni

Java crea un oggetto eccezione quando si verifica un errore durante l’esecuzione di un’istruzione. L’oggetto eccezione contiene molte informazioni di debug, come la gerarchia dei metodi, il numero di riga in cui si è verificata l’eccezione e il tipo di eccezione.

Se si verifica un’eccezione in un metodo, il processo di creazione dell’oggetto eccezione e del suo passaggio all’ambiente di esecuzione viene chiamato “lanciare l’eccezione”. Il flusso normale del programma si interrompe e l’ambiente di esecuzione Java (JRE) cerca di trovare l’handler per l’eccezione. L’Exception Handler è il blocco di codice che può elaborare l’oggetto eccezione.

  • La logica per trovare l’Exception Handler inizia cercando nel metodo in cui si è verificato l’errore.
  • Se non viene trovato alcun handler appropriato, si sposterà al metodo chiamante.
  • E così via.

Quindi, se lo stack delle chiamate del metodo è A->B->C e si verifica un’eccezione nel metodo C, la ricerca dell’handler appropriato si sposterà da C->B->A.

Se viene trovato un handler eccezione appropriato, l’oggetto eccezione viene passato all’handler per elaborarlo. L’handler si dice “catturare l’eccezione”. Se non viene trovato alcun handler eccezione appropriato, il programma termina e stampa le informazioni sull’eccezione sulla console.

Il framework di gestione delle eccezioni Java viene utilizzato solo per gestire gli errori in fase di esecuzione. Gli errori in fase di compilazione devono essere corretti dallo sviluppatore che scrive il codice, altrimenti il programma non verrà eseguito.

Parole chiave per la gestione delle eccezioni in Java

Java fornisce parole chiave specifiche per scopi di gestione delle eccezioni.

  1. throw – Sappiamo che se si verifica un errore, viene creato un oggetto eccezione e quindi l’esecuzione di Java inizia a gestirli. A volte potremmo voler generare eccezioni esplicitamente nel nostro codice. Ad esempio, in un programma di autenticazione utente, dovremmo lanciare eccezioni ai client se la password è null. La parola chiave throw viene utilizzata per lanciare eccezioni all’esecuzione per gestirle.
  2. throws – Quando stiamo lanciando un’eccezione in un metodo e non la stiamo gestendo, allora dobbiamo utilizzare la parola chiave throws nella firma del metodo per far sapere al programma chiamante le eccezioni che potrebbero essere lanciate dal metodo. Il metodo chiamante potrebbe gestire queste eccezioni o propagarle al proprio metodo chiamante utilizzando la parola chiave throws. Possiamo fornire più eccezioni nella clausola throws, e può essere utilizzata anche con il metodo main().
  3. try-catch – Utilizziamo il blocco try-catch per la gestione delle eccezioni nel nostro codice. try è l’inizio del blocco e catch è alla fine del blocco try per gestire le eccezioni. Possiamo avere più blocchi catch con un blocco try. Il blocco try-catch può essere anche annidato. Il blocco catch richiede un parametro che dovrebbe essere di tipo Exception.
  4. finalmente – il blocco finally è opzionale e può essere usato solo con un blocco try-catch. Poiché l’eccezione interrompe il processo di esecuzione, potremmo avere alcune risorse aperte che non verranno chiuse, quindi possiamo usare il blocco finally. Il blocco finally viene sempre eseguito, sia che si verifichi un’eccezione sia che non si verifichi.

Un Esempio di Gestione delle Eccezioni

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • Il metodo testException() lancia eccezioni usando la parola chiave throw. La firma del metodo utilizza la parola chiave throws per far sapere al chiamante il tipo di eccezioni che potrebbe lanciare.
  • Nel metodo main(), sto gestendo le eccezioni usando il blocco try-catch nel metodo main(). Quando non le sto gestendo, le sto propagando a runtime con la clausola throws nel metodo main().
  • Il testException(-10) non viene mai eseguito a causa dell’eccezione e poi viene eseguito il blocco finally.

Il printStackTrace() è uno dei metodi utili nella classe Exception per scopi di debug.

Questo codice produrrà il seguente output:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

Alcuni punti importanti da notare:

  • Non possiamo avere una clausola catch o finally senza un’istruzione try.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • Non possiamo scrivere alcun codice tra i blocchi try-catch-finally.
  • Possiamo avere più blocchi catch con un singolo statement try.
  • I blocchi try-catch possono essere annidati in modo simile agli statement if-else.
  • Possiamo avere solo un blocco finally con un’istruzione try-catch.

Gerarchia delle eccezioni Java

Come già detto, quando viene generata un’eccezione, viene creato un oggetto eccezione. Le eccezioni Java sono gerarchiche e l’ereditarietà viene utilizzata per categorizzare diversi tipi di eccezioni. Throwable è la classe padre della gerarchia delle eccezioni Java e ha due oggetti figlio – Error ed Exception. Le Exception sono ulteriormente divise in Exception controllate e Exception non controllate.

  1. Errori: Error sono scenari eccezionali che esulano dallo scopo dell’applicazione e non è possibile prevedere e recuperare da essi. Ad esempio, guasto hardware, crash della macchina virtuale Java (JVM) o errore di memoria esaurita. Ecco perché abbiamo una gerarchia separata di Error e non dovremmo cercare di gestire queste situazioni. Alcuni dei comuni Error sono OutOfMemoryError e StackOverflowError.
  2. Eccezioni Verificate: Le eccezioni verificate sono scenari eccezionali che possiamo prevedere in un programma e cercare di recuperare da esso. Ad esempio, FileNotFoundException. Dovremmo catturare questa eccezione e fornire un messaggio utile all’utente e registrarla correttamente per scopi di debug. L’Exception è la classe genitore di tutte le eccezioni verificate. Se stiamo lanciando un’eccezione verificata, dobbiamo catcharla nello stesso metodo, o dobbiamo propagarla al chiamante utilizzando la parola chiave throws.
  3. Eccezione in Tempo di Esecuzione: Le eccezioni in tempo di esecuzione sono causate da una programmazione scorretta. Ad esempio, cercare di recuperare un elemento da un array. Dovremmo controllare prima la lunghezza dell’array prima di tentare di recuperare l’elemento, altrimenti potrebbe lanciare ArrayIndexOutOfBoundException durante l’esecuzione. RuntimeException è la classe genitore di tutte le eccezioni in tempo di esecuzione. Se stiamo lanciando una qualsiasi eccezione in tempo di esecuzione in un metodo, non è necessario specificarle nella clausola di firma del metodo throws. Le eccezioni in tempo di esecuzione possono essere evitate con una programmazione migliore.

Alcuni metodi utili delle Classi di Eccezione

Java Exception e tutte le sue sottoclassi non forniscono metodi specifici, e tutti i metodi sono definiti nella classe base – Throwable. Le classi Exception vengono create per specificare diversi tipi di scenari di Exception in modo che possiamo identificare facilmente la causa principale e gestire l’eccezione in base al suo tipo. La classe Throwable implementa l’interfaccia Serializable per l’interoperabilità.

Alcuni dei metodi utili della classe Throwable sono:

  1. public String getMessage() – Questo metodo restituisce il messaggio String di Throwable e il messaggio può essere fornito durante la creazione dell’eccezione tramite il suo costruttore.
  2. public String getLocalizedMessage() – Questo metodo è fornito in modo che le sottoclassi possano sovrascriverlo per fornire un messaggio specifico per la località al programma chiamante. L’implementazione della classe Throwable di questo metodo utilizza il metodo getMessage() per restituire il messaggio dell’eccezione.
  3. public synchronized Throwable getCause() – Questo metodo restituisce la causa dell’eccezione o null se la causa è sconosciuta.
  4. public String toString() – Questo metodo restituisce le informazioni su Throwable in formato String, la String restituita contiene il nome della classe Throwable e il messaggio localizzato.
  5. public void printStackTrace() – Questo metodo stampa le informazioni sulla traccia dello stack sul flusso di errore standard. Il metodo è sovraccaricato, e possiamo passare PrintStream o PrintWriter come argomento per scrivere le informazioni sulla traccia dello stack su file o flusso.

Java 7 Gestione automatica delle risorse e miglioramenti del blocco Catch

Se stai affrontando molte eccezioni in un singolo blocco try, noterai che il codice nel blocco catch consiste principalmente in codice ridondante per registrare l’errore. In Java 7, una delle nuove caratteristiche era un miglioramento del blocco catch in cui possiamo gestire più eccezioni in un unico blocco catch. Ecco un esempio del blocco catch con questa funzionalità:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

Ci sono alcune restrizioni, come ad esempio l’oggetto eccezione è finale e non possiamo modificarlo all’interno del blocco catch. Leggi l’analisi completa su Miglioramenti del blocco Catch in Java 7.

La maggior parte delle volte, usiamo il blocco finally solo per chiudere le risorse. A volte dimentichiamo di chiuderle e otteniamo eccezioni durante l’esecuzione quando le risorse sono esaurite. Queste eccezioni sono difficili da debuggare e potremmo dover controllare ogni punto in cui stiamo utilizzando quella risorsa per assicurarci di chiuderla. In Java 7, uno dei miglioramenti è stato try-with-resources, dove possiamo creare una risorsa nello stesso blocco try e usarla all’interno del blocco try-catch. Quando l’esecuzione esce dal blocco try-catch, l’ambiente di runtime chiude automaticamente queste risorse. Ecco un esempio del blocco try-catch con questo miglioramento:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

Java fornisce molte classi di eccezioni per noi da usare, ma a volte potremmo avere bisogno di creare le nostre classi di eccezioni personalizzate. Ad esempio, per notificare il chiamante di un tipo specifico di eccezione con il messaggio appropriato. Possiamo avere campi personalizzati per il tracciamento, come codici di errore. Ad esempio, diciamo che scriviamo un metodo per elaborare solo file di testo, quindi possiamo fornire al chiamante il codice di errore appropriato quando viene inviato un altro tipo di file in input.

Prima, crea MyException:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

Poi, crea un CustomExceptionExample:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

Possiamo avere un metodo separato per elaborare diversi tipi di codici di errore che otteniamo da metodi diversi. Alcuni di essi vengono consumati perché potremmo non voler notificare l’utente di quello, o alcuni di essi li rimanderemo per notificare all’utente il problema.

Ecco come sto estendendo Exception in modo che ogni volta che questa eccezione viene generata, deve essere gestita nel metodo o restituita al programma chiamante. Se estendiamo RuntimeException, non è necessario specificarlo nella clausola throws.

Questa è stata una decisione di progettazione. L’uso delle eccezioni controllate Exception ha il vantaggio di aiutare gli sviluppatori a capire quali eccezioni possono essere attese e intraprendere azioni appropriate per gestirle.

Linee guida per la gestione delle eccezioni in Java

  • Usa eccezioni specifiche – Le classi di base dell’ierarchia delle eccezioni non forniscono informazioni utili, ecco perché Java ha molte classi di eccezioni, come IOException con ulteriori sottoclassi come FileNotFoundException, EOFException, ecc. Dovremmo sempre throw e catch classi di eccezioni specifiche in modo che il chiamante possa conoscere facilmente la causa radice dell’eccezione e elaborarla. Questo rende il debug più facile e aiuta le applicazioni client a gestire le eccezioni in modo appropriato.
  • Lancia presto o fallisci in modo rapido – Dovremmo cercare di throw eccezioni il prima possibile. Considera il metodo processFile() sopra, se passiamo l’argomento null a questo metodo, otterremo la seguente eccezione:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

Durante il debug, dovremo prestare attenzione alla traccia dello stack per identificare con precisione la posizione effettiva dell’eccezione. Se cambiamo la logica di implementazione per controllare queste eccezioni anticipatamente come segue:

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... ulteriore elaborazione
}

Allora la traccia dello stack dell’eccezione indicherà dove si è verificata l’eccezione con un messaggio chiaro:

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • Cattura Tardiva – Poiché Java impone di gestire l’eccezione controllata o dichiararla nella firma del metodo, talvolta gli sviluppatori tendono a catch l’eccezione e registrare l’errore. Ma questa pratica è dannosa perché il programma chiamante non riceve alcuna notifica dell’eccezione. Dovremmo catch le eccezioni solo quando possiamo gestirle in modo appropriato. Ad esempio, nel metodo sopra, sto throwando le eccezioni al metodo chiamante per gestirle. Lo stesso metodo potrebbe essere utilizzato da altre applicazioni che potrebbero voler elaborare l’eccezione in modo diverso. Durante l’implementazione di qualsiasi funzionalità, dovremmo sempre throware le eccezioni al chiamante e lasciare che sia lui a decidere come gestirle.
  • Chiusura delle Risorse – Poiché le eccezioni bloccano l’elaborazione del programma, dovremmo chiudere tutte le risorse nel blocco finally o utilizzare il miglioramento try-with-resources di Java 7 per consentire al runtime di Java di chiuderle per voi.
  • Registrazione delle eccezioni – Dovremmo sempre registrare i messaggi di eccezione e, durante il lancio delle eccezioni, fornire un messaggio chiaro in modo che chi chiama sappia facilmente perché si è verificata l’eccezione. Dovremmo sempre evitare un blocco catch vuoto che consuma semplicemente l’eccezione e non fornisce dettagli significativi dell’eccezione per il debug.
  • Blocco catch singolo per eccezioni multiple – La maggior parte delle volte registriamo i dettagli delle eccezioni e forniamo un messaggio all’utente, in questo caso dovremmo utilizzare la funzione Java 7 per gestire eccezioni multiple in un unico blocco catch. Questo approccio ridurrà le dimensioni del nostro codice e avrà un aspetto più pulito.
  • Utilizzo di eccezioni personalizzate – È sempre meglio definire una strategia di gestione delle eccezioni durante la progettazione e anziché lanciare e catturare eccezioni multiple, possiamo creare un’eccezione personalizzata con un codice di errore e il programma chiamante può gestire questi codici di errore. È anche una buona idea creare un metodo di utilità per elaborare diversi codici di errore e utilizzarli.
  • Convenzioni di denominazione e pacchettizzazione – Quando si crea la propria eccezione personalizzata, assicurarsi che termini con Exception in modo che sia chiaro dal nome stesso che si tratta di una classe di eccezione. Inoltre, assicurarsi di confezionarle come è fatto nel Java Development Kit (JDK). Ad esempio, IOException è l’eccezione di base per tutte le operazioni di IO.
  • Usa le eccezioni con giudizio – Le eccezioni sono costose, e a volte non è necessario lanciare eccezioni affatto, e possiamo restituire una variabile booleana al programma chiamante per indicare se un’operazione è stata eseguita con successo o meno. Questo è utile quando l’operazione è facoltativa e non vuoi che il tuo programma si blocchi se fallisce. Ad esempio, durante l’aggiornamento delle quotazioni di borsa nel database da un servizio web di terze parti, potremmo voler evitare di lanciare eccezioni se la connessione fallisce.
  • Documenta le eccezioni lanciate – Usa Javadoc @throws per specificare chiaramente le eccezioni lanciate dal metodo. È molto utile quando stai fornendo un’interfaccia per altre applicazioni da utilizzare.

Conclusione

In questo articolo hai appreso come gestire le eccezioni in Java. Hai imparato a utilizzare throw e throws. Hai anche imparato su blocchi try (e try-with-resources), catch e finally.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java