Metodologie di Programmazione: Lezione 3

Riccardo Silvestri

Classi e Oggetti

Entriamo nel vivo della programmazione orientata agli oggetti. In Java per introdurre un nuovo tipo di oggetti bisogna definire una classe che determina lo stato dei nuovi oggetti e ne regola la creazione e il comportamento. Questo non è molto diverso da come sono introdotti nuovi tipi di oggetti in altri linguaggi come ad esempio Python. Però a differenza di Python, il linguaggio Java è tipato staticamente e permette di imporre l'incapsulamento restringendo l'accesso dall'esterno della classe. Queste caratteristiche rendono la definizione di un nuovo tipo di oggetti più sofisticata e delicata.

In questa lezione vedremo anche come definire classi annidate (statiche) e la loro utilità. Inoltre, continuiamo a spiegare il meccanismo delle eccezioni considerando i casi in cui è conveniente lanciare un'eccezione per segnalare che si è verificato un errore.

Orientazione agli oggetti

Cosa significa dire che un linguaggio è "orientato agli oggetti"? Per rispondere a questa domanda conviene fare un passo indietro e ricordarsi qual'è l'obbiettivo di un linguaggio di programmazione. Il principale obbiettivo è rendere facile la vita dei programmatori in tutte quelle fasi dello sviluppo del software in cui il linguaggio di programmazione riveste un ruolo importante (ad esempio, nella progettazione, nella scrittura del codice, nel debugging, nel testing e nella manutenzione del codice). E come fa un linguaggio a tentare di raggiungere questo obbiettivo? Cercando di gestire al meglio la complessità che è intrinseca in un qualsiasi sistema software di dimensioni non banali. E c'è, essenzialmente, un solo modo per fronteggiare la complessità: cercare di decomporre l'intero in parti meno complesse. I diversi linguaggi di programmazione usano filosofie e meccanismi differenti per aiutare i programmatori ad attuare questa strategia.

I linguaggi puramente procedurali, come ad esempio il C, offrono pochi mezzi: funzioni o procedure e la possibilità di costruire nuovi tipi aggregando altri tipi (ad esempio, tramite le struct). Una procedura permette di isolare una parte del sistema software dal resto. Così che il resto del sistema può disinteressarsi di come è fatta la procedura al suo interno e considerare solamente ciò che serve per poterla usare. Questo riduce la complessità riducendo il numero delle potenziali relazioni fra le varie parti del sistema. Sostanzialmente, è il principio dell'information hiding: suddividere il sistema in parti in modo tale che le loro interazioni si possano definire in base a semplici interfacce che risultano indipendenti da come le parti sono implementate (l'informazione che viene nascosta). Questo principio è così consolidato e naturale che ormai è quasi dato per scontato. Oltre ad esso ci sono altri aspetti di un linguaggio che possono aiutare a fronteggiare la complessità anche se non sono altrettanto importanti. La possibilità di costruire nuovi tipi tramite semplice aggregazione di altri tipi aiuta a ridurre la complessità tramite la diminuzione della distanza tra la natura delle informazioni reali e la loro rappresentazione nel sistema. Questo a sua volta migliora la leggibilità del codice e quindi anche la capacità di modificare ed estendere le funzionalità del sistema.

I linguaggi orientati agli oggetti come Java offrono mezzi più sofisticati per fronteggiare la complessità del software. Uno dei più importanti trae la sua forza dalla combinazione delle due caratteristiche sopra menzionate. Una classe può essere più efficace di una mera aggregazione di dati e funzioni nell'isolare una parte del sistema software perché può rendere più chiare sia le relazioni tra dati e procedure che la separazione dal resto del sistema. Questo è il cosiddetto incapsulamento (encapsulation), cioè la versione orientata agli oggetti dell'information hiding. Ad esempio, in un sistema software per la gestione dei dati del personale di una azienda, ci potrebbe essere una classe Impiegato per rappresentare e manipolare i dati relativi agli impiegati. Questa conterrà, oltre ai soliti campi (nome, cognome, data_di_nascita, ecc.), anche dei metodi: un metodo età() che calcola l'età attuale, un metodo stipendio() che calcola la busta paga, un metodo stampa() per la stampa formattata dei dati dell'impiegato, ecc. Ogni istanza della classe Impiegato rappresenta uno specifico impiegato. Complessivamente i valori dei campi di un oggetto sono lo stato di quell'oggetto. Il comportamento di un oggetto, cioè il risultato dell'invocazione di un qualsiasi metodo relativamente a quell'oggetto, dipende dallo stato dell'oggetto. Così, se rossi è il riferimento all'oggetto che rappresenta l'impiegato Mario Rossi, l'invocazione del metodo rossi.età() ritorna proprio l'età di Mario Rossi, così come verdi.età() ritorna invece l'età di Giuseppe Verdi, se verdi è un riferimento all'oggetto che rappresenta l'impiegato Giuseppe Verdi.

Nuovi tipi di oggetti

Abbiamo già visto in vari esempi la definizione di campi e metodi statici. I metodi statici sono relativi alla classe e non sono metodi degli oggetti e quindi non possono essere invocati in relazione agli oggetti. La definizione dei campi e metodi degli oggetti segue la stessa sintassi dei campi e metodi statici senza però il modificatore static. Ma gli oggetti hanno anche dei metodi speciali che sono invocati per costruire gli oggetti stessi.

Costruttori

Un costruttore è un metodo speciale che è invocato solamente quando un nuovo oggetto della classe è creato. Il compito di un costruttore è di svolgere tutte quelle elaborazioni e inizializzazioni (dei campi) che sono necessarie affinché l'oggetto appena creato risulti valido. Un esempio d'uso di un costruttore lo abbiamo già visto per la classe Scanner. Anche la sintassi dei costruttori è speciale:

modificatori NomeClasse(parametri) {
    corpo-del-costruttore
}

Come si vede non c'è un tipo del valore ritornato perché questo è sempre il tipo dell'oggetto (cioè il tipo determinato dalla classe). Inoltre, il nome del costruttore deve sempre coincidere con il nome della classe. Generalmente l'unico modificatore usato è public (raramente è differente). Grazie all'overloading ci possono essere più costruttori per la stessa classe. C'è sempre un costruttore di default (senza parametri) che è invocato solamente quando non c'è ne uno definito che può essere usato. Questo implica che una classe può anche non avere costruttori (definiti).

Come si è detto, un costruttore è invocato solamente quando un nuovo oggetto della classe viene creato. Questo avviene in congiunzione con l'operatore new. Ad esempio, per creare un nuovo oggetto della classe NomeClasse si può scrivere:

NomeClasse nuovoOggetto = new NomeClasse();

L'espressione new NomeClasse() crea un'istanza della classe NomeClasse e ritorna il riferimento all'oggetto creato. È importante osservare che la variabile nuovoOggetto non conterrà un oggetto di tipo NomeClasse ma un riferimento ad un oggetto di quel tipo. Si può immaginare un riferimento ad un oggetto come un puntatore a quell'oggetto, come nel C/C++.

A differenza del C/C++, in Java non dobbiamo preoccuparci di rilasciare la memoria allocata per un oggetto. Infatti, ci penserà il garbage collector che automaticamente e costantemente durante l'esecuzione del programma rilascia la memoria degli oggetti che non sono più usati dal programma.

Classi per oggetti

Per illustrare le regole di base di Java per definire nuovi tipi di oggetti useremo l'esempio classico di una classe Dipendente per rappresentare i dipendenti di una ipotetica azienda. Ogni oggetto della classe Dipendente rappresenterà quindi un particolare dipendente. Consideriamo che i dati di un dipendente comprendano il nome, cognome1 e l'importo dello stipendio:

package mp;

/** Un oggetto {@code Dipendente} rappresenta un dipendente dell'azienda */
public class Dipendente {
    private String nomeCognome;
    private double stipendio;
}

I campi sono stati dichiarati private perché non vogliamo che dall'esterno della classe si possa modificarli senza passare per un eventuale controllo effettuato dalla classe stessa2. E infatti introduciamo dei metodi pubblici per poter leggere tali dati:

public class Dipendente {
    /** @return il nome e cognome di questo dipendente */
    public String getNomeCognome() { return nomeCognome; }

    /** @return lo stipendio di questo dipendente */
    public double getStipendio() { return stipendio; }

    private String nomeCognome;
    private double stipendio;
}

Di solito un metodo che semplicemente ritorna il valore di un campo è detto getter e il nome inizia con get. Già in questa forma così semplice si potrebbero creare oggetti di tipo Dipendente ma sarebbero di scarsa utilità,

Dipendente dip = new Dipendente();     // È invocato il costruttore di default
String nomCog = dip.getNomeCognome();  // Ritorna null

perché non si ha neanche la possibilità di impostare il nome e cognome o lo stipendio (ricordiamo che avranno i valori di default che sono null per nomeCognome e 0 per stipendio). Per ovviare a ciò possiamo introdurre un opportuno costruttore:

public class Dipendente {
    /** Crea un dipendente con i dati specificati.
     * @param nomeCognome  nome e cognome del dipendente
     * @param stipendio  stipendio del dipendente */
    public Dipendente(String nomeCognome, double stipendio) {
        this.nomeCognome = nomeCognome;
        this.stipendio = stipendio;
    }
    . . .
}

Avendo definito almeno un costruttore non è più possibile creare oggetti Dipendente tramite il costruttore di default. Nel costruttore è stata usata la parola chiave this che rappresenta il riferimento all'oggetto stesso (è simile a self di Python). Più precisamente, nel corpo di ogni metodo degli oggetti di una classe, this ha come valore il riferimento all'oggetto (della classe) su cui è stato invocato il metodo. Qui this è usato per potersi riferire ai campi nomeCognome e stipendio dell'oggetto che altrimenti sarebbero stati mascherati dagli omonimi argomenti del costruttore. Adesso possiamo creare oggetti che hanno uno stato adeguato:

Dipendente rossi = new Dipendente("Mario Rossi", 2000.0);
Dipendente verdi = new Dipendente("Ugo Verdi", 1850.0);
for (Dipendente d : new Dipendente[] {rossi, verdi})
    out.println("Lo stipendio di "+d.getNomeCognome()+" è "+
            d.getStipendio());

Questo frammento di codice stamperà:

Lo stipendio di Mario Rossi è 2000.0
Lo stipendio di Ugo Verdi è 1850.0

Si tenga presente che le inizializzazioni relative alle dichiarazioni dei campi di un oggetto sono eseguite prima dell'esecuzione del costruttore. Così potremmo introdurre un costruttore Dipendente(String nomeCognome) che non imposta lo stipendio che potrebbe essere impostato nella dichiarazione del campo con un valore di default, ad esempio, private double stipendio = 1000.0. Tale valore sarebbe poi sostituito da quello esplicitamente impostato dal costruttore che abbiamo definito.

Adesso potremmo introdurre un metodo per modificare lo stipendio:

public class Dipendente {
    . . .
    /** Imposta un nuovo stipendio per questo dipendente.
     * @param nuovoStipendio  l'importo del nuovo stipendio */
    public void setStipendio(double nuovoStipendio) {
        stipendio = nuovoStipendio;
    }
    . . .
}

Se la nostra classe fosse più realistica il metodo setStipendio al pari del costruttore dovrebbe fare dei controlli sulla congruità dei dati impostati, ma per adesso preferiamo privilegiare la semplicità. Tipicamente un metodo come questo che semplicemente imposta il valore di un campo è detto setter e il nome inizia con set.

Mutabilità e immutabilità

In Java tutti i valori sono oggetti eccetto i valori primitivi. Ad esempio, un oggetto s di tipo String ha uno stato immutabile da cui dipendono il comportamento dei suoi metodi come s.charAt e s.length. Invece, un oggetto in di tipo Scanner è mutabile, ad esempio, ogni invocazione del metodo in.nextLine cambia lo stato di in per far sì che le invocazioni successive dei suoi metodi leggano a partire dalla fine dell'ultima linea letta. Un oggetto di tipo String è detto appunto immutabile perché il suo stato non può mai cambiare dopo che è stato creato. Mentre un oggetto di tipo Scanner è detto mutabile perché il suo stato può cambiare.

Anche i metodi possono essere classificati in base alla possibilità se possano cambiare o meno lo stato dell'oggetto sul quale sono invocati. I metodi che possono cambiare lo stato sono detti mutator methods e quelli che non lo possono cambiare sono detti accessor methods. Ovviamente tutti i metodi di oggetti immutabili sono necessariamente accessor methods. Ma i metodi di oggetti mutabili possono essere sia mutator che accessor methods. Ad esempio, il metodo nextLine degli oggetti di tipo Scanner è un mutator mentre hasNextLine è un accessor method. Tutti i metodi getter sono accessors e i metodi setter sono mutators.

L'immutabilità è una proprietà desiderabile sopratutto in relazione all'accesso parallelo o concorrente agli oggetti. Gli effetti dell'accesso parallelo o concorrente di più processi o threads su uno stesso oggetto sono molto più semplici da gestire se l'oggetto è immutabile. Anche per questo alcune caratteristiche tipiche dei linguaggi funzionali sono di aiuto. Ma ne parleremo più avanti.

Sull'ordine

Non c'è nessun ordine imposto da Java per i membri di una classe siano essi statici o degli oggetti, privati o pubblici, campi o metodi. In queste lezioni seguiremo la convenzione di mettere i membri pubblici prima di quelli privati. In questo modo l'interfaccia pubblica della classe e degli oggetti è messa per prima. Inoltre, i costruttori pubblici precedono gli altri metodi e se ci sono metodi o campi statici pubblici questi precedono tutti gli altri membri non statici. Riassumendo seguiremo il seguente ordine:

public class NomeClasse {
    Membri statici pubblici

    Costruttori pubblici

    Metodi e campi degli oggetti pubblici

    Tutti gli altri membri
}

L'importante è scegliere un ordine (ragionevole) è poi seguirlo con coerenza. Questo aiuta la leggibilità del codice che a sua volta aiuta la manutenzione e la modificabilità del codice.

Statici e non statici

Per approfondire le relazioni tra membri statici e membri degli oggetti, consideriamo che ai dipendenti del nostro esempio gli si debba assegnare un codice identificativo. Si potrebbe usare il codice fiscale, ma l'azienda preferisce usare un codice gestito internamente. Per semplicità assumeremo che il codice sia un numero intero e che questo sia generato al momento della creazione di un oggetto Dipendente. I dettagli di come i codici sono generati non devono essere pubblici. Siccome la generazione e l'assegnamento dei codici sono procedure relative a tutti gli oggetti Dipendente e non appartengono a nessun oggetto specifico, è naturale che siano definite al livello della classe quindi tramite membri statici.

. . .
public Dipendente(String nomeCognome, double stipendio) {
    this.nomeCognome = nomeCognome;
    this.stipendio = stipendio;
    codice = nuovoCodice();           // Assegna un codice al nuovo dipendente
}

/** @return il codice di questo dipendente */
public long getCodice() { return codice; }

. . .

private static long ultimoCodice;     // Ultimo codice usato

private static long nuovoCodice() {   // Ritorna un nuovo codice
    ultimoCodice++;
    return ultimoCodice;
}

private final long codice;                  // Il codice del dipendente
. . .

I codici sono semplicemente generati in sequenza. Abbiamo modificato il costruttore per assegnare un codice al nuovo dipendente e abbiamo aggiunto un getter per il codice. Il campo codice ha il modificatore final che significa che il valore del campo non può essere modificato dopo che l'oggetto è stato creato. Inoltre, se un campo è final deve essere esplicitamente inizializzato (e una sola volta) o nella dichiarazione o nel costruttore.

Si noti che per invocare un metodo statico o accedere a un campo statico all'interno della classe a cui appartengono non è necessario specificare il nome della classe. Forse è bene ricordare che il campo ultimoCodice e il metodo nuovoCodice sono statici e quindi non sono campi o metodi degli oggetti. Invece appartengono alla classe Dipendente però Java permette che si possa accedere ad essi tramite un oggetto della classe, cioè, se ad esempio rossi è un oggetto Dipendente, non è un errore rossi.ultimoCodice o rossi.nuovoCodice(), ma sicuramente è un cattivo stile di programmazione. Viceversa non è mai possibile accedere a campi o metodi degli oggetti senza un riferimento esplicito o implicito (all'interno della classe stessa) a un oggetto.

L'implementazione che abbiamo fornito per i codici non è adeguata per una gestione persistente (cioè, mantenuta su supporti persistenti come i file) dei dipendenti. Questo perché la generazione e l'assegnamento dei codici dipende dall'ordine con cui gli oggetti dei dipendenti sono creati. E anche se si rispettasse sempre lo stesso ordine sia nel recupero da supporto persistente che nel salvataggio sul medesimo supporto, se degli oggetti Dipendente fossero eliminati i codici cambierebbero. Per ovviare a questo problema potremmo aggiungere un nuovo costruttore che richiede anche il codice e che verrebbe usato solamente quando i dati del dipendente sono recuperati dal supporto persistente. Ovviamente occorre anche che la procedura per la generazione dei codici tenga conto di ciò. Inoltre anche la procedura di generazione dei codice deve poter tener conto che un codice è in uso. Per questo introduciamo un metodo che aggiorna la generazione dei codici tenendo conto che un dato codice è in uso.

. . .
/** Crea un dipendente con i dati specificati. Da usarsi solamente se al
 * dipendente è già stato assegnato un codice.
 * @param nomeCognome  nome e cognome del dipendente
 * @param stipendio  stipendio del dipendente
 * @param codice  codice del dipendente */
public Dipendente(String nomeCognome, double stipendio, long codice) {
    this.nomeCognome = nomeCognome;
    this.stipendio = stipendio;
    this.codice = codice;
    codiceUsato(codice);          // Comunica che il codice è usato
}

. . .

// Aggiorna la generazione dei codici tenendo conto che il dato codice è in uso
private static void codiceUsato(long codice) {
    ultimoCodice = Math.max(ultimoCodice, codice);
}
. . .

Invocare un costruttore in un altro costruttore

A questo punto potremmo ridefinire il primo costruttore invocando opportunamente il secondo. Per invocare un costruttore all'interno di un altro costruttore bisogna usare this al posto del nome e l'invocazione del costruttore deve essere la prima istruzione:

. . .
public Dipendente(String nomeCognome, double stipendio) {
    this(nomeCognome, stipendio, nuovoCodice());
}
. . .

Bisogna sempre evitare la duplicazione di codice. Non solo questo rende il codice più snello e quindi più leggibile ma aiuta a mantenerne la coerenza evitando che parti di codice che dovrebbero essere uguali possano per errore diventare differenti, ad esempio, ci si può dimenticare di riportare una modifica in tutte le parti.

A questo punto potremmo anche definire un terzo costruttore per evitare di dover specificare al momento della creazione anche lo stipendio:

. . .
/** Crea un dipendente con il dato nome e cognome e lo stipendio a zero.
 * @param nomeCognome  nome e cognome del dipendente */
public Dipendente(String nomeCognome) {
    this(nomeCognome, 0);
}
. . .

Classi annidate

In una classe si possono anche definire delle classi, oltre alle interfacce che vedremo in seguito. Queste possono essere di due tipi classi annidate statiche (nested static classes) e classi interne (inner classes). Per adesso vedremo solamente quelle del primo tipo. Una classe annidata statica è direttamente connessa alla classe che la contiene al pari di qualsiasi altro membro statico di una classe. Generalmente, una classe annidata statica è usata per definire un tipo che ha senso ed è utile nel contesto della classe di definizione ma che potrebbe non avere senso o non essere utile al di fuori della classe di definizione oppure quando non si vuole introdurre in un package troppe classi o si vuole restringerne l'accesso. Essenzialmente si tratta di un meccanismo che può risultare utile per migliorare la struttura logica delle definizioni di più classi. La sintassi (un po' semplificata) della definizione di una classe annidata statica è semplice e diretta:

public class NomeClasse {
    modificatori static class NomeClasseAnnidata {
        corpo della classe annidata
    }
}

I modificatori possono essere tutti quelli che si possono usare nella definizione di una classe, in particolare possono essere quelli di accesso public e private. Anche il corpo può contenere qualsiasi cosa può contenere una classe, comprese definizioni di classi ulteriormente annidate. Dall'interno della classe che ne contiene la definizione (NomeClasse) la classe annidata è accessibile tramite il suo nome semplice (NomeClasseAnnidata). Invece, dall'esterno (purché non sia privata) è accessibile solamente specificando anche il nome della classe che la contiene (NomeClasse.NomeClasseAnnidata).

Come esempio aggiungiamo alla classe Dipendente i dati riguardanti i contatti del dipendente, indirizzo, telefono, ecc. Questi possono essere raggruppati in una classe che chiamiamo Contatti. È utile averli raggruppati perché così possiamo avere un metodo che li ritorna tutti insieme e la separazione dagli altri dati risulta esplicitamente dalla struttura delle classi. Inoltre, definendoli in una classe annidata ci permette di limitare l'accesso dall'esterno ad alcune funzionalità. Per semplicità consideriamo solamente indirizzo e numero di telefono (la classe potrebbe gestirne altri, come più recapiti telefonici, indirizzi email, ecc.). La definizione seguente è una delle tante possibili:

. . .
public class Dipendente {
    /** Mantiene i contatti di un dipendente come indirizzo, telefono, ecc. */
    public static class Contatti {
        /** @return  l'indirizzo del dipendente */
        public String getIndirizzo() { return indirizzo; }

        /** @return  il recapito telefonico del dipendente */
        public String getTelefono() { return telefono; }


        private Contatti() {
            indirizzo = "";
            telefono = "";
        }

        private String indirizzo;
        private String telefono;
    } 
    . . .

Per come l'abbiamo definita gli unici membri che hanno accesso pubblico sono i getter per l'indirizzo e il telefono. Il costruttore è privato, questo significa che dall'esterno della classe non è possibile costruire oggetti di tipo Dipendente.Contatti. Infatti, vogliamo che i contatti di un dipendente possano essere impostati e quindi creati solamente da un oggetto Dipendente (quello a cui i contatti si riferiscono), vedremo fra poco come. La possibilità di restringere la creazione di oggetti all'interno di una classe è uno dei vantaggi offerti dalle classi annidate. Si noti che abbiamo inizializzato i campi indirizzo e telefono con stringhe vuote perché l'inizializzazione di default sarebbe stata il valore null che è meno adatto a rappresentare l'assenza, almeno per questo tipo di dati3. Vediamo ora come i contatti sono gestiti dagli oggetti Dipendente:

. . .
public Dipendente(String nomeCognome, double stipendio, long codice) {
    this.nomeCognome = nomeCognome;
    this.stipendio = stipendio;
    this.codice = codice;
    codiceUsato(codice);          // Comunica che il codice è usato
    contatti = new Contatti();
}

. . .

/** @return i contatti di questo dipendente */
public Contatti getContatti() { return contatti; }

/** Imposta l'indirizzo di questo dipendente.
 * @param indirizzo  il nuovo indirizzo */
public void setIndirizzo(String indirizzo) { contatti.indirizzo = indirizzo; }

/** Imposta il recapito telefonico di questo dipendente.
 * @param telefono  il nuovo numero di telefono */
public void setTelefono(String telefono) { contatti.telefono = telefono; }

. . .

private Contatti contatti;
. . .

Abbiamo aggiunto un campo contatti all'oggetto Dipendente che è inizializzato nel costruttore. Si noti che dall'interno della classe è possibile creare oggetti di tipo Contatti. Il metodo getContatti ritorna l'oggetto Contatti ma siamo sicuri che dall'esterno della classe non può essere modificato. Invece può essere modificato dall'interno e infatti i metodi setIndirizzo e setTelefono modificano i valori dei suoi campi privati. Ecco un piccolo esempio d'uso della classe che abbiamo definito:

Dipendente rossi = new Dipendente("Mario Rossi", 2000.0);
rossi.setIndirizzo("Roma, via Rossini, 15");
rossi.setTelefono("06 8989898");
Dipendente verdi = new Dipendente("Ugo Verdi", 1850.0);
verdi.setIndirizzo("Roma, via G. Verdi, 30");
for (Dipendente d : new Dipendente[]{rossi, verdi}) {
    out.print("Dipendente: "+d.getNomeCognome());
    out.println("  codice: "+d.getCodice());
    out.println("    stipendio:  " + d.getStipendio());
    Dipendente.Contatti con = d.getContatti();
    out.println("    Indirizzo: " + con.getIndirizzo());
    out.println("    Telefono: " + con.getTelefono());
}

Si noti che dall'esterno della classe Dipendente, per riferirsi al tipo Contatti bisogna dichiarare il nome completo Dipendente.Contatti. In alternativa si può usare un import statico. Se eseguito stampa:

Dipendente: Mario Rossi  codice: 1
    stipendio:  2000.0
    Indirizzo: Roma, via Rossini, 15
    Telefono: 06 8989898
Dipendente: Ugo Verdi  codice: 2
    stipendio:  1850.0
    Indirizzo: Roma, via G. Verdi, 30
    Telefono: 

Errori ed eccezioni II

Alcuni dei metodi che abbiamo definito per la classe Dipendente necessiterebbero di un controllo sui dati forniti in input. Ad esempio, non ha senso impostare uno stipendio negativo o un nome e cognome contenente caratteri estranei come le cifre. In queste e in tutte le situazioni che possono accadere durante l'esecuzione di un programma in cui qualcosa non soddisfa una certa condizione, si può comunicare l'anomalia lanciando un'opportuna eccezione. La sintassi è semplice,

throw new NomeEccezione(eventuali parametri);

La parola chiave throw (che significa appunto lancia) ha l'effetto di lanciare l'eccezione che è stata appena creata. Ovviamente questo fa immediatamente terminare l'esecuzione del metodo in cui viene eseguita una tale istruzione (a meno che non si trovi all'interno di un blocco di try - catch). Quindi sarà poi il metodo invocante che potrà eventualmente gestire l'eccezione o qualche altro metodo più in alto nello stack delle invocazioni. Se nessun metodo la cattura, l'eccezione fa terminare il programma.

Un parametro che tutti i costruttori di eccezione accettano è una stringa che rappresenta un messaggio di spiegazione, oppure nessun parametro. Come esempio consideriamo una condizione che accade molto spesso di dover controllare e che in particolare dovrebbe essere controllata per il metodo setIndirizzo:

/** Imposta l'indirizzo di questo dipendente.
 * @param indirizzo  il nuovo indirizzo 
 * @throws NullPointerException  se indirizzo è null */
public void setIndirizzo(String indirizzo) {
    if (indirizzo == null)
        throw new NullPointerException("Indirizzo non può essere null");
    contatti.indirizzo = indirizzo;
}

È buona regola documentare le eventuali eccezioni che possono venir lanciate da un metodo. Siccome è molto comune controllare tale condizione, la classe Objects, del package java.util, ha il metodo statico di utilità Objects.requireNonNull che esegue il controllo sul parametro di input e eventualmente lancia l'eccezione NullPointerException, del package java.lang. Così potremmo riscrivere il metodo setIndirizzo come segue:

public void setIndirizzo(String indirizzo) {
    Objects.requireNonNull(indirizzo, "Indirizzo non può essere null");
    contatti.indirizzo = indirizzo;
}

Un altro esempio tipico è il controllo circa un valore numerico che deve essere compreso in un certo intervallo. Nel nostro caso il metodo setStipendio (e anche il costruttore) dovrebbe controllare che non si tenti d'impostare un importo negativo. In casi come questi si può lanciare un'eccezione di tipo IllegalArgumentException, del package java.lang, che serve proprio a segnalare errori dovuti a un valore non permesso per un argomento di un metodo. Così possiamo riscrivere il nostro metodo:

/** Imposta un nuovo stipendio per questo dipendente.
 * @param nuovoStipendio  l'importo del nuovo stipendio
 * @throws IllegalArgumentException  se lo stipendio è negativo */
public void setStipendio(double nuovoStipendio) {
    if (nuovoStipendio < 0)
        throw new IllegalArgumentException("Stipendio non può essere negativo");
    stipendio = nuovoStipendio;
}

Esercizi

[Errori]    Nel seguente programma ci sono tre errori trovarli e spiegarli.

class Pair {
    public Pair(int v) { val = v; }
    public Pair(String s) { str = s; }

    public void set(String s) { str = s; }
    public int getVal() { return val; }
    public String getStr() { return str; }

    private final int val;
    private String str;
}

public class Test {
    public static void main(String[] args) {
        Pair p = new Pair();
        Pair p2 = new Pair(13);
        System.out.println(p2.getStr());
        Pair pp;
        pp.set("A");
    }
}

[Immutabilità]    Si consideri il seguente frammento di codice Java:

String s = "prima";
String s2 = s;
s2 += "dopo";
System.out.println(s);

Spiegare perché se viene eseguito stamperà prima invece di primadopo.

[PiùContatti]    Migliorare la classe Contatti aggiungendo un indirizzo email e anche la possibilità di specificare fino a 5 recapiti telefonici. Definire però un solo metodo getter e un solo metodo setter che permettano, rispettivamente, di leggere e impostare ognuno dei 5 possibili recapiti telefonici, ma uno solo alla volta.

[ControlloArgomenti]    Aggiungere ai metodi e ai costruttori della classe Dipendente i seguenti controlli sugli argomenti: indirizzo, telefono e nomeCognome non devono essere null, stipendio non deve essere negativo, telefono deve contenere solamente caratteri che sono o cifre 0-9 o lo spazio, nomeCognome deve contenere solamente caratteri che sono o lettere o lo spazio o l'apice (per determinare se un carattere è una lettera si può usare Character.isLetter).

[CodiceFiscale]    Aggiungere un metodo per impostare il codice fiscale del dipendente. Il metodo dovrebbe fare un controllo di correttezza sintattica secondo quanto specificato qui.

[Nascita]    Aggiungere alla classe Dipendente la possibilità di gestire anche la data e il comune di nascita del dipendente. Per rappresentare la data di nascita usare gli oggetti della classe LocalDate nel package java.time della piattaforma 8 di Java. Per costruire un oggetto LocalDate si può usare il metodo statico LocalDate.of. Si tenga presente che gli oggetti LocalDate sono immutabili, quindi possono essere ritornati da un metodo getter senza problemi. Aggiungere a Dipendente sia un getter che un setter.

[MettiAllaProva]    Scrivere un programma che mette alla prova le eventuali modifiche apportate alla classe Dipendente in uno o più degli esercizi precedenti. Questo è in realtà un meta-esercizio perché può essere applicato a tutti gli esercizi che seguono.

[StampaDipendenti]    Aggiungere alla classe Dipendente un metodo statico che prende in input un array di Dipendente e stampa i dati relativi ai dipendenti nell'array.

[Razionali]    Definire una nuova classe Razionale per rappresentare numeri razionali. I campi dovrebbero essere numeratore e denominatore entrambi di tipo long. La classe deve avere due costruttori Razionale(long num, long den) e Razionale(double d). Il secondo costruttore dovrà fare una conversione determinando il numeratore e il denominatore dal numero in virgola mobile d. Questa conversione potrebbe non essere possibile se d è troppo vicino a zero (ma non è zero) o è troppo grande.

[Razionali+]    Aggiungere alla classe Razionale il metodo void add(Razionale r) che addiziona il razionale r modificando così il valore dell'oggetto (su cui è invocato il metodo). Aggiungere anche un metodo statico static Razionale add(Razionale r1, Razionale r2) che ritorna un nuovo oggetto della classe Razionale il cui valore è pari alla somma del valore di r1 e il valore di r2.

[Point]    Definire una classe Point per oggetti immutabili che rappresentano punti del piano. Definire un costruttore che crea un Point con date coordinate e un costruttore senza argomenti che crea un Point nell'origine (0, 0). Definire inoltre dei getter per le coordinate, un metodo translate che ritorna il punto traslato di date quantità nelle direzioni x e y e un metodo scale che ritorna il punto scalato di un dato fattore in entrambe le coordinate. Ad esempio,

Point p = new Point(3, 4.5).translate(4, 1.5).scale(0.5);

dovrebbe porre in p un punto di coordinate (3.5, 3).

[Point2] Definire una versione mutabile di quella dell'esercizio precedente. In particolare i metodi translate e scale devono diventare dei mutators.

2 Mar 2015


  1. Per brevità usiamo un solo campo invece di campi separati per nome e cognome e non consideriamo per adesso altri dati come ad esempio la data di nascita.

  2. Inoltre, alcuni membri, campi o metodi, sono dichiarati privati perché dipendono dalla particolare implementazione e così possono essere modificati (ad esempio, possiamo cambiare il loro nome o il loro significato) senza che questo alteri l'interfaccia o il comportamento pubblico degli oggetti della classe.

  3. Come regola generale è meglio evitare il valore null perché può provocare problemi. Un caso molto comune si verifica quando si cerca di invocare un metodo su un valore che è null, in tal caso si produce un errore con lancio dell'eccezione NullPointerException. Ritorneremo sulla questione del null più avanti.