Sommario
< Home
Stampa

Generics

La programmazione generica (in inglese “generics”) è una tecnica di programmazione che permtte di sfruttare il

Tipi “contenitore”

Per tipo contenitore si intende una classe che utilizza istanze di oggetti di un’altra classe (o sue sottoclassi). Le classi contenitore hanno un insieme di proprietà e metodi propri, che possono utilizzare, grazie al polimorfismo, proprietà e metodi della classe utilizzata.

Facciamo un esempio:

class Acqua {
    int quantita;

    public Acqua(int quantita) {
        this.quantita = quantita;
    }

    public String toString() {
        return this.quantita + " di acqua";
    }
}

class BicchiereAcqua {
    private Acqua acqua;

    public BicchiereAcqua(Acqua acqua) {
        this.acqua = acqua;
    }

    public String toString() {
        return "Bicchiere di " + this.acqua.toString();
    }
}

Fin qui è tutto molto semplice. Poniamo però di voler creare, in modo analogo, due classi Latte e BicchiereLatte.

class Latte {
    int quantita;

    public Latte(int quantita) {
        this.quantita = quantita;
    }

    public String toString() {
        return this.quantita + " di latte";
    }
}

class BicchiereLatte {
    private Latte latte;

    public BicchiereLatte(Latte latte) {
        this.latte = latte;
    }

    public String toString() {
        return "Bicchiere di " + this.latte.toString();
    }
}

Come si può vedere il codice delle due classi BicchiereX è del tutto ridondante, sarebbe utile un meccanismo perché bicchiere possa contenere liquidi differenti senza dover scrivere un contenitore per ogni tipo di Liquido.

Generics

In questo senso Java viene in aiuto col concetto di Generics: classi contenitore che anziché definire una classe contenuto specifica (acqua o latte per esempio) ne definiscono una generica. Vediamo come viene creato quindi un bicchiere generico:

class Bicchiere<T> {
    private T liquido;

    public Bicchiere(T liquido) {
        this.liquido = liquido;
    }

    public String toString() {
        return "Bicchiere di " + this.liquido.toString();
    }
}

Osserviamo il codice:

  • per definire una classe generica si usano le parentesi angolari e si indica un nome generico di tipo (convenzionalmente si usa una lettera sola maiuscola, di solito T, U, K, ecc.)
  • il tipo generico viene poi indicato dentro la classe dove viene utilizzato.

Quando poi si utilizza la classe si indica esplicitamente il tipo:

Acqua acqua = new Acqua(200);
Latte latte = new Latte(100);
Bicchiere<Latte> bicchiere1 = new Bicchiere<Latte>(latte);
Bicchiere<Acqua> bicchiere2 = new Bicchiere<Acqua>(acqua);

Jolly

Possiamo essere ancora più generici e non indicare, in fase di dichiarazione di una variabile, quale sarà il tipo di dato contenuto:

Bicchiere<?> bicchiere1 = new Bicchiere<Latte>(latte);
Bicchiere<?> bicchiere2 = new Bicchiere<Acqua>(acqua);

Il carattere ? è indicato come jolly. Questo ci consente a runtime di avere il tipo di dati corretto solo in fase di assegnazione, per esempio in seguito ad una elaborazione di un algoritmo di scelta.

Vincoli

Tuttavia un tipo contenitore non sempre deve essere troppo generico. Ad esempio la dipendenza potrebbe essere solo per classi di una specifica gerarchia. Ad esempio Latte ed Acqua potrebbero essere generalizzati nella classe Liquido:

class Liquido {
    int quantita;
    protected String tipo;

    public Liquido(int quantita) {
        this.quantita = quantita;
    }

    public String toString() {
        return this.quantita + " di " + this.tipo;
    }
}

class Acqua extends Liquido{

    public Acqua(int quantita) {
        super(quantita);
        this.tipo = "acqua";
    }
}

class Latte extends Liquido{

    public Latte(int quantita) {
        super(quantita);
        this.tipo = "latte";
    }
}

Si può quindi mettere un vincolo a Bicchiere semplicemente dicendo che T deve estendere Liquido:

class Bicchiere<T extends Liquido> {
    private T liquido;

    public Bicchiere(T liquido) {
        this.liquido = liquido;
    }

    public String toString() {
        return "Bicchiere di " + this.liquido.toString();
    }
}

In questo caso si potranno istanziare bicchieri solo con oggetti di classi che derivano da Liquido.

Nota: questo esempio è utile per capire come funzionano i Generics, si ricorda tuttavia che in questo caso non serviva creare delle sottoclassi, sarebbe bastato fare due istanze diverse della classe Liquido, indicando esplicitamente il tipo di liquido. Bisogna creare sottoclassi solo quando effettivamente hanno comportamenti diversi e/o aggiunti dalla classe base.

E’ possibile anche imporre un vincolo implements, ed è pure possibile porre entrambi i vincoli:

class MyClass<T extends A, implements I>

E’ possibile creare contenitori Generics che si basano anche su più classi. In questo caso la notazione è questa:

class Contenitore1<T, U> ... 
class Contenitore2<K, V, C>

Generics nella libreria Java

Java fornisce nella sua libreria un insieme di contenitori di largo utilizzo. Vediamo in particolare la gestione delle liste e dei dizionari.

List<T> e ArrayList<T>

L’interfaccia List<T> è un contenitore di liste di oggetti di qualsiasi tipo. I principali metodi sono:

MetodoDescrizione
add(T value)aggiunge un elemento in coda
T get(int index)legge l’elemento alla posizione index
T remove(int index)legge e rimuove l’elemento alla posizione index
T set(int index, T value)sostituisce l’elemento alla posizione index (se non esistente lo inserisce)
int indexOf(T value)restituisce la prima posizione dell’elemento value (-1 se non esistente)
boolean contains(T value)verifica se value è presente
int size()restituisce la dimensione della lista
boolean isEmpty()verifica se la lista è vuota

La più importante classe di questa interfaccia è ArrayList<T>.

ArrayList<T> è generalmente usata al posto dell’array perchè offre molti vantaggi perché a differenza dell’array che ha dimensione fissa, l’ArrayList è dinamica, può quindi crescere e diminuire a piacimento. Inoltre ha già integrati un insieme di metodi che consentono un accesso facilitato agli elementi della lista

Map<K, V> e HashMap<K, V>

L’interfaccia Map<K, V> è un contenitore di tipo dizionario. Un dizionario in informatica è un contenitore di elementi dove ogni elemento è una coppia di dati: la chiave e il valore. La chiave serve per identificare il valore associato. Ad esempio il vocabolario è un dizionario, dove la chiave è la parola cercata, e il valore la sua definizione.

In termini computazionali, il dizionario (se supportato da un algoritmo opportuno) consente di ricercare elementi più velocemente che in una lista. Le Hashmap per esempio utilizzano l’algoritmo di hash, che è possibile approfondire nell’apposita lezione. Le Hashmap più diffuse in Java usano come chiave il tipo String.

Vediamo i principali metodi:

MetodoDescrizione
V put(K key, V value)Inserisce una nuova coppia
V get(K key)Restituisce value data key
V remove(K key)Rimuove il valore con chiave key
boolean containsKey(K key)Verifica se esiste un valore per key
boolean containsValue(V value)Verifica se esiste un valore V
int size()restituisce la dimensione della lista
boolean isEmpty()verifica se la lista è vuota

Esempio Conti correnti con Generics

Se usiamo i Generics possiamo vedere che:

  • possiamo creare una interfaccia generica IConto<T>;
  • possiamo creare una classe che la implementa generica ContoBancario<T extends Persona>
  • non abbiamo più bisogno di creare la classe ContoDeposito, è infatti un ContoBancario<Studente>
  • la classe ContoCorrente<T extends Lavoratore> va creata perché invece offre funzioni in più;
  • possiamo usare ArrayList<IConto<?>> anziché un array.

Vediamo le nuove classi:

interface IConto<T> {
    void setNumeroConto(int numeroConto);

    void versa(double quantita);

    void preleva(double quantita);

    String getInfo();

    T getIntestatario();
}

class ContoBancario<T extends Persona> implements IConto<T> {
    protected int numeroConto;
    protected double saldo;
    protected T intestatario;

    public ContoBancario(T intestatario) {        
        this.intestatario = intestatario;
        this.saldo = 0;
    }

    public void setNumeroConto(int numeroConto) {
        this.numeroConto = numeroConto;
    }

    public void versa(double quantita) {
        this.saldo += quantita;
    }

    public void preleva(double quantita) {
        this.saldo -= quantita;
    }

    public double getSaldo() {
        return this.saldo;
    }

    public T getIntestatario() {
        return this.intestatario;
    }

    public String getInfo() {
        String result = "Intestatario: " + this.intestatario.getTipo() + " - Numero Conto: " + this.numeroConto + "\n";
        result += "Nome: " + this.intestatario.getNominativo() + "\n";
        result += "Saldo: " + this.saldo;
        return result;
    }
}

class ContoCorrente<T extends Lavoratore> extends ContoBancario<T> {

    public ContoCorrente(T intestatario) {
        super(intestatario);
    }

    public void bonifico(Banca banca, double quantita, int numeroContoDestinatario) {
        this.preleva(quantita);
        banca.bonifico(this.numeroConto, numeroContoDestinatario, quantita);
    }
}

Il main è lasciato per esercizio.

Conclusioni

I Generics sono uno strumento per creare classi contenitore generiche, che possono quindi essere utilizzate per tipi differenti di classi. E’ possibile anche vincolare la classe contenuta ad una gerarchia di classi e/o a interfacce specifiche. Si possono dichiarare variabili generiche con Jolly ?, per decidere a runtime quale istanza specifica dovrà essere assegnata.

Java fornisce diverse classi generiche di libreria, in particolare ArrayList<T> (che sostituiscono gli array) e HashMap<T> che si utilizzano come dizionari.