< Home
Stampa

Generics

Sommario

La programmazione generica (in inglese “generics”) è una tecnica di programmazione che permette di definire a compile time delle classi contenitore (tipicamente collezioni, come dizionari o array) e delle classi contenuto. Questo consente di sfruttare il polimorfismo per gestire il comportamento del contenitore in modo indipendente dal contenuto.

Ma partiamo dall’inizio.

Tipi “contenitore”

Poniamo di avere le seguenti quattro classi.

class Acqua {
    int quantita;

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

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


class Latte {
    int quantita;

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

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

class BicchiereAcqua {
    private Acqua acqua;

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

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

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 BicchiereLatte e BicchiereAcqua è praticamente identico, ma siccome sono strettamente accoppiate al proprio contenuto. Sarebbe quindi molto utile avere una classe Bicchiere che è indipendente dal suo contenuto. In questo senso vengono in aiuto i Generics.

Generics

I Generics sono classi contenitore che anziché essere associate ad un contenuto specifico, ne definiscono uno generico, che poi verrà sostituito in fase di creazione dell’oggetto con il tipo effettivo. Vediamo come funziona con la classe Bicchiere:

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.)
  • analoga definizione generica di tipo si usa anche all’interno della classe.

Quando poi si utilizza la classe e si crea l’oggetto si va ad indicare esplicitamente il tipo specifico usato.

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

Jolly

Possiamo creare anche oggetti generici, usando l’operatore ?, operatore jolly.

Questo consente di dichiarare variabili contenitore generiche, senza indicare il tipo di dato contenuto.Il tipo contenuto sarà poi definito nell’assegnazione.

Bicchiere<?> bicchiere;
bicchiere = new Bicchiere<Latte>(latte);

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

Un Generics potrebbe essere “troppo generico”, ovvero accettare formalmente classi contenuto di qualsiasi tipo. Sarebbe invece utile un controllo in compilazione che le classi contenuto appartengano ad una specifica gerarchia di classi (ad esempio indicando una classe astratta) o meglio ancora delle interfacce (restando quindi indipendenti dalla gerarchia di classi).
Vediamo entrambi i casi.
Latte ed Acqua potrebbero essere generalizzati nella classe Liquido:

class Liquido {
    public int quantita;
    public String nome;

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

    public getQuantita() { return quantita; }
    public getNome() { return nome; }
}

class Acqua extends Liquido{

    public Acqua(int quantita) {
        super(quantita, "acqua");
    }
}

class Latte extends Liquido{

    public Latte(int quantita) {
        super(quantita, "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 con " + this.liquido.getQuantita() + "ml di " + this.liquido.getNome();
    }
}

Questo sistema introduce un controllo a compile time che impedisce di avere un T troppo generico.1

Possiamo invece progettare una interfaccia e quindi gerarchie di classi indipendenti che però sfruttano la stessa interfaccia.

interface Contenuto {
   String getNome();
   String getQuantita();
}

class Liquido implements Contenuto {
... vedi sopra
}
class Latte...
class Acqua..

class Sale implements Contenuto {
...
}

E poi ridefinire il nostro contenitore per gestire questa interfaccia:

class Bicchiere<T implements Contenuto> {
    private T contenuto;

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

    public String toString() {
        return "Bicchiere con " + this.contenuto.getQuantita() + "ml di " + this.contenuto.getNome();
    }
}

E’ possibile creare contenitori Generics con contenuti multipli. Un classico esempio è questo:

class Pair<T,U> {
  private T first;
  private U second;

  public Pair(T first, U second) {
    this.first = first;
    this.second = second;
  }

 public T getFirst() { return first; }
 public U getSecond() { return second; }
}

const coppia = new Pair<Latte, Acqua>(new Latte(100), new Acqua(150));

Questa semplice classe contenitore permette di associare due oggetti diversi sotto lo stesso contenitore. Valgono ovviamente le stesse regole di cui sopra, quindi vincoli e jolly.

In conclusione:

  • i Generics sono tipi di classi speciali che svolgono il ruolo di contenitore di oggetti di tipo non conosciuto a compile time;
  • permettono quindi di avere la flessibilità di associare contenitore e contenuto a runtime (quindi in base ai dati e le scelte dell’utente);
  • è possibile però fissare vincoli di ereditarità e di implementazione al tipo di contenuto, garantendo controlli a build time.

Generics nella libreria Java

Con l’introduzione dei Generics è possibile ora usare una delle librerie più importanti di Java, ovvero il Collection Framework. Esso consente l’utilizzo di classi contenitore che contengono insiemi di dati. Le principali sono:

  • List<T>: sono le classiche liste ordinate di elementi dello stesso tipo.
  • Set<T>: rappresenta insiemi di dati non ordinati ma dove ogni elemento è presente una sola volta.
  • Map<K,V>: sono i classici dizionari, cioè coppie di chiavi di tipo T (ad esempio stringhe) associate ad oggetti di tipo U. I dizionari sono

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> (ma ce ne sono anche altre).

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

Set<T> e HashSet<T>

L’interfaccia Set<T> è un contenitore di liste di oggetti dello stesso tipo T. Si differenzia dalla lista perché non è ordinata e per il principio di unicità. In altri termini non possono esserci due oggetti che A e B dove A.equals(B) == true.

MetodoDescrizione
add(T value)aggiunge un elemento all’insieme.
toArray()restituisce un array con tutti gli elementi
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)
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 è HashSet<T>.

L’uso di insiemi è fondamentale per garantire unicità in numerose situazioni reali dove bisogna evitare dati duplicati, inserimenti doppi, quando si devono gestire chiavi di dizionari, ed in generale per evitare ridondanza.

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.

  1. 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. ↩︎