Restrizioni sui Generics in Java

Ci sono alcune restrizioni che dobbiamo tenere a mente quando utilizziamo i tipi generici in Java.

Esse riguardano la creazione di oggetti di un parametro di tipo, membri statici, eccezioni e array. In questa lezione, esploreremo queste restrizioni e come influenzano il design del codice.

Concetti Chiave
  • I parametri di tipo non possono essere istanziati direttamente.
  • I membri static non possono utilizzare i parametri di tipo della classe che li racchiude.
  • Non è possibile creare un array il cui tipo di elemento è un parametro di tipo.
  • Le classi generiche non possono estendere Throwable, quindi non è possibile creare eccezioni generiche.

I Parametri di Tipo Non Possono Essere Istanziati

Una prima restrizione sull'uso dei tipi generici riguarda il fatto che non è possibile creare un'istanza di un parametro di tipo.

Ad esempio, consideriamo questa classe:

// Non si può creare un'istanza di T.
class Gen<T> {

    // Dichiarazione di un oggetto di tipo T.
    T og;

    Gen() {
        // Istruzione illegale!
        og = new T();
    }

}

Qui, è illegale tentare di creare un'istanza di T. Il motivo dovrebbe essere facile da capire: il compilatore non sa che tipo di oggetto creare. T è semplicemente un segnaposto.

In realtà, la motivazione deriva da come funziona la cancellazione o erasure dei generics che abbiamo discusso nelle lezioni precedenti. Durante la cancellazione, il compilatore sostituisce T con Object, quindi l'istruzione og = new T(); diventa og = new Object();, che non è ciò che intendiamo.

Per cui, ad esempio, se creiamo un'istanza di Gen come segue:

Gen<String> oggetto = new Gen<String>();

il compilatore trasforma il codice del costruttore in:

og = new Object();

quindi non sta creando una stringa, ma un oggetto di tipo Object.

Restrizioni sui Membri static

Nessun membro static può utilizzare un parametro di tipo dichiarato dalla classe che lo racchiude.

Ad esempio, entrambi i membri static di questa classe sono illegali:

class Sbagliata<T> {

    // Errore: nessuna variabile static di tipo T.
    static T ogg;

    // Errore: nessun metodo static può utilizzare T.
    static T getOgg() {
        return ogg;
    }

}

Per capire perché, dobbiamo ritornare alla cancellazione dei generics e vedere cosa succede se fosse consentito.

Se fosse permesso, il compilatore trasformerebbe il codice in qualcosa del genere:

class Sbagliata {

    static Object ogg;

    static Object getOgg() {
        return ogg;
    }

}

Ora, tuttavia, poiché ogg è static così come getOgg(), non è più legato a un'istanza specifica di Sbagliata. Quindi, non ha senso che sia di tipo T, perché T è un parametro di tipo legato a un'istanza specifica della classe.

Per cui, se istanziamo due oggetti di Sbagliata con tipi diversi, entrambi i membri static farebbero riferimento allo stesso membro static, il che non avrebbe senso:

Sbagliata<Integer> oggettoI = new Sbagliata<Integer>();
Sbagliata<String> oggettoS = new Sbagliata<String>();

// Quale oggetto otteniamo qui?
Object o = Sbagliata.getOgg();

Per evitare questa confusione, Java non consente ai membri static di utilizzare i parametri di tipo della classe che li racchiude.

Tuttavia, è possibile utilizzare i parametri di tipo nei membri static di classi sia generiche che non generiche, come mostrato qui:

class Corretta<T> {

    static <V> V metodoStatico(V v) {
        /* ... */
    }

    /* ... */

}

In questo caso, il metodo metodoStatico() è static e utilizza un parametro di tipo V, che non è legato al parametro di tipo T della classe Corretta. Questo è legale perché V è un parametro di tipo del metodo, non della classe.

A questo punto possiamo invocare metodoStatico() come segue:

Corretta.<Integer>metodoStatico(10);
Corretta.<String>metodoStatico("Test");

Restrizioni sugli Array Generici

Ci sono due importanti restrizioni dei generics che si applicano agli array.

Prima restrizione: non è possibile istanziare un array il cui tipo di elemento è un parametro di tipo.

Seconda restrizione: non è possibile creare un array di riferimenti generici tipo-specifici.

Il seguente breve programma mostra entrambe le situazioni:

// Generics e array.
class Gen<T extends Number> {

    T oggetto;
    // Dichiarazione di un array di tipo T.
    // Qui è legale dichiarare un array di tipo T.
    T[] valori;

    Gen(T o, T[] numeri) {
        oggetto = o;

        // Questa istruzione è illegale.
        // Non si può creare un array di T.
        // valori = new T[10];

        // Ma, questa istruzione è OK.
        // Assegna un riferimento ad un array esistente.
        valori = numeri;
    }

}

class ArrayGenerici {

    public static void main(String[] args) {

        Integer[] n = { 1, 2, 3, 4, 5 };
        Gen<Integer> oggettoI = new Gen<Integer>(50, n);

        // Errore: Non si può creare un array
        // di riferimenti generici tipo-specifici.
        // Gen<Integer>[] generici = new Gen<Integer>[10];

        // Ma, questa istruzione è OK.
        // Si può creare un array di riferimenti generici con wildcard.
        Gen<?>[] generici = new Gen<?>[10]; // OK
    }

}

Come mostra il programma, è valido dichiarare un riferimento ad un array di tipo T, come fa questa riga:

T[] valori; // OK

Ma, non è possibile istanziare un array di T, come tenta questa linea commentata:

// Questa istruzione è illegale.
// Non si può creare un array di T.
// valori = new T[10];

La ragione per cui non è possibile creare un array di T è che non c'è modo per il compilatore di sapere che tipo di array creare effettivamente.

Tuttavia, è possibile passare un riferimento ad un array compatibile con T a Gen() quando un oggetto viene creato e assegnare quel riferimento a valori, come fa il programma in questa riga:

// Assegna un riferimento ad un array esistente.
valori = numeri;

Questo funziona perché l'array passato a Gen ha un tipo conosciuto, che sarà lo stesso tipo di T al momento della creazione dell'oggetto.

All'interno di main(), notiamo che non è possibile dichiarare un array di riferimenti ad un tipo generico specifico. Cioè, questa linea

// Errore: Non si può creare un array
// di riferimenti generici tipo-specifici.
// Gen<Integer>[] generici = new Gen<Integer>[10];

non compilerà.

È possibile creare un array di riferimenti ad un tipo generico se utilizziamo un wildcard, tuttavia, come mostrato qui:

// Si può creare un array di riferimenti generici con wildcard.
Gen<?>[] generici = new Gen<?>[10]; // OK

Questo approccio è migliore dell'usare un array di tipi raw, perché almeno qualche controllo di tipo sarà ancora applicato.

Restrizione delle Eccezioni Generiche

Una classe generica non può estendere Throwable. Questo significa che non è possibile creare classi di eccezione generiche.

Per esempio, questa classe non è valida:

// ERRORE
// Non è possibile estendere Throwable con un tipo generico.
class EccezioneGenerica<T> extends Throwable {
    T valore;

    EccezioneGenerica(T v) {
        valore = v;
    }

    @Override
    public String getMessage() {
        return "Valore: " + valore;
    }

}

Il motivo per cui non è possibile estendere Throwable con un tipo generico è che le eccezioni devono essere istanziate con un tipo specifico al momento del lancio. Poiché i generics sono cancellati durante la compilazione, non c'è modo di garantire che il tipo specifico sia disponibile al momento dell'istanza dell'eccezione.