Gerarchie di Classi Generiche in Java

Le classi generiche possono essere parte di una gerarchia di classi nello stesso modo di una classe non generica.

Quindi, una classe generica può agire come una superclasse o essere una sottoclasse. La differenza chiave tra gerarchie generiche e non generiche è che in una gerarchia generica, qualsiasi argomento di tipo necessario da una superclasse generica deve essere passato su per la gerarchia da tutte le sottoclassi. Questo è simile al modo in cui gli argomenti del costruttore devono essere passati su per una gerarchia.

Concetti Chiave
  • Le classi generiche possono essere parte di una gerarchia di classi, proprio come le classi non generiche.
  • Una classe generica può agire come una superclasse o essere una sottoclasse.
  • In una gerarchia generica, qualsiasi argomento di tipo necessario da una superclasse generica deve essere passato su per la gerarchia da tutte le sottoclassi.
  • Questo è simile al modo in cui gli argomenti del costruttore devono essere passati su per una gerarchia.

Utilizzare una Superclasse Generica

Ecco un semplice esempio di una gerarchia che utilizza una superclasse generica:

// Una semplice gerarchia di classi generiche.
class Gen<T> {
    T oggetto;

    // Passa al costruttore un riferimento a
    // un oggetto di tipo T.
    Gen(T o) {
        oggetto = o;
    }

    // Restituisce oggetto.
    T getOggetto() {
        return oggetto;
    }

}

// Una sottoclasse di Gen.
class Gen2<T> extends Gen<T> {

    // Passa al costruttore un riferimento a
    // un oggetto di tipo T.
    Gen2(T o) {
        super(o);
    }

}

In questa gerarchia, Gen2 estende la classe generica Gen. Si noti come Gen2 è dichiarata dalla seguente riga:

class Gen2<T> extends Gen<T> {

Il parametro di tipo T è specificato da Gen2 ed è anche passato a Gen nella clausola extends. Questo significa che qualunque tipo sia passato a Gen2 sarà anche passato a Gen. Per esempio, questa dichiarazione,

Gen2<Integer> numero = new Gen2<Integer>(100);

passa Integer come parametro di tipo a Gen. Quindi, l'oggetto all'interno della porzione Gen di Gen2 sarà di tipo Integer.

Si noti anche che Gen2 non usa il parametro di tipo T tranne che per supportare la superclasse Gen. Quindi, anche se una sottoclasse di una superclasse generica altrimenti non avrebbe bisogno di essere generica, deve ancora specificare il/i parametro/i di tipo richiesti dalla sua superclasse generica.

Naturalmente, una sottoclasse è libera di aggiungere i propri parametri di tipo, se necessario. Per esempio, ecco una variazione della gerarchia precedente in cui Gen2 aggiunge un parametro di tipo proprio:

// Una sottoclasse può aggiungere i propri parametri di tipo.
class Gen<T> {
    T oggetto;

    // Passa al costruttore un riferimento a
    // un oggetto di tipo T.
    Gen(T o) {
        oggetto = o;
    }

    // Restituisce oggetto.
    T getOggetto() {
        return oggetto;
    }

}

// Una sottoclasse di Gen che definisce un secondo
// parametro di tipo, chiamato V.
class Gen2<T, V> extends Gen<T> {

    V oggetto2;

    // Passa al costruttore un riferimento a
    // un oggetto di tipo T e un oggetto di tipo V.
    Gen2(T o, V o2) {
        super(o);
        oggetto2 = o2;
    }

    // Restituisce oggetto2.
    V getOggetto2() {
        return oggetto2;
    }

}

// Crea un oggetto di tipo Gen2.
class DemoGerarchia {

    public static void main(String[] args) {

        // Crea un oggetto Gen2 per String e Integer.
        Gen2<String, Integer> x =
            new Gen2<String, Integer>("Il valore è: ", 99);

        System.out.print(x.getOggetto());
        System.out.println(x.getOggetto2());
    }

}

Si noti la dichiarazione di questa versione di Gen2, che è mostrata qui:

class Gen2<T, V> extends Gen<T> {

Qui, T è il tipo passato a Gen, e V è il tipo che è specifico di Gen2.

V è usato per dichiarare un oggetto chiamato oggetto2, e come tipo di ritorno per il metodo getOggetto2(). In main(), un oggetto Gen2 è creato in cui il parametro di tipo T è String, e il parametro di tipo V è Integer.

Il programma visualizza il seguente risultato, come previsto:

Il valore è: 99

Una Sottoclasse Generica

È perfettamente accettabile che una classe non generica sia la superclasse di una sottoclasse generica.

Ad esempio, consideriamo questo programma:

// Una classe non generica può essere la superclasse
// di una sottoclasse generica.

// Una classe non generica.
class NonGen {

    int numero;

    // Passa al costruttore un riferimento a
    // un oggetto di tipo int.
    // Questo costruttore non è generico.
    NonGen(int i) {
        numero = i;
    }

    // Restituisce numero.
    // Questo metodo non è generico.
    int getNumero() {
        return numero;
    }
}

// Una sottoclasse generica di NonGen.
// Questa classe è generica sul parametro di tipo T.
// NonGen non è generica, quindi non è necessario
// specificare un argomento di tipo per NonGen.
class Gen<T> extends NonGen {
    // dichiara un oggetto di tipo T
    T oggetto;

    // Passa al costruttore un riferimento a
    // un oggetto di tipo T.
    Gen(T o, int i) {
        super(i);
        oggetto = o;
    }

    // Restituisce oggetto.
    T getOggetto() {
        return oggetto;
    }

}

// Crea un oggetto Gen.
class DemoGerarchie2 {

    public static void main(String[] args) {
        // Crea un oggetto Gen per String.
        Gen<String> w = new Gen<String>("Ciao", 47);

        System.out.print(w.getOggetto() + " ");
        System.out.println(w.getNumero());
    }

}

L'output del programma è mostrato qui:

Ciao 47

Nel programma, notiamo come Gen derivi da NonGen nella seguente dichiarazione:

class Gen<T> extends NonGen {

Poiché NonGen non è generica, non viene specificato alcun argomento di tipo. Pertanto, anche se Gen dichiara il parametro di tipo T, non è necessario per (nè può essere utilizzato da) NonGen. Così, NonGen è ereditata da Gen nel modo normale. Non si applicano condizioni speciali.

Confronti di Tipo a Tempo di Esecuzione All'interno di una Gerarchia Generica

Ricordiamo l'operatore di informazioni sui tipi a tempo di esecuzione instanceof che è stato introdotto nelle lezioni precedenti.

Come spiegato, instanceof determina se un oggetto è un'istanza di una classe. Restituisce true se un oggetto è del tipo specificato o può essere convertito al tipo specificato. L'operatore instanceof può essere applicato agli oggetti delle classi generiche.

La seguente classe dimostra alcune delle implicazioni di compatibilità dei tipi di una gerarchia generica:

// Usa l'operatore instanceof con una gerarchia di classi generiche.

// Una classe generica.
// Questa classe è generica sul parametro di tipo T.
class Gen<T> {

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

    Gen(T o) {
        oggetto = o;
    }

    // Restituisce oggetto.
    T getOggetto() {
        return oggetto;
    }

}

// Una sottoclasse di Gen.
class Gen2<T> extends Gen<T> {

    Gen2(T o) {
        super(o);
    }

}

// Dimostra le implicazioni dell'ID di tipo
// a tempo di esecuzione della gerarchia
// di classi generiche.
class DemoGerarchia3 {

    public static void main(String[] args) {
        // Crea un oggetto Gen per Integers.
        Gen<Integer> iOgg = new Gen<Integer>(88);
        // Crea un oggetto Gen2 per Integers.
        Gen2<Integer> iOgg2 = new Gen2<Integer>(99);
        // Crea un oggetto Gen2 per Strings.
        Gen2<String> strOgg2 = new Gen2<String>("Test Generici");

        // Verifica se iOgg2 è qualche forma di Gen2.
        if(iOgg2 instanceof Gen2<?>)
            System.out.println("iOgg2 è istanza di Gen2");

        // Verifica se iOgg2 è qualche forma di Gen.
        if(iOgg2 instanceof Gen<?>)
            System.out.println("iOgg2 è istanza di Gen");

        System.out.println();

        // Verifica se strOgg2 è un Gen2.
        if(strOgg2 instanceof Gen2<?>)
            System.out.println("strOgg2 è istanza di Gen2");
        // Verifica se strOgg2 è un Gen.
        if(strOgg2 instanceof Gen<?>)
            System.out.println("strOgg2 è istanza di Gen");

        System.out.println();

        // Verifica se iOgg è un'istanza di Gen2, che non è.
        if(iOgg instanceof Gen2<?>)
            System.out.println("iOgg è istanza di Gen2");
        // Verifica se iOgg è un'istanza di Gen, che è.
        if(iOgg instanceof Gen<?>)
            System.out.println("iOgg è istanza di Gen");
    }

}

L'output del programma è mostrato qui:

iOgg2 è istanza di Gen2
iOgg2 è istanza di Gen

strOgg2 è istanza di Gen2
strOgg2 è istanza di Gen

iOgg è istanza di Gen

In questo programma, Gen2 è una sottoclasse di Gen, che è generica sul parametro di tipo T. In main(), vengono creati tre oggetti. Il primo è iOgg, che è un oggetto di tipo Gen<Integer>. Il secondo è iOgg2, che è un'istanza di Gen2<Integer>. Infine, strOgg2 è un oggetto di tipo Gen2<String>.

Poi, il programma esegue questi test instanceof sul tipo di iOgg2:

// Verifica se iOgg2 è qualche forma di Gen2.
if(iOgg2 instanceof Gen2<?>)
    System.out.println("iOgg2 è istanza di Gen2");
// Verifica se iOgg2 è qualche forma di Gen.
if(iOgg2 instanceof Gen<?>)
    System.out.println("iOgg2 è istanza di Gen");

Come mostra l'output, entrambi hanno successo.

Nel primo test, iOgg2 viene controllato contro Gen2<?>. Questo test ha successo perché conferma semplicemente che iOgg2 è un oggetto di qualche tipo di oggetto Gen2. L'uso del wildcard consente a instanceof di determinare se iOgg2 è un oggetto di qualsiasi tipo di Gen2. Successivamente, iOgg2 viene testato contro Gen<?>, il tipo della superclasse. Questo è anche vero perché iOgg2 è qualche forma di Gen, la superclasse. Le prossime righe in main() mostrano la stessa sequenza (e gli stessi risultati) per strOgg2.

Successivamente, iOgg, che è un'istanza di Gen<Integer> (la superclasse), viene testato da queste righe:

// Verifica se iOgg è un'istanza di Gen2, che non è.
if(iOgg instanceof Gen2<?>)
    System.out.println("iOgg è istanza di Gen2");
// Verifica se iOgg è un'istanza di Gen, che è.
if(iOgg instanceof Gen<?>)
    System.out.println("iOgg è istanza di Gen");

Il primo if fallisce perché iOgg non è qualche tipo di oggetto Gen2. Il secondo test ha successo perché iOgg è qualche tipo di oggetto Gen.

Casting e Gerarchie Generiche

È possibile eseguire il cast di un'istanza di una classe generica in un'altra solo se le due sono compatibili e il parametro di tipo della classe generica è lo stesso o un supertipo del parametro di tipo della classe generica in cui si sta eseguendo il cast.

Ad esempio, assumendo il programma precedente, questo cast è legale:

(Gen<Integer>) iOgg2 // legale

perché iOgg2 include un'istanza di Gen<Integer>.

Ma, questo cast:

// Errore di compilazione
(Gen<Long>) iOgg2

non è legale perché iOgg2 non è un'istanza di Gen<Long>.

Override dei Metodi in una Classe Generica

Un metodo in una classe generica può essere sottoposto a override proprio come qualsiasi altro metodo.

Ad esempio, consideriamo questo programma in cui il metodo ottieniOgg() viene sottoposto a override:

// Override di un metodo generico in una classe generica.

// Una classe generica.
// Questa classe è generica sul parametro di tipo T.
class Gen<T> {

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

    // Passa al costruttore un riferimento a
    // un oggetto di tipo T.
    Gen(T o) {
        oggetto = o;
    }

    // Restituisce oggetto.
    T ottieniOggetto() {
        System.out.print("ottieniOggetto() di Gen: " );
        return oggetto;
    }

}

// Una sottoclasse di Gen che effettua l'override di ottieniOggetto().
class Gen2<T> extends Gen<T> {

    Gen2(T o) {
        super(o);
    }

    // Override di ottieniOggetto().
    T ottieniOggetto() {
        System.out.print("ottieniOggetto() di Gen2: ");
        return oggetto;
    }

}

// Dimostra l'override di un metodo generico.
class DemoOverride {

    public static void main(String[] args) {
        // Crea un oggetto Gen per Integers.
        Gen<Integer> oggInt = new Gen<Integer>(88);
        // Crea un oggetto Gen2 per Integers.
        Gen2<Integer> oggInt2 = new Gen2<Integer>(99);
        // Crea un oggetto Gen2 per Strings.
        Gen2<String> oggStr2 = new Gen2<String> ("Test Generics");

        System.out.println(oggInt.ottieniOggetto());
        System.out.println(oggInt2.ottieniOggetto());
        System.out.println(oggStr2.ottieniOggetto());
    }

}

L'output è mostrato qui:

ottieniOggetto() di Gen: 88
ottieniOggetto() di Gen2: 99
ottieniOggetto() di Gen2: Test Generics

Come conferma l'output, la versione sottoposta a override di ottieniOggetto() viene chiamata per gli oggetti di tipo Gen2, ma la versione della superclasse viene chiamata per gli oggetti di tipo Gen.