Metodologie di Programmazione: Lezione 5

Riccardo Silvestri

Classi astratte e interfacce

Il meccanismo di base dell'ereditarietà che abbiamo visto nella lezione precedente non consente di distinguere tra interfaccia e implementazione. Invece, in molti casi è conveniente ereditare solamente l'interfaccia o addirittura conviene definire solamente l'interfaccia per talune funzionalità. D'altronde una interfaccia, permettendo implementazioni variabili, è proprio ciò che serve per poter scrivere codice che dipende solamente dalle funzionalità fornite dagli oggetti e non dal particolare modo con cui sono realizzate o implementate. A sua volta questo riduce la replicazione del codice, migliora la leggibilità e le possibilità di riuso.

Come vedremo Java offre meccanismi più flessibili, più astratti e delicati dell'ereditarietà di base e concreta che abbiamo già visto. Questi sono ampiamente sfruttati nella piattaforma Java e come minimo bisogna conoscerli per poter usare al meglio le librerie della piattaforma. Padroneggiarli così da sfruttarli per migliorare l'organizzazione del codice richiede una maggiore conoscenza ed esperienza che avremo modo di acquisire nel proseguo del corso.

Classi astratte

Accade piuttosto spesso che la classe base di una gerarchia di classi non possa fornire l'implementazione di alcuni metodi perché non esiste alcuna implementazione significativa al livello della classe base. Però tali metodi devono comunque essere definiti nella classe base perché questo permette di trattare poi gli oggetti delle varie sotto-classi in modo uniforme sfruttando il polimorfismo. Si pensi a una gerarchia di classi per il disegno (su finestre grafiche) di varie figure geometriche (rettangoli, ellissi, poligoni, ecc.). Probabilmente conviene introdurre una classe base Shape come radice della gerarchia. Le sotto-classi rappresentano le specifiche figure geometriche (una classe per i rettangoli, una per le ellissi, ecc.). La classe Shape dovrebbe prevedere tra gli altri metodi sicuramente un metodo draw che disegna la figura geometrica. Questo sarà poi ridefinito in ogni sotto-classe. Essendo definito al livello della classe base Shape garantisce che può essere invocato in modo uniforme su qualsiasi oggetto facente parte della gerarchia, rappresentante una delle figure geometriche.

Ma quale implementazione dovrebbe dare la classe base al metodo draw? Siccome Shape non rappresenta nessuna figura geometrica specifica non può fornire alcuna implementazione significativa del metodo. Potrebbe semplicemente lasciare l'implementazione vuota: draw() {}. Questo può essere una soluzione accettabile se il metodo è come draw che non ritorna nessun valore. Ma se il metodo dovesse ritornare un valore? Quale valore ritorna? Inoltre, anche nel caso di metodi che come draw non ritornano valori, la soluzione dell'implementazione vuota non è pienamente soddisfacente perché tende a nascondere il fatto che la classe base, nel nostro esempio Shape, non è una classe concreta. Nel senso che gli oggetti di tale classe non possono essere usati direttamente. Gli oggetti della classe Shape non rappresentano alcuna figura specifica e quindi non possono in nessun modo essere usati direttamente, solamente gli oggetti delle sotto-classi possono essere usati direttamente. In altre parole, non ha senso istanziare oggetti della classe Shape.

Le situazioni come quella di Shape sono molto più comuni di quelle in cui la classe base della gerarchia è una classe concreta. Inoltre, quest'ultime sono gerarchie molto particolari perché rappresentano quasi sempre i dati di un qualche tipo di archivio (come quello dei dipendenti della nostra ipotetica azienda). Mentre le gerarchie in cui la classe base non è intesa per essere istanziata sono molto più comuni, come è anche mostrato dalla piattaforma di Java. Perfino nel caso dell'archivio dei dipendenti se questo viene esteso introducendo una classe Persona, come super-classe di Dipendente e di una nuova classe Consulente (un collaboratore esterno all'azienda), la nuova classe base Persona non dovrebbe essere una classe concreta. Perché non avrebbe senso istanziare un oggetto di tipo Persona in quanto non ha alcun significato di per sé ma lo acquista solamente nel contesto di uno specifico ruolo, dipendente o consulente.

Proprio allo scopo di fornire strumenti per risolvere in modo soddisfacente situazioni come quelle descritte, il linguaggio Java permette di definire classi astratte (abstract classes). Una classe astratta è come una classe normale (concreta) con però uno o più metodi senza implementazione. Un metodo senza implementazione è un metodo astratto (abstract method) per il quale è definita solamente l'intestazione (ovvero l'interfaccia). La sintassi per definire metodi astratti e classi astratte è molto semplice. È sufficiente usare il modificatore abstract e terminare l'intestazione dei metodi astratti con ; che sostituisce il corpo del metodo. Ecco un breve elenco delle caratteristiche principali di una classe astratta.

Oltre queste caratteristiche, una classe astratta è del tutto simile ad una classe concreta.

Come esempio consideriamo il caso di voler realizzare alcune semplici applicazioni che permettono all'utente di scegliere da un menu testuale (cioè, un elenco di possibili voci) una voce e di eseguire la corrispondente operazione. Ci sono delle funzionalità che sono comuni ad applicazioni di questo tipo, sicuramente la gestione del menu e della scelta effettuata dall'utente. Allora, conviene definire una classe astratta MenuApp (in un opportuno package che chiameremo mp.tapp) il cui scopo è di fornire lo scheletro per applicazioni (programmi) la cui interazione con l'utente è basata su un menu testuale. Quindi la classe MenuApp gestisce il menu testuale, le cui voci saranno inizializzate dalla sotto-classe, e invoca un metodo astratto, che sarà implementato dalla sotto-classe, in risposta alle scelte effettuate dall'utente. Ecco una possibile definizione.

package mp.tapp;

import java.util.*;
import static java.lang.System.out;

/** {@code MenuApp} fornisce le funzionalità di base per un'applicazione basata
 * su un menu testuale. La sotto-classe deve fornire i contenuti. */
public abstract class MenuApp {
    /** Esegue l'applicazione. L'utente sceglie una voce del menu digitando il
     * numero mostrato a sinistra della voce. */
    public void run() {
        int n = menu.length;
        Scanner input = new Scanner(System.in);
        boolean quit = false;
        while (!quit) {
            for (int i = 0 ; i < n ; i++)
                out.println((i+1)+". "+menu[i]);
            while (!input.hasNextInt())
                input.next();      // Scarta qualsiasi input che non è un intero
            int choice = input.nextInt();
            if (choice >= 1 && choice < n) doMenu(choice);
            else if (choice == n) quit = true;
        }
        out.println("Applicazione terminata");
    }

    /** Usato dalla sotto-classe per inizializzare le voci del menu, esclusa la
     * voce per terminare "Quit", che è gestita direttamente da questa classe.
     * @param items  le voci del menu */
    protected MenuApp(String...items) {
        menu = Arrays.copyOf(items, items.length + 1);
        menu[menu.length-1] = "Quit";
    }

    /** Implementato dalla sotto-classe per eseguire la voce del menu scelta.
     * @param choice  il numero della voce di menu scelta */
    protected abstract void doMenu(int choice);

    private String[] menu;
}

Le voci del menu sono fornite tramite il costruttore, la voce "Quit" è direttamente implementata dalla classe.

protected    Si noti che il costruttore e il metodo astratto doMenu sono definiti usando il modificatore d'accesso protected. Tale modificatore dichiara che l'accesso è ristretto al package (nel nostro caso mp.tapp) e alle sotto-classi, anche se appartenenti a package differenti. Quindi si tratta di una modalità di accesso più ampia di quella di default che limita l'accesso solamente all'interno del package. Qui il modificatore protected è usato perché non ha senso che classi al di fuori del package mp.tapp che non sono sotto-classi di MenuApp possano accedere al costruttore e al metodo doMenu. Mentre è necessario che l'accesso sia garantito alle sotto-classi anche se non appartengono al package mp.tapp. Infatti, in una realizzazione realistica la classe MenuApp sarà parte di una libreria di uso generale e quindi le sue classi apparteranno a opportuni package, mentre le sotto-classi clienti che forniscono le implementazioni relative ad applicazioni concrete apparteranno necessariamente a package differenti.

Come esempio di sotto-classe che implementa un'applicazione concreta consideriamo una semplicissima classe che realizza un'applicazione che permette di calcolare alcune funzioni matematiche scelte dall'utente tramite il menu.

package mp;

import mp.tapp.MenuApp;
import java.util.Scanner;
import static java.lang.System.out;

/** Una semplice applicazione con menu testuale per il calcolo di funzioni
 * matematiche. */
public class MathApp extends MenuApp {
    public MathApp() {
        super("Logaritmo", "Radice quadrata");
    }

    /** Esegue il calcolo relativo alla voce di menu scelta.
     * @param choice  il numero della voce di menu scelta */
    @Override
    protected void doMenu(int choice) {
        Scanner input = new Scanner(System.in);
        out.print("Digita un numero: ");
        while (!input.hasNextDouble())
            input.next();    // Scarta qualsiasi input che non è un numero
        double x = input.nextDouble();
        switch(choice) {
            case 1: out.println("log("+x+") = "+Math.log(x)); break;
            case 2: out.println("sqrt("+x+") = "+Math.sqrt(x)); break;
        }
    }

    public static void main(String[] args) {  // Mette alla prova l'applicazione
        MathApp app = new MathApp();
        app.run();
    }
}

Template design pattern

Dovrebbe essere ormai chiaro che un linguaggio di programmazione orientato agli oggetti come Java offre strumenti sofisticati per la progettazione del software. Ereditarietà, polimorfismo e classi astratte sono strumenti potenti che da una parte possono semplificare la struttura di un programma e dall'altra la rendono più sofisticata e delicata. Il loro buon uso non è scontato né facile. Proprio per suggerire un buon uso di questi strumenti sono stati individuati e studiati modelli emblematici. I migliori di questi modelli di progettazione sono stati sistematicamente raccolti e descritti così che possano essere a disposizione di un qualsiasi programmatore e sono comunemente chiamati design patterns.

Un esempio di design pattern, che è particolarmente adatto ad essere realizzato tramite classi astratte, è chiamato Template ed è atto a definire lo scheletro di un algoritmo (o procedura), che deve realizzare una o più operazioni, in cui l'implementazione di alcune parti è demandata alle sotto-classi. Così le sotto-classi implementano alcune parti dell'algoritmo lasciandone inalterata la struttura. Gli usi più comuni del design pattern Template si trovano in librerie che forniscono framework per l'implementazione di applicazioni. In questi casi una classe astratta (fornita dalla libreria) implementa alcune funzionalità generali e comuni a tutte le applicazioni (gestione degli eventi, menu, finestre di dialogo, ecc.) e lascia alle sotto-classi (che corrispondono ad applicazioni concrete) il compito di implementare le specifiche azioni da compiere in risposta agli eventi (movimenti del mouse, tasti premuti, ecc.) generati dall'utente. Un esempio molto semplice d'uso del design pattern Template è proprio quello che abbiamo appena visto realizzato dalla classe MenuApp.

Il design pattern Template si basa su una struttura di controllo in un certo senso invertita perché una super-classe (MenuApp) invoca le operazioni (doMenu) implementate in una sotto-classe (MathApp). Questa struttura di controllo è anche conosciuta con il nome pittoresco di "the Hollywood principle" cioè "Don't call us, we'll call you".

Interfacce

Quando una sotto-classe eredita un metodo della super-classe ne eredita l'interfaccia (cioè l'intestazione del metodo) e, se non lo ridefinisce, anche l'implementazione. Gli esempi relativi alle classi astratte mostrano che a volte l'ereditarietà dell'implementazione non è necessaria e anzi può diventare un inutile fardello. I metodi astratti permettono di venire incontro proprio a questa esigenza di avere solamente una ereditarietà di interfaccia. Questo non è casuale perché è proprio l'ereditarietà di interfaccia che abilita l'uso di una delle caratteristiche più utili dell'ereditarietà: poter scrivere codice che dipende solamente dalle funzionalità fornite dagli oggetti e non dal particolare modo con cui sono realizzate o implementate.

Il termine interfaccia è comunemente usato anche con un significato più stringente e però anche meno formale. Infatti, di solito per interfaccia di un metodo si intende l'intestazione del metodo insieme con la specifica del risultato che deve essere prodotto dall'invocazione del metodo. Ad esempio, l'interfaccia del metodo astratto void doMenu(int choice) (della classe MenuApp) non solo definisce un'intestazione, che sarà automaticamente ereditata dalle sotto-classi, ma specifica informalmente anche quale deve essere il risultato della sua invocazione (cioè, eseguire la voce choice del menu). Quindi un'interfaccia, nella sua accezione più stringente, consiste di una parte sintattica, le intestazioni dei metodi, e di una parte semantica, le specifiche dei risultati delle invocazioni dei metodi.

Purtroppo il termine interfaccia è usato in modo ambiguo. A volte lo si usa per intendere solamente la parte sintattica e altre volte per intendere entrambi le parti, quella sintattica insieme a quella semantica. Per questa ragione è stato introdotto un termine specifico per indicare sia la parte sintattica che quella semantica: il contratto. Una classe che implementa una interfaccia è come se aderisse a un contratto: si obbliga a rispettare le intestazioni dei metodi e per ognuno di essi si obbliga a produrre, a seguito di una invocazione, il risultato richiesto. Ovviamente, solamente la parte sintattica del contratto può essere gestita automaticamente. La parte semantica è una responsabilità del programmatore. Come vedremo il contratto non deve essere necessariamente una specifica precisa del comportamento di ogni metodo, ma potrebbe essere solamente una specifica parziale a volte anche molto lasca.

Il linguaggio Java offre un meccanismo più flessibile e affidabile per l'ereditarietà di interfaccia di quello offerto dalla ereditarietà tra classi che abbiamo già visto. È possibile definire un'interfaccia, tramite la parola chiave interface, in cui si definiscono solamente le intestazioni dei metodi. Una interface è quindi simile ad una classe astratta in cui tutti i metodi sono astratti o hanno un'implementazione di default e non ci sono costruttori. Però c'è una differenza fondamentale: le interface supportano l'ereditarietà multipla. Questo significa che una classe può implementare più di una interfaccia e una interfaccia può estendere più interfacce. Come vedremo questa maggiore flessibilità risulta molto utile e infatti è frequentemente usata nelle librerie di Java.

La definizione di una interfaccia è simile a quella di una classe con la parola chiave interface al posto della parola chiave class. Però ci sono delle regole speciali per i membri di una interfaccia. Ecco un elenco delle principali regole e caratteristiche delle interfacce.

I motivi che rendono conveniente l'introduzione di una interfaccia possono essere molteplici. Tra i più comuni c'è il desiderio di separare in modo netto l'interfaccia di un servizio o funzionalità dalla sua implementazione. Ad esempio, supponiamo che ci serva un gestore per insiemi di stringhe e per questo definiamo una classe concreta StrSet con una specifica implementazione. Il nostro codice userà quindi gli oggetti di tipo StrSet tramite i metodi pubblici forniti. Se poi in seguito ci accorgiamo che in certe situazioni sarebbe meglio per motivi di efficienza usare un gestore di insiemi di stringhe implementato da un'altra classe StrSetPlus, avremo sicuramente dei problemi a modificare il codice che usa StrSet dovendo sostituire in molti punti l'uso del tipo StrSet con StrSetPlus. Inoltre, dovremmo stare molto attenti che i metodi pubblici delle due classi siano effettivamente compatibili, stessa intestazione e stessa semantica. Se invece introduciamo un'opportuna interfaccia per insiemi di stringhe potremmo definire con cura le operazioni che ci servono tramite i metodi astratti dell'interfaccia e poi avremo la massima libertà nel decidere quale implementazione usare. Infatti, è sufficiente che le varie classi concrete implementino l'interfaccia. Mentre il codice che usa gli insiemi di stringhe sarà indipendente dalle classi concrete perché userà i metodi dell'interfaccia. Solamente al momento della creazione di un insieme di stringhe ci si dovrà preoccupare di quale implementazione usare tra quelle fornite dalle varie classi concrete.

Queste sono essenzialmente le ragioni che hanno portato alla strutturazione tramite interfacce del framework Collection della piattaforma Java. Vedremo questo importante framework in una imminente lezione.

Un altro motivo, anch'esso molto comune, che può rendere conveniente l'introduzione di una interfaccia è la definizione di un servizio o funzionalità di uso molto generale. In tali casi è spesso impossibile fornire delle implementazioni che vanno bene per tutte le possibili varietà del servizio. Però è possibile specificare cosa devono fare le operazioni, ovvero i metodi, che il servizio deve fornire. Come primo esempio considereremo proprio un caso di questo tipo.

In molte applicazioni che richiedono l'immissione di dati, sia tramite interfacce utente testuali che grafiche, c'è la necessità di dover effettuare controlli sui dati immessi. Spesso tali dati sono stringhe. Ad esempio, se l'utente deve immettere un numero di telefono vogliamo controllare che la stringa immessa contenga solamente cifre e spazi. Se deve immettere un codice fiscale, vogliamo controllare che la stringa abbia lunghezza 16 e che rispetti le regole di un codice fiscale. I controlli possibili sono tantissimi. Però una parte dell'applicazione deve sempre fare le stesse cose indipendentemente dalla natura del controllo specifico. Deve leggere la stringa immessa dall'utente, eseguire il controllo e se questo ha esito positivo fare qualcosa e altrimenti segnalare l'errore all'utente. Se ogni controllo è implementato ad-hoc senza nessun tentativo di uniformarne l'interfaccia, quella parte di codice che potrebbe essere scritta una sola volta ed essere usata in tutti i casi dovrà invece essere riscritta per ogni caso specifico. Ed è qui che le interfacce mostrano la loro utilità. Basterà introdurre un'opportuna interfaccia che definisce ciò che è necessario e sufficiente per usare un controllo (su una stringa). In realtà basta molto poco e l'interfaccia può essere definita così

package mp.app;

/** Un {@code Checker} effettua uno specifico controllo di validità su stringhe.
 * Ad esempio, potrebbe controllare che i caratteri di una stringa siano tutti
 * delle lettere, o che la lunghezza sia compresa in un certo range, o che
 * rispetti la sintassi di un indirizzo email, ecc. */
public interface Checker {
    /** Esegue un controllo di validità della stringa e se è valida ritorna
     * {@code null}, altrimenti ritorna una stringa che contiene una spiegazione
     * del perché non è valida.
     * @param s  la stringa da controllare
     * @return {@code null} oppure una spiegazione della non validità */
    String valid(String s);
}

Nei javadoc è descritto il contratto che ogni implementazione dell'interfaccia Checker dovrebbe rispettare. Chiaramente ogni controllo specifico sarà un oggetto di un'opportuna classe che implementa l'interfaccia Checker. Per vedere quest'interfaccia in azione, scriviamo una piccola applicazione per gestire l'archivio della nostra ipotetica azienda. Per l'interfaccia utente possiamo usare un semplice menu testuale gestito dalla classe MenuApp.

package mp;

import mp.app.Checker;
import mp.tapp.MenuApp;

import java.util.Arrays;
import java.util.Scanner;
import static java.lang.System.out;

/** Una semplice applicazione con menu testuale per gestire un archivio 
 * dipendenti */
public class AziendaApp extends MenuApp {
    public static void main(String[] args) {
        AziendaApp app = new AziendaApp();
        app.run();
    }

    public AziendaApp() {
        super("Nuovo...", "Cerca...", "Rimuovi...", "Tutti");
        dipendenti = new Dipendente[0];
    }

    @Override
    protected void doMenu(int choice) {
        switch (choice) {
            case 1: nuovo(); break;     // Aggiunge un dipendente
            case 2: cerca(); break;     // Cerca un dipendente
            case 3: rimuovi(); break;   // Rimuove un dipendente
            case 4: tutti(); break;     // Stampa tutti i dipendenti
        }
    }

    private Dipendente[] dipendenti;    // Mantiene l'archivio dei dipendenti
}

Quindi useremo un array per mantenere i dipendenti. Per adesso abbiamo solamente menzionato le quattro operazioni che intendiamo gestire avendole demandate ad altrettanti metodi (nuovo,cerca, ecc.). Ovviamente se ne potrebbero aggiungere delle altre.

Il metodo nuovo chiederà all'utente di immettere i dati (tramite console) del nuovo dipendente. Per semplicità ci limiteremo a nome e cognome, indirizzo e telefono. Per ognuno di questi vogliamo effettuare un controllo. I caratteri di nome e cognome possono essere solamente lo spazio, l'apice e le lettere e non può essere la stringa vuota; per l'indirizzo oltre a questi si possono usare il punto, la virgola e le cifre 0 - 9; per il telefono solamente lo spazio e le cifre. Sono quindi tre controlli diversi che dobbiamo implementare con altrettante classi che implementano l'interfaccia Checker. Possiamo usare classi annidate (statiche) e private

private static class CheckNomeCognome implements Checker {
    @Override
    public String valid(String s) {
        String r = checkString(s, " '", true);
        return (r != null ? r : (s.isEmpty() ? "Non può essere vuoto" : null));
    }
}

private static class CheckIndirizzo implements Checker {
    @Override
    public String valid(String s) {
        return checkString(s, " ',.0123456789", true);
    }
}

private static class CheckTelefono implements Checker {
    @Override
    public String valid(String s) {
        return checkString(s, " 0123456789", false);
    }
}

Siccome i tre controlli sono diversi ma piuttosto simili, abbiamo definito il metodo checkString che ci permette di scriverli minimizzando la duplicazione di codice:

/** Controlla che la stringa data non sia {@code null} e che i suoi caratteri
 * siano in {@code chars} o, se {@code letters} è {@code true}, che siano
 * lettere.
 * @param s  la stringa da controllare
 * @param chars  caratteri permessi
 * @param letters  se {@code true}, sono permesse anche le lettere
 * @return  null se la stringa è valida, altrimenti una stringa con la
 * spiegazione dell'errore */
private static String checkString(String s, String chars, boolean letters) {
    if (s == null) return "Non può essere null";
    for (char c : s.toCharArray())
        if (chars.indexOf(c) < 0 && (!letters || !Character.isLetter(c)))
            return "Il carattere '"+c+"' non è valido";
    return null;
}

Adesso possiamo creare gli oggetti che eseguiranno i controlli e che possono essere creati una volta per tutte e per questo li creiamo all'inizio e li mettiamo a disposizione della classe in campi statici finali:

private static final Checker CHECK_NOMECOGNOME = new CheckNomeCognome();
private static final Checker CHECK_INDIRIZZO = new CheckIndirizzo();
private static final Checker CHECK_TELEFONO = new CheckTelefono();

Si osservi che tutti e tre i campi sono di tipo Checker e non del tipo attuale degli oggetti creati. Questo perché solamente così potremo scrivere codice che tratta i controllori in modo uniforme, cioè, indipendentemente dal loro tipo specifico. Possiamo quindi scrivere un metodo che legge dalla console una stringa, esegue il controllo e agisce di conseguenza:

/** Legge dallo {@link java.util.Scanner} {@code input} una stringa finché non
 * passa il controllo del {@link mp.app.Checker} {@code check}.
 * @param input  scanner da cui leggere
 * @param prompt  la descrizione della stringa da leggere
 * @param check  il controllo
 * @return la prima stringa letta che passa il controllo */
private static String inputString(Scanner input, String prompt, Checker check) {
    while (true) {
        out.print(prompt);
        String s = input.nextLine();
        String r = check.valid(s);
        if (r != null)
            out.println("ERRORE: "+r);
        else
            return s;
    }
}

Adesso possiamo definire il metodo (privato) della classe AziendaApp che aggiunge un nuovo dipendente all'archivio:

/** Legge dalla console i dati di un nuovo dipendente e lo aggiunge
 * all'archivio. */
private void nuovo() {
    Scanner input = new Scanner(System.in);
    String nc = inputString(input, "Nome e cognome: ", CHECK_NOMECOGNOME);
    String ind = inputString(input, "Indirizzo: ", CHECK_INDIRIZZO);
    String tel = inputString(input, "Telefono: ", CHECK_TELEFONO);
    Dipendente d = new Dipendente(nc);
    d.setIndirizzo(ind);
    d.setTelefono(tel);
    dipendenti = Arrays.copyOf(dipendenti, dipendenti.length+1);
    dipendenti[dipendenti.length-1] = d;
    out.println("Il dipendente "+nc+" è stato inserito");
}

Come si vede abbiamo potuto usare il metodo inputString per tutti e tre i dati, grazie all'interfaccia Checker.

Terminiamo la nostra piccola applicazione scrivendo uno dei tre metodi rimasti, lasciando gli altri due per esercizio:

/** Legge dalla console una stringa e stampa i dependenti che contengono nel
 * loro nome e cognome la stringa letta. */
private void cerca() {
    out.println("Non ancora implementato");    // Lasciato come esercizio
}

/** Legge dalla console un codice e rimuove dall'archivio il dipendente con
 * quel codice, se esiste. */
private void rimuovi() {
    out.println("Non ancora implementato");    // Lasciato come esercizio
}

/** Stampa sulla console i dati di tutti i dipendenti nell'archivio */
private void tutti() {
    for (Dipendente d : dipendenti) {
        out.println(d.getCodice()+"  "+d.getNomeCognome());
        Dipendente.Contatti c = d.getContatti();
        out.println("  Indirizzo: "+c.getIndirizzo()+" Tel: "+c.getTelefono());
    }
}

Metodi di default

Come è già stato menzionato nella descrizione generale, un'interfaccia può anche fornire metodi con un'implementazione di default. Questi possono anche non essere ridefiniti dalle classi che implementano l'interfaccia e in tal caso li ereditano insieme alla loro implementazione di default. Siccome le interfacce non possono avere campi degli oggetti, l'implementazione di un metodo di default non potrà accedere, almeno direttamente, ad alcun campo dell'oggetto sul quale è invocato. Però può invocare uno qualsiasi dei metodi definiti nell'interfaccia e se questo è implementato o ridefinito dalla classe concreta, allora anche il metodo di default può fare qualcosa che dipende, anche se indirettamente, dallo stato dell'oggetto. Nel caso della nostra interfaccia Checker potremmo introdurre un metodo di default che risulta conveniente quando la stringa che descrive l'eventuale errore non interessa:

/** Come {@link Checker#valid(String)} ma ritorna un booleano.
 * @param s  la stringa da controllare
 * @return {@code true} se la stringa è valida */
default boolean isValid(String s) {
    return valid(s) == null;
}

Si noti che tutto il codice che abbiamo già scritto con la vecchia definizione di Checker rimane corretto anche con la nuova.

Metodi statici e altro

Le interfacce possono aver anche metodi statici. Uno degli usi più comuni sono i cosiddetti factory methods, cioè metodi che costruiscono o fabbricano oggetti. Ad esempio, nella nostra interfaccia Checker potremmo definire dei factory methods che ritornano oggetti Checker che realizzano dei controlli comuni, ad esempio, sulla lunghezza o sui caratteri permessi. Oppure potremmo definire metodi statici che ritornano la combinazione di due o più Checker, ad esempio ritornando un oggetto che esegue la congiunzione di questi. Però non abbiamo ancora gli strumenti che ci consentono di definire al meglio questi factory methods per cui li tratteremo più avanti. Così come altri tipi di membri di un'interfaccia, cioè interfacce e classi annidate, e l'ereditarietà multipla.

Ciò che abbiamo detto finora sulle classi astratte e le interfacce rappresenta solamente la base della loro conoscenza. Nel proseguo del corso avremo modo di approfondire i loro usi attraverso molti esempi che svilupperanno un'esperienza sufficiente per poter sfruttare al meglio questi strumenti.

Esercizi

[MathApp]    Aggiungere altre operazioni a MathApp. Ad esempio, le funzioni seno e coseno e la funzione potenza che legge due numeri x e y e calcola x elevato alla y.

[Menu]    Modificare la classe MenuApp in modo tale che permetta di visualizzare il menu con una cornice come nell'esempio qui sotto:

************************
*  1. Logaritmo        *
*  2. Radice quadrata  *
*  3. Quit             *
************************

Però la sotto-classe deve poter decidere se usufruire o meno di questa opzione.

[AziendaApp]    Implementare i metodi cerca e rimuovi in AziendaApp.

[AziendaApp+]    Aggiungere nel metodo nuovo l'immissione dello stipendio con un opportuno controllo (Checker) sulla stringa digitata dell'utente.

[IntSeq]    Definire un'interfaccia IntSeq per sequenze di interi con due metodi astratti: hasNext che ritorna true se c'è un prossimo intero della sequenza e next che ritorna il prossimo intero della sequenza, se non c'è lancia NoSuchElementException. Definire le seguenti classi che implementano l'interfaccia IntSeq.

Definire infine un metodo statico (ad es. in una classe di test) average che prende in input una sequenza IntSeq e un intero n e ritorna la media dei primi n interi della sequenza (se la sequenza ha meno di n interi, la media è di tutti gli interi). Provare il metodo con gli oggetti delle classi definite.

[IntSeqRewind]    Definire un'interfaccia IntSeqR che estende l'interfaccia IntSeq dell'esercizio precedente con un ulteriore metodo rewind che riporta la sequenza all'inizio, quindi dopo l'invocazione di tale metodo la sequenza può essere riletta dall'inizio. Definire le versioni SquaresR, MinDivsR, IntsR, IntReads delle classi dell'esercizio precedente, che implementano l'interfaccia IntSeqR.

[AbsIntSeqR]    Definire una classe astratta AbsIntSeqR per facilitare l'implementazione dell'interfaccia IntSeqR, dell'esercizio precedente. La classe deve implementare tutti i metodi di IntSeqR e introdurre due metodi astratti e protected: _hasNext e _next con lo stesso significato dei metodi hasNext e next dell'interfaccia IntSeq. Così una classe concreta che vuole implementare IntSeqR può estendere AbsIntSeqR e deve solamente preoccuparsi di implementare una sequenza semplice, cioè senza rewind, tramite i metodi _hasNext e _next. Fornire quindi anche una implementazione delle classi SquaresR, MinDivsR, IntsR, IntReads estendendo la classe astratta AbsIntSeqR. Si osservino le differenze con le implementazioni date nell'esercizio precedente.

[List]    Definire una interfaccia List per rappresentare liste di oggetti qualsiasi, cioè di tipo Object, con i seguenti metodi astratti.

In tutti i casi, se gli indici non sono corretti i metodi lanciano IndexOutOfBoundsException.

Definire una classe ArrayList che implementa l'interfaccia List tramite un array. Definire inoltre un'altra implementazione con una classe LinkedList tramite liste linkate. Ciò può essere fatto definendo una classe annidata (statica) Elem di LinkedList la quale mantiene un oggetto della lista e un riferimento al prossimo Elem della lista o null se non c'è. Quindi, la lista è realizzata tramite una catena di oggetti Elem, uno per ogni oggetto della lista, e dal primo di questi è possibile accedere a tutti gli altri seguendo ogni volta il riferimento al prossimo. Ad esempio, la lista "abc", 13, true, null, 2.55, sarebbe realizzata così

[PyList]    Definire un'interfaccia PyList che estende l'interfaccia List con i seguenti metodi astratti.

Definire classi ArrayPyList e LinkedPyList che implementano l'interfaccia PyList analoghe alle classi ArrayList e LinkedList del precedente esercizio.

[AbsPyList]    Definire una classe astratta AbsPyList per facilitare l'implementazione dell'interfaccia PyList, dell'esercizio precedente. La classe deve implementare i metodi aggiuntivi di PyList e lasciare astratti i metodi di List, cioè len, get, sub, add, set e del. Così una classe concreta che vuole implementare PyList può estendere AbsPyList e deve solamente preoccuparsi di implementare una lista semplice, cioè senza i metodi aggiuntivi di PyList. Fornire quindi un'implementazione delle classi ArrayPyList e LinkedPyList estendendo la classe astratta AbsPyList. Si osservino le differenze con le implementazioni date nell'esercizio precedente.

Invece di definire la classe astratta AbsPyList non si può ottenere un risultato equivalente aggiungendo all'interfaccia PyList un'implementazione di default dei suoi metodi?

[ConfrontoListe]    Mettere alla prova le implementazioni delle interfacce List o PyList confrontando i tempi di esecuzioni su liste piccole e grandi e rispetto alle operazioni che si possono compiere tramite i vari metodi. Per misurare i tempi d'esecuzione si può usare il metodo System.nanoTime. Il confronto può anche essere effettuato per testare la correttezza delle implementazioni che, se sono corrette, devono comportarsi nello stesso modo.

9 Mar 2015