La gestione delle eccezioni in Java

La gestione delle eccezioni in Java non è solitamente parte dei programmi didattici delle scuole superiori ma vale la pena dargli un’occhiata per alcune applicazioni più spinte o valorizzare gli alunni più volenterosi.

Introduzione

Le eccezioni nascono dall’esigenza di eseguire all’interno del codice una serie di istruzioni che possono creare un errore, un comportamento non desiderato che però di fatto crea un flusso di istruzioni necessariamente alternative a quelle convenzionali. Detto così non sembra nulla di strano, nulla che non sia affrontabile intuitivamente con un un if-else al punto giusto. L’idea però è quella di creare opportune classi che gestiscono al meglio le singole situazioni che possono anche ripetersi più volte nei nostri programmi perché magari manipoliamo gli stessi dati od oggetti in più punti del nostro codice.

Per usare i termini tecnici: quando si verifica una situazione di errore viene lanciata una “eccezione” che, se viene catturata, permette di gestire l’errore, altrimenti viene eseguita da Java un comportamento standard che potrebbe però di fatto terminare il programma o non comportarsi al meglio.

Pensiamo ad un calcolo algebrico non ammesso perché l’utente inserisce dati sbagliati, un file su cui leggere o scrivere che non è reperibile per qualche errore di permessi utenti, un accesso ad un indice di un vettore non esistente. Come si comporta Java in questi casi? Se “crasha” uscendo, possibile, non ci sia nulla di meglio che avvisare l’utente della situazione critica e poi al limite recuperare l’errore o uscire dal programma?

In Java esiste già una classe Exception preposta ad intercettare le eccezioni più importanti o standard ed una struttura ad hoc che serve a tentare di eseguire un frammento di codice critico ed eventualmente intercettare una o più eccezioni che possono essere scatenate da questi frammenti di codice: il try – catch e il throw che deliberatamente scatena una eccezioni in determinate circostanze.

Le eccezioni “standard”

ArithmeticException: viene scatenata nei problemi di calcolo aritmetico, perlopiù se si presentano delle divisioni per zero con variabili fuori controllo o non inizializzate correttamente.

ArrayIndexOutOfBoundsException: viene scatenata se si cerca di accedere a una posizione/indice inesistente in un array.

NullPointerException: è decisamente la più frequente nei problemi anche semplice poiché in Java tutto è una classe. Se si tenta di usare o accedere ad una variabile non semplice di tipo oggetto ma questa variabile è vuota, o non inizializzata e quindi ha un riferimento null

NumberFormatException: è scatenata spesso quando si utilizza la GUI di Java e si tenta di convertire un tipo di dato stringa in un numero o qualcosa senza il giusto formato atteso.

FileNotFoundException: la più intuitiva, si presenta quando c’è un errore nell’apertura o gestione di un file che ha problemi di permessi o in generale accesso/creazione.

StringIndexOutOfBoundsException : meno frequente ed assomiglia ad ArrayIndexOutOfBoundsException. Viene sollevata se si cerca di accedere a una posizione inesistente in una stringa.

ClassCastException: riscontrabile quando si usa massicciamente il polimorfismo, forse poco nelle scuole superiori. Viene sollevata se si tenta di fare un cast non lecito tra classi padre/figlio o non facente parti della corretta gerarchia.

Il costrutto try catch

Il modo migliore per capire le eccezioni è con un esempio! Cominciamo da quelle standard.

Immaginiamo di avere un frammento di codice con il calcolo della media di 5 numeri, ma invece di dividere per 5, per sbaglio si lascia zero:

class Main 
{  
  public static void main(String args[]) 
  { 
    int n = 0;
    int somma = 1+12+3+4+10;

    double media = somma / n;
    
    System.out.println(media);
      
  } 
}

Eseguendolo, in console avremo un errore palese che uscirà dall’esecuzione senza la possibilità di stampare l’ultima istruzione a video:

Exception in thread “main” java.lang.ArithmeticException: / by zero at Main.main(Main.java:8)

Proviamo invece il costrutto try catch e comprendiamone il significato:

class Main 
{  
  public static void main(String args[]) 
  { 
    int n = 0;
    int somma = 0;
    double media = 0;
    try
    {
      media = somma / n;
    }   
    catch(ArithmeticException e)
    {
      System.out.println("Occhio, hai sbagliato");
    }
    
    System.out.println("La media è: " + media);
      
  } 
}

L’errore ovviamente si ripresenta, ma questa volta non viene lanciato un arresto critico. Viene stampato il messaggio nel ramo catch, così come potrebbe essere eseguito altro codice di recupero o alternativo a quello che è andato in errore e poi il sistema continua al codice successivo, qui al messaggio successivo. Quindi abbiamo potuto gestire un frammento di codice critico senza perdere l’esecuzione globale.

Il try-catch è molto utile in blocchi di codice a rischio. Non è facile certamente individuare e progettare a priori quali frammenti inserire nel try-catch. Spesso alcuni programmatori, ne abusano inserendo a prescindere interi metodi. Diciamo che non ci sarebbero errori o problemi di performance (qualche benchmark indica l’uso del try catch aggiunga un 10% circa di lentezza), ma non è una pratica consigliata usarlo ovunque nel flusso del codice, se non necessario. Anche perché dai test, è sempre possibile poi inserirne successivamente.

L’unico “problema” sta nel fatto che le variabili dichiarate nella porzione del try, non sono visibili poi all’esterno del try stesso. Occhio quindi a dichiararle eventualmente immediatamente prima se ci dovessero servire anche all’esterno delle graffe.

Eccezioni personalizzate

Potrebbe essere utile scrivere nuove eccezioni che vanno sollevate se capita un certo evento indesiderato in una classe o, meglio, metodo realizzato da noi. Qui abbiamo la possibilità di estendere la classe Exception. Vediamo subito un esempio che commentiamo dopo. Creiamo un esempio classico minimale: Una classe alunno con attributi nome, cognome, anno di nascita, sezione. Quali ecezioni potrebbero presentarsi? Beh, in gase di inserimento delle informazioni di nome e cognome l’utente potrebbe digitare una stringa vuota, quindi una lunghezza non valida, o inserirci numeri o segni i punteggiatura quindi caratteri non validi. Nel caso dell’anno di nascita potremmo invece considerare errore un inserimento prima di una certa soglia, ad esempio prima del 1970.

EccezioniAlunno.java

package gestioneeccezioni;

/**
 *
 * @author alfedo centinaro
 */
public class EccezioniAlunno extends Exception
{

    /* Definisco le eccezioni che possono presentarsi, da gestire */
    public static final int LUNGHEZZA_NON_VALIDA= 1;
    public static final int CARATTERI_NON_VALIDI= 2;
    public static final int NUMERO_NON_VALIDO= 3;
    
    /* attributi di utilità */
    public String msg;
    public int code;

    public EccezioniAlunno(String msg, int code) {
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public int getCode() {
        return code;
    }
}

La classe estende ovviamente quella principale Exception. La cosa fondamentale, almeno per cominciare ad usare le proprie eccezioni senza particolari tecnicismi, è di creare tante voci public static final int che descrivono l’errore. Il resto può essere copiato di sana pianta.

Alunno.java

package gestioneeccezioni;

/**
 *
 * @author alfredo centinaro
 */
public class Alunno 
{
    private String  nome;
    private String  cognome;
    private int     annodinascita;  
    private String  sezione;

    public Alunno() {
        this.nome = "";
        this.cognome = "";
        this.annodinascita = 1970;
        this.sezione = "-";
    }

    public Alunno(String nome, String cognome, int annodinascita, String sezione) {
        this.nome = nome;
        this.cognome = cognome;
        this.annodinascita = annodinascita;
        this.sezione = sezione;
    }

    
    public String getNome() {
        return nome;
    }

    public void setNome(String nome) throws EccezioniAlunno{
        if (this.nome.length() <= 0)
            throw new EccezioniAlunno("Il nome non può essere vuoto", EccezioniAlunno.LUNGHEZZA_NON_VALIDA);
        if (!this.nome.matches("[a-zA-Z]*"))
            throw new EccezioniAlunno("Il nome non può contenere numeri o punteggiatura", EccezioniAlunno.CARATTERI_NON_VALIDI);
        
        this.nome = nome;
    }

    public String getCognome() {
        return cognome;
    }

    public void setCognome(String cognome) throws EccezioniAlunno {
        if (this.cognome.length() <= 0)
            throw new EccezioniAlunno("Il cognome non può esere vuoto", EccezioniAlunno.LUNGHEZZA_NON_VALIDA);        
        
        if (!this.nome.matches("[a-zA-Z]*"))
            throw new EccezioniAlunno("Il nome non può contenere numeri o punteggiatura", EccezioniAlunno.CARATTERI_NON_VALIDI);        
        
        this.cognome = cognome;
    }

    public int getAnnodinascita() {
        return annodinascita;
    }

    public void setAnnodinascita(int annodinascita) throws EccezioniAlunno {
        
        if (this.annodinascita < 1970)
            throw new EccezioniAlunno("Data non valida", EccezioniAlunno.NUMERO_NON_VALIDO);        
        this.annodinascita = annodinascita;
    }

    public String getSezione() {
        return sezione;
    }

    public void setSezione(String sezione) throws EccezioniAlunno {
        if (this.sezione.length() !=2 )
            throw new EccezioniAlunno("Sezione non valida", EccezioniAlunno.LUNGHEZZA_NON_VALIDA);           
        
        this.sezione = sezione;
    }
}

Nella nostra classe cambia qualcosina. Ogni metodo dove vogliamo gestire una eccezione, ha una dicitura throws EccezioniAlunno ovvero la nostra classe custom di eccezioni. Poi in realtà ci saranno degli if che controllano la bontà dei dati, nel nostro caso ma spesso sono quelli inseriti dall’utente. Se qualcosa non va, la dicitura throw scatena esplicitamente l’eccezione critica ed interrompe il flusso del software.

Cosa cambia quando chiamiamo il metodo in una classe main? I metodi che hanno specificato il throw, dovranno necessariamente essere chiamati all’interno del costrutto try-catch

public class Main 
{
    public static void main(String[] args) 
    {       
        Alunno a = new Alunno();
        
        try
        {
            a.setNome("Alfred102");
        }
        catch(EccezioniAlunno e)
        {
            System.out.println("ERRORE Alunno");
            e.printStackTrace();
        }
            
    }
}

La riga e.printStackTrace(); è opzionale solo per debug

Ultima modifica 18 Aprile 2022