Eccezioni e Gestione degli Errori in Java: Guida Completa

Eccezioni e Gestione degli Errori in Java: Guida Completa

Questa guida ti accompagna passo dopo passo nel mondo delle eccezioni in Java: cosa sono, perché esistono, come usarle correttamente e come creare le tue eccezioni personalizzate. È pensata per chi ha già le basi del linguaggio e vuole scrivere codice più robusto e professionale.

schema gestione eccezioni Java con try catch e finally

1. Cosa sono le eccezioni in Java?

Durante l’esecuzione di un programma possono verificarsi situazioni anomale: file mancanti, connessioni di rete interrotte, input non valido, divisione per zero e così via. In Java queste situazioni sono gestite tramite le eccezioni (exceptions).

Un’eccezione è un oggetto che rappresenta un errore o un evento imprevisto. Quando si verifica un errore, la JVM “lancia” (throw) un oggetto eccezione e il flusso normale del programma viene interrotto finché l’eccezione non viene catturata (catch) o il programma termina.

Tutte le eccezioni derivano dalla classe base java.lang.Throwable, che ha due sottoclassi principali:

  • Error: problemi gravi legati alla JVM o all’ambiente (es. OutOfMemoryError). Di solito non vanno gestiti nel codice applicativo.
  • Exception: errori che possono (e dovrebbero) essere gestiti dal programma.

Gerarchia semplificata

In modo molto semplificato:

Throwable
 ├── Error
 └── Exception
      ├── IOException (checked)
      ├── SQLException (checked)
      ├── RuntimeException (unchecked)
      │    ├── NullPointerException
      │    ├── IllegalArgumentException
      │    └── ...
      └── ...

2. Eccezioni Java Checked vs Unchecked: Differenze e Utilizzo

In Java esistono due grandi categorie di eccezioni applicative: checked e unchecked. La differenza è importante perché influenza il modo in cui scrivi il codice.

2.1 Eccezioni checked

Le eccezioni checked sono tutte quelle che estendono Exception ma non RuntimeException. Vengono verificate dal compilatore: se un metodo può lanciare una checked exception, hai due possibilità obbligatorie:

  • catturarla con un blocco try-catch;
  • propagarla dichiarandola nella firma del metodo con throws.

Esempi tipici di checked exception: IOException, SQLException, ParseException.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class LettoreFile {
    public static void leggiFile(String percorso) throws IOException {
        // IOException è una checked exception
        BufferedReader reader = new BufferedReader(new FileReader(percorso));
        String linea = reader.readLine();
        System.out.println(linea);
        reader.close();
    }
}

2.2 Eccezioni unchecked

Le eccezioni unchecked sono tutte quelle che estendono RuntimeException. Il compilatore non ti obbliga a gestirle esplicitamente: puoi scegliere se catturarle o lasciarle propagare.

Esempi comuni: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ArithmeticException.

public class Divisione {
    public static int dividi(int a, int b) {
        // Può lanciare ArithmeticException (unchecked) se b == 0
        return a / b;
    }

    public static void main(String[] args) {
        int risultato = dividi(10, 0); // ArithmeticException a runtime
        System.out.println(risultato);
    }
}

2.3 Quando usare le Eccezioni Java Checked e Unchecked?

  • Checked: per errori che il chiamante può ragionevolmente gestire (es. un file non trovato, una connessione al database fallita).
  • Unchecked: per errori di programmazione o di uso scorretto dell’API (es. argomenti non validi, stato incoerente dell’oggetto).

In pratica: le checked indicano situazioni prevedibili e “normali” ma indesiderate; le unchecked indicano bug o utilizzo errato del codice.

3. Gestire le Eccezioni Java con try-catch-finally

Lo strumento principale per gestire le eccezioni è il blocco try-catch-finally. La struttura generale è:

try {
    // codice che può lanciare eccezioni
} catch (TipoEccezione1 e) {
    // gestione dell'eccezione 1
} catch (TipoEccezione2 e) {
    // gestione dell'eccezione 2
} finally {
    // codice eseguito SEMPRE, con o senza eccezione
}

3.1 Esempio di base con try-catch

public class GestioneEccezioniBase {
    public static void main(String[] args) {
        try {
            int risultato = 10 / 0; // genera ArithmeticException
            System.out.println("Risultato: " + risultato);
        } catch (ArithmeticException e) {
            System.out.println("Errore: divisione per zero!");
            // e.printStackTrace(); // utile in fase di debug
        }

        System.out.println("Programma terminato.");
    }
}

3.2 Il blocco finally

Per approfondire ulteriormente, consulta la Documentazione ufficiale Oracle sulle eccezioni Java.

Il blocco finally viene eseguito sempre, sia che venga lanciata un’eccezione sia che non venga lanciata. È utile per rilasciare risorse (chiudere file, connessioni, stream…).

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class EsempioFinally {
    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader("dati.txt"));
            String linea = reader.readLine();
            System.out.println(linea);
        } catch (IOException e) {
            System.out.println("Errore durante la lettura del file: " + e.getMessage());
        } finally {
            // Chiusura della risorsa, se aperta
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.out.println("Errore durante la chiusura del file.");
                }
            }
        }
    }
}

3.3 Try-with-resources: Gestione Automatica delle Eccezioni Java

Dalla versione Java 7 esiste la sintassi try-with-resources, che chiude automaticamente le risorse che implementano l’interfaccia AutoCloseable. È preferibile quando possibile.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class EsempioTryWithResources {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("dati.txt"))) {
            String linea = reader.readLine();
            System.out.println(linea);
        } catch (IOException e) {
            System.out.println("Errore durante la lettura del file: " + e.getMessage());
        }
        // Nessun finally necessario per chiudere reader
    }
}

4. Lanciare Eccezioni Java: throw e throws

4.1 throw: lanciare un’eccezione

La parola chiave throw serve per lanciare esplicitamente un’eccezione da una certa posizione del codice.

public class ValidaEta {
    public static void impostaEta(int eta) {
        if (eta < 0) {
            throw new IllegalArgumentException("L'età non può essere negativa");
        }
        System.out.println("Età impostata a " + eta);
    }

    public static void main(String[] args) {
        impostaEta(-5); // genera IllegalArgumentException
    }
}

4.2 throws: dichiarare le eccezioni di un metodo

La parola chiave throws viene usata nella firma di un metodo per dichiarare che il metodo può lanciare una o più eccezioni checked. In questo modo “sposti” la responsabilità di gestirle al chiamante.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ParserDate {
    public static Date parseData(String testoData) throws ParseException {
        SimpleDateFormat formato = new SimpleDateFormat("dd/MM/yyyy");
        return formato.parse(testoData); // può lanciare ParseException (checked)
    }

    public static void main(String[] args) {
        try {
            Date data = parseData("31/02/2023"); // data non valida
            System.out.println(data);
        } catch (ParseException e) {
            System.out.println("Formato di data non valido: " + e.getMessage());
        }
    }
}

Nota: si possono dichiarare più eccezioni separate da virgola, ad esempio throws IOException, ParseException.

5. Eccezioni Java Personalizzate: Checked e Unchecked

A volte le eccezioni standard di Java non sono sufficienti a descrivere in modo chiaro il problema. In questi casi è utile definire eccezioni personalizzate, specifiche per la tua applicazione o dominio.

5.1 Eccezioni personalizzate checked

Per creare una checked exception personalizzata, estendi Exception (ma non RuntimeException).

// Eccezione checked personalizzata
public class SaldoInsufficienteException extends Exception {
    public SaldoInsufficienteException(String messaggio) {
        super(messaggio);
    }
}
public class ContoBancario {
    private double saldo;

    public ContoBancario(double saldoIniziale) {
        this.saldo = saldoIniziale;
    }

    public void preleva(double importo) throws SaldoInsufficienteException {
        if (importo > saldo) {
            throw new SaldoInsufficienteException(
                "Saldo insufficiente. Saldo attuale: " + saldo
            );
        }
        saldo -= importo;
    }

    public double getSaldo() {
        return saldo;
    }

    public static void main(String[] args) {
        ContoBancario conto = new ContoBancario(100.0);
        try {
            conto.preleva(150.0);
        } catch (SaldoInsufficienteException e) {
            System.out.println("Operazione fallita: " + e.getMessage());
        }
        System.out.println("Saldo finale: " + conto.getSaldo());
    }
}

5.2 Eccezioni personalizzate unchecked

Per creare una eccezione unchecked personalizzata, estendi RuntimeException. È utile per segnalare errori di programmazione specifici del tuo dominio.

// Eccezione unchecked personalizzata
public class ConfigurazioneNonValidaException extends RuntimeException {
    public ConfigurazioneNonValidaException(String messaggio) {
        super(messaggio);
    }
}
public class ServizioApplicazione {
    private String urlApi;

    public ServizioApplicazione(String urlApi) {
        if (urlApi == null || urlApi.isBlank()) {
            throw new ConfigurazioneNonValidaException(
                "L'URL dell'API non può essere nullo o vuoto"
            );
        }
        this.urlApi = urlApi;
    }

    public void esegui() {
        System.out.println("Chiamata all'API: " + urlApi);
    }

    public static void main(String[] args) {
        // Genererà l'eccezione unchecked
        ServizioApplicazione servizio = new ServizioApplicazione("");
        servizio.esegui();
    }
}

6. Esempi Pratici di Gestione delle Eccezioni Java

6.1 Validazione dell’input utente

In questo esempio leggiamo un numero da riga di comando e gestiamo diversi possibili errori: nessun argomento passato, formato non numerico, numero negativo.

public class LeggiNumero {
    public static int leggiNumeroPositivo(String[] args) {
        if (args.length == 0) {
            throw new IllegalArgumentException("Devi passare un numero come argomento.");
        }

        try {
            int valore = Integer.parseInt(args[0]);
            if (valore <= 0) {
                throw new IllegalArgumentException("Il numero deve essere positivo.");
            }
            return valore;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("L'argomento deve essere un numero intero.", e);
        }
    }

    public static void main(String[] args) {
        try {
            int numero = leggiNumeroPositivo(args);
            System.out.println("Hai inserito: " + numero);
        } catch (IllegalArgumentException e) {
            System.out.println("Errore di input: " + e.getMessage());
        }
    }
}

6.2 Wrapper di eccezioni (exception chaining)

6.2 Wrapping di Eccezioni Java (exception chaining)

// Eccezione di dominio
public class ErroreRepositoryException extends Exception {
    public ErroreRepositoryException(String messaggio, Throwable causa) {
        super(messaggio, causa);
    }
}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UtenteRepository {
    public String trovaNomeUtente(int id) throws ErroreRepositoryException {
        String url = "jdbc:esempio://localhost:5432/db";

        try (Connection conn = DriverManager.getConnection(url);
             PreparedStatement stmt = conn.prepareStatement("SELECT nome FROM utenti WHERE id = ?")) {

            stmt.setInt(1, id);
            ResultSet rs = stmt.executeQuery();

            if (rs.next()) {
                return rs.getString("nome");
            } else {
                return null;
            }

        } catch (SQLException e) {
            // Wrapping dell'eccezione tecnica in una eccezione di dominio
            throw new ErroreRepositoryException("Impossibile leggere l'utente dal database", e);
        }
    }
}

7. Buone Pratiche per le Eccezioni Java

  • Non ignorare le eccezioni: evitare blocchi catch (Exception e) {} vuoti. Almeno logga l’errore o ripristina una situazione consistente.
  • Non usare le eccezioni per il flusso normale: non dovrebbero sostituire gli if, ma gestire situazioni eccezionali.
  • Dai messaggi chiari: i messaggi delle eccezioni dovrebbero aiutare a capire cosa è andato storto e, se possibile, come rimediare.
  • Preferisci eccezioni specifiche: usare eccezioni mirate (es. FileNotFoundException invece di Exception) rende il codice più chiaro.
  • Mantieni la causa originale: quando fai wrapping di un’eccezione, passa sempre la causa al costruttore con super(messaggio, causa).
  • Gestisci vicino alla fonte o ai bordi del sistema: in genere o gestisci subito l’errore dove avviene, o lo lasci propagare fino a un livello alto che può reagire in modo sensato (es. mostrare un messaggio all’utente).

8. Riepilogo

In questa guida hai visto cosa sono le eccezioni in Java, la differenza tra checked e unchecked, come usare i blocchi try-catch-finally, le parole chiave throw e throws, come creare eccezioni personalizzate e alcuni esempi pratici di gestione degli errori.

Imparare a progettare e gestire bene le eccezioni è fondamentale per scrivere applicazioni affidabili, manutenibili e facili da debuggare. Quando scrivi nuovo codice, chiediti sempre: “Cosa può andare storto qui?” e decidi consapevolmente se e come gestire le eccezioni che possono verificarsi.

Commenti

Lascia un commento