Vai al contenuto

Java/Generics

Wikibooks, manuali e libri di testo liberi.

Java

Linguaggio Java
Linguaggio Java

categoria · sviluppo · modifica
Come posso contribuire?

→ Vai su Wikiversity

Java:  Guida alla programmazioneJava/Introduzione - Specifica di linguaggio - Libreria standardJava/Comunicazione seriale

Tipi di datoJava/Tipi di datoEspressioniJava/OperatoriVariabiliJava/Variabili localiIstruzioniJava/IstruzioniStrutture di controlloJava/Strutture di controlloPackageJava/Package
Classi e oggettiJava/Classi e oggettiOggettiJava/OggettiMetodiJava/MetodiMetodi (cont.)Java/Metodi/2ArrayJava/Array
Modificatori di accessoJava/Modificatori di accessoEreditarietàJava/Ereditarietà
Gestione delle eccezioniJava/Gestione delle eccezioniGenericsJava/Generics
Thread e parallelismoJava/MultithreadingInterblocco ricontrollatoJava/Interblocco ricontrollato
Alcune differenze con il CJava/Concetti fondamentaliLessicoJava/LessicoGrammaticaJava/Grammatica

Type variables

[modifica | modifica sorgente]

Può risultare molto utile (in alcuni casi necessario) che una stessa entità di programma sia scritta per lavorare con tipi distinti e non legati tra loro da vincoli di ereditarietà. L'esempio classico è quello di una classe che implementa una collezione, e il cui codice deve poter lavorare nello stesso identico modo con tipi completamente diversi e che non sono noti a priori.

Un modo semplice per ottenere ciò è scrivere del codice che lavori con la classe java.lang.Object, dato che essa è a capo della gerarchia di tutti i tipi riferimento. Lo svantaggio principale è che i client devono servirsi di un cast ogni volta, e ciò non solo allunga il codice, ma aumenta il rischio di bachi (ClassCastExceptions non desiderate) nel caso di errori di battitura nella scrittura dell'operatore di cast.

Il linguaggio fornisce una alternativa, che consiste nell'uso di una type variable, ovvero un identificatore che rappresenta un tipo che non è noto a priori. Le type variables sono definite nel capitolo 4 della specifica di linguaggio.

L'idea di fondo è che il codice generico definisca nell'interfaccia uno o più type parameters, i quali assumono il ruolo di type variables nell'implementazione, comportandosi esattamente come tipi riferimento noti. Il client passa come parametro i tipi esatti da utilizzare.

Una type variable può essere definita a livello di classe, di interfaccia, di metodo o di costruttore.

Tipi generici

[modifica | modifica sorgente]

È possibile parametrizzare un tipo riferimento con degli altri tipi riferimento. Ad esempio, l'interfaccia java.util.Collection rappresenta un tipo collezione, ma questo nome è incompleto: collezione di cosa? Si può aver bisogno di una collezione di stringhe, una collezione di componenti grafici, una collezione di collezioni di numeri telefonici, e così via. Il parametro che completa la dicitura collezione di ... svolge il ruolo di type parameter.

Un tipo che dichiara uno o più type parameters è detto tipo generico. Ad esempio:

/**
 * @param E Il tipo degli elementi contenuti nella collezione.
 */
public interface Collezione<E> {
    public void aggiungi(E e);
    public void rimuovi(E e);
    
    ... // altri metodi
}

In questo caso, Collezione è un tipo generico, ed E è il nome dell'unico type parameter. La scrittura Collezione è chiamata raw type; la scrittura Collezione<java.lang.String> identifica una collezione di stringhe e si chiama parametrized type.

Istanziazione

[modifica | modifica sorgente]

Al momento di istanziare una classe concreta generica, è necessario indicare il tipo effettivo da sostituire al parametro. Si può istanziare secondo queste due modalità:

  • lasciando che il compilatore attribuisca il tipo in automatico, oppure
  • esplicitando il tipo.

Il primo caso consiste nell'indicare una coppia di parentesi angolate vuota (chiamata diamond operator):

ArrayList<String> listaDiStringhe = new ArrayList<>();

Il secondo caso consiste nell'indicare i tipi effettivi tra parentesi angolate; può essere usato per rendere il codice più chiaro, ed è richiesto dal compilatore in caso di ambiguità:

new ArrayList<String>();

Uso dei raw types

[modifica | modifica sorgente]

Prima di Java 5, i generics non esistevano. Da Java 5 in poi, l'uso di una classe generica come raw type è fortemente sconsigliato, tuttavia è consentito come soluzione temporanea che consente di compilare ancora il codice scritto prima dell'introduzione dei generics.[F 1]

Metodi generici

[modifica | modifica sorgente]

Anche i metodi e i costruttori possono definire uno o più type parameters.

Ad esempio, si consideri un metodo che estrae un elemento a caso da un array il cui component type non è noto a priori. Prima dell'introduzione dei generics, il metodo doveva accettare un parametro Object[] e avere tipo di ritorno Object. Con i generics, si può definire una type variable E ed usarla sia come component type dell'array, sia come tipo di ritorno del metodo.

public class Utilities {
    public <E> E unElementoACaso(E[] elementi) {
        int indice = ...; // genera l'indice casualmente, in base al numero degli elementi dell'array
        return elementi[indice];
    }
    
    // ... altri metodi
}

In questo esempio, il metodo

    public E unElementoACaso(E[] array)

è stato scritto in base al type parameter E. Una sintassi analoga vale per i costruttori.

I client che invocano il metodo indicano il tipo effettivo da sostituire al parametro. Possono invocare il metodo secondo queste due modalità:

  • lasciando che il compilatore attribuisca il tipo in automatico, oppure
  • esplicitando il tipo.

Il primo caso è quello utilizzato più di frequente, e consiste nell'usare la stessa sintassi che invoca un metodo non generico. Ad esempio:

    public void stampaRecordACaso(Record[] records) {
        Utilities utilities = new Utilities();
        Record r = utilities.unElementoACaso(records);
        stampaRecord(r);
    }

Il secondo caso può essere usato per rendere il codice più chiaro, e a volte è richiesto dal compilatore in caso di ambiguità.

    public void stampaRecordACaso(Record[] records) {
        Utilities utilities = new Utilities();
        Record r = utilities.<Record> unElementoACaso(records);
        stampaRecord(r);
    }

Convenzioni di nomenclatura

[modifica | modifica sorgente]

I nomi dei type parameters dovrebbero:

  • essere veramente brevi (un solo carattere, se possibile)
  • indicare simbolicamente il ruolo svolto dal type parameter.

Queste convenzioni permettono di distinguere a colpo d'occhio una type variable da un tipo noto.[A 1]

Utilizzo delle type variables

[modifica | modifica sorgente]

Una type variable può essere usata in ogni punto in cui la sintassi ammette il nome di un tipo riferimento, tranne che:

  • ...

Un parametrized type può essere usato in ogni punto in cui la sintassi ammette il nome di un tipo riferimento, tranne che:

  • ...

Aspetti avanzati

[modifica | modifica sorgente]

Clausole extends e super

[modifica | modifica sorgente]

È possibile delimitare il tipo che può essere passato come parametro alla classe tramite l'utilizzo di extends o super e dell'ereditarietà delle classi. Infatti, poiché i tipi che vengono passati sono delle classi, esse hanno una superclasse e una sottoclasse, attraverso esse è possibile istruire il compilatore affinché accetti classi che rientrino entro il limite superiore o il limite inferiore imposto, tramite extends o super rispettivamente. Si considerino ad esempio le seguenti classi:

class ClassePadre {}
class ClasseFiglio extends ClassePadre{}
class ClasseFiglio2 extends ClasseFiglio{}

Ora, se si dovesse creare una classe generic che, per possibili problemi con il codice scritto al suo interno, debba accettare solo classi che ereditano da una certa classe, ad esempio: da ClasseFiglio, la classe Generic dovrà avere questo aspetto:

class ClasseGeneric<T extends ClasseFiglio>{
    [...]
}

in questo modo ogni tentativo di passare una classe che non erediti da ClasseFiglio, come ClassePadre, non farà compilare.
Mentre se si vuole che una classe Generic accetti tutte le classi superclassi di una classe, la classe Generic dovrà delimitare con super, prendendo d'esempio le classi prima presentate, avrà il seguente aspetto:

class ClasseGeneric<T super ClasseFiglio>{
    [...]
}

in questo modo ogni tentativo di passare alla classe generic una sottoclasse della classe ClasseFiglio, come ClasseFiglio2, non farà compilare.

La clausola & permette di definire un intersection type.[A 2] Essa risulta utile quando si vuole cambiare la erasure di un metodo per mantenere la retrocompatibilità, a livello di bytecode (binary compatibility), con il codice compilato prima dell'introduzione dei generics.

Heap pollution

[modifica | modifica sorgente]

Implementazione

[modifica | modifica sorgente]

Il compilatore elimina i generics dal codice sorgente, e inserisce automaticamente i cast necessari per simulare questa funzionalità. Il bytecode contiene informazioni sotto forma di metadati che identificano i tipi e i metodi generici, ma il codice eseguibile dei metodi non contiene queste informazioni: il risultato è che il codice eseguito dalla macchina virtuale non usa le informazioni sui generics.

Ciò è stato necessario per introdurre i generics nel linguaggio, senza che ciò richiedesse l'aggiornamento della specifica della macchina virtuale. Il codice post-1.5 può essere eseguito anche su macchine virtuali che aderiscono alla seconda edizione della JVMS.

Tuttavia, ciò ha uno svantaggio importante: i generics non possono essere usati dove è necessario conoscere quale sia il tipo effettivo assegnato a run-time, ed è questo il motivo principale per cui il linguaggio impone delle restrizioni sull'uso delle type variables.

Differenze con il C++

[modifica | modifica sorgente]

In C++, un template è una classe o una funzione che definisce uno o più parametri di tipo, ed è l'analogo della classe generica o del metodo generico in Java; tuttavia, il meccanismo di implementazione è completamente diverso: in C++, il codice sorgente della funzione o della classe viene copiato ogni volta che un client lo richiama con un tipo ben preciso.

La programmazione generica in informatica

[modifica | modifica sorgente]

Con la programmazione generica si ottiene il cosiddetto polimorfismo parametrico, chiamato così perché i tipi effettivi sono passati al codice come parametri. Non va confuso con l'altro tipo di polimorfismo riscontrato spesso in programmazione, che è il polimorfismo per inclusione, e che si ottiene usando i meccanismi di ereditarietà tra tipi.

Il polimorfismo parametrico si basa sulla definizione dei cosiddetti tipi universali, che hanno questo nome perché il codice scritto vale per ogni tipo effettivo ammesso; la clausola "per ogni" si esprime matematicamente con il cosiddetto "quantificatore universale". I tipi universali corrispondono alle type variables del Java.
Analogamente, il polimorfismo per inclusione si basa sulla definizione di tipi esistenziali, che hanno questo nome perché esiste almeno un tipo compatibile; la clausola "esiste almeno un" si esprime matematicamente con il cosiddetto "quantificatore esistenziale".

Note esplicative


Fonti
  1. The Java Language Specification, Chapter 4
Approfondimenti
  1. Capitolo 6 della specifica di linguaggio.
  2. Intersection Types

Bibliografia

[modifica | modifica sorgente]
Specifiche
Materiale presente sul sito della Oracle
Materiale di terze parti