I Generics: classi generiche, tipi limitati, parametri multipli, wildcards

« Older   Newer »
 
  Share  
.
  1.     +4    
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    I Generics
    Interamente a cura di Marco 'RootkitNeo'


    Prefazione

    Dopo molti mesi di stop, eccomi tornato con un nuovo articolo! Questa volta ho deciso di tornare su Java, poichè ho notato l'assenza di un importante concetto: i Generics! Nell'articolo vedremo cosa sono, perchè sono stati introdotti e qualche esempio (il più possibile pratico, e magari utile). Non verranno trattati i Generics al completo, poichè l'articolo risulterebbe davvero troppo lungo; verranno però spiegate le basi del loro utilizzo, sino agli argomenti wildcards.


    Un po' di storia

    Nel corso dell'articolo verrà mostrata la loro sintassi, ma al momento mi limito ad una spiegazione puramente "teorica", così da volegere il nostro sguardo al passato, almeno per un attimo, così da comprendere meglio la loro importanza.
    I tipi generici, chiamati appunto Generics, consentono di creare delle classi e dei metodi generici in modo tale da permettere il riutilizzi di queste classi con tipi di dati differenti, senza dover riscrivere una classe identica. Un esempio classico sono sicuramente le liste, la gestione delle code, gli stack: in tutti questi casi, l'unica differenza è il tipo di dato memorizzato. Pensandoci infatti, qual è la differenza tra la gestione di uno Stack di interi ed uno di stringhe? Praticamente nessuna! L'unica differenza risiede nel tipo di dato.

    Prima dell'avvento di Java 1.5 (Java 5), la riscrittura di una classe solo per gestire un tipo di dato differente, veniva comunque evitata. In che modo? Veniva utilizzata la superclasse Object. Com'è noto, questa classe è la superclasse di tutte le altre classi, e questo consente di effettuare il cast di qualsiasi oggetto in un Object. Utilizzando questa strategia si poteva scrivere una classe che accettasse qualsiasi tipo di dato. Il problema in realtà è proprio questo: creando uno stack, era possibile inserire qualsiasi tipo di dato nello stesso momento; vale a dire che si poteva inserire un intero,
    una stringa o qualsiasi altro oggetto all'interno della stessa struttura. A prima vista potrebbe non sembrare tanto grave, ma per mezzo di un esempio pratico, vedremo invece che il problema è rilevante:



    Ciò che avviene nella classe mostrata qui sopra, è la gestione di un classico stack, che si espande automaticamente quando il numero di elementi non può essere contenuto nella dimensione attuale. Come noterete, l'operazione di pop() restituisce un Object. Questo fa si che il chiamante poi debba castare quel dato per poterlo utilizzare. Ricordate che può essere qualsiasi oggetto, come String, Integer o un oggetto creato dal programmatore. Quindi un cast è obbligatorio.


    Classe di test
    CODICE
    class Test1 {
     public static void main(String[] args) {
       OldStack os = new OldStack();
       
       // Inseriamo delle stringhe nel nostro Stack
       os.push("a");
       os.push("b");
       // Ora inseriamo un intero...
       os.push(1);
       
       // Dovendo estrarre il valore con pop(), e' poi
       // necessario castarlo esplicitamente se lo memorizziamo
       String first  = (String) os.pop();  // Exception
       String second = (String) os.pop();
       String third  = (String) os.pop();
     }
    }


    Se provate ad eseguire questo codice, otterrete questo output:
    CODICE
    Exception in thread "main" java.lang.ClassCastException: java.lang.Integer canno
    t be cast to java.lang.String
           at Test1.main(Test1.java:13)


    La riga 13 è quella indicata con Exception. Ricordo che lo Stack è una pila LIFO (l'ultimo elemento ad entrare, è il primo ad uscire), quindi il primo elemento che estrarremo sarà l'ultimo inserito, in questo caso, il numero 1 (Integer). Cosa evidenzia questo fatto? Che una gestione errata, anche un piccolo errore, provocherebbe un errore in fase di runtime (durante l'esecuzione).

    Java >= 5: i Generics

    L'esempio che abbiamo visto, mette principalmente in evidenza che il codice utilizzato, pecca sicuramente di sicurezza del tipo. Non può esserci un controllo in fase di compilazione, semplicemente perchè non si sa che dato verrà inserito, e poi a quale dato avverrà il cast.

    I Generics nascono anche per questo motivo: garantiscono la sicurezza del tipo.

    La sintassi di una classe che utilizza i Generics è diversa rispetto alla classe "tradizionale". Per indicare l'utilizzo di Generics vengono utilizzate le "parentesi uncinate", < >, con all'interno un segnaposto o più (in base ai parametri).
    CODICE
    class Stack<T> {...}


    Non vi è una regola particolare, qualsiasi segnaposto va bene. Di solito però è qualcosa di esplicativo: nel caso di una mappa, potrebbe essere una buona idea utilizzare K, V (Key, Value).
    All'interno del codice, il segnaposto verrà utilizzato proprio come se fosse il tipo di dato che vogliamo utilizzare. Possiamo quindi definire una variabile utilizzando T.

    Vediamo subito un esempio pratico:



    Il main è il seguente:
    CODICE
    class TestStack {
     public static void main(String[] args) {
       Stack<String> stackString = new Stack<String>();
       
       System.out.println("----------------------------------");
       for(int i=0; i<11; i++) {
         stackString.push(String.valueOf(i+65));
       }

       System.out.println("Size: "+stackString.getSize());
       System.out.println("Real Size: " + stackString.getRealSize());
       
       System.out.println("----------------------------------\nEstrazione");
       
       for(int i=0; i<11; i++) {
         System.out.println(stackString.pop());
       }
       
       System.out.println("----------------------------------");
       
       System.out.println("Size: "+stackString.getSize());
       System.out.println("Real Size: " + stackString.getRealSize());

       // stackString.push(1); Error  
     
     }

    }


    Osservando subito il main, si può notare che l'ultima riga è commentata: togliendo quel commento, otterremmo un errore in fase di compilazione!
    Guardando la prima riga, si nota la sintassi per la creazione di un oggetto con sintassi Generica:
    CODICE
    Stack<String> stackString = new Stack<String>();


    E' infatti la stessa sintassi che troviamo nel Collection Framework con gli ArrayList, le mappe, Vector, e Stack.

    Analizziamo ora il codice della nuova classe Stack:
    CODICE
    class Stack<T> {
     private int size;            // Numero di elementi effettivamente contenuti
     private Object[] elements;   // Elementi contenuti


    Non vi sarà sfuggito che stiamo utilizzando ancora un Object, immagino. Questa è una delle limitazioni dei Generics: non si possono creare array di tipi generici, ma si possono però creare variabili. O per meglio dire, si possono creare riferimenti, ed assegnare ad essi un array passato da costruttore. Nel nostro caso invece dovremmo istanziare un array, e l'operazione non è permessa.
    L'utilizzo di Object comunque non crea il problema visto in precedenza, in quanto ora il cast avviene nella classe che utilizza Object, ed avviene utilizzando l'etichetta T. Queste etichette, che ho già definito prima segnaposto, verranno di fatto sostituite in fase di compilazione con il tipo utilizzato nelle parentesi uncinate. Nel caso del nostro test, il tipo è String, e di conseguenza altri tipi di dati non sono permessi.

    Questa è infatti l'operazione di pop():
    CODICE
    T pop() {
       return (T) elements[size--];
     }



    Parametri di tipo

    Come detto in precedenza, ci possono essere più parametri di tipo ("segnaposti"). Per utilizzare più di un parametro è necessario semplicemente separarli da una virgola, come in questo caso:
    CODICE
    class MyClass<K, V> {...}


    L'utilizzo è praticamente identico al precedente. Sarà possibile utilizzare questi parametri per definire una variabile di quel tipo, quindi:
    CODICE
    class MyClass<K, V> {
     private K key;
     private V value;
     
    }



    Tipi limitati

    Il titolo può trarre in inganno: non si tratta di qualche limitazione imposta dai Generics, ma di una limitazione imposta dal programmatore. Immaginate di voler creare una classe Generica che non consenta di utilizzato un tipo diveso da un numero... come si potrebbe fare? Nell'esempio di Stack infatti non vi è alcuna limitazione.
    E' possibile far si che un tipo generico estenda una classe, così da porre un limite superiore.

    Come esempio si supponga il seguente scenario: deve essere utilizzata una classe generica che al suo interno si occupi di svariati calcoli (magari statistici), e che quindi può avere come tipi solo numeri. Io semplifico la situazione mostrando solo la media:
    CODICE
    class Statistic<T extends Number> {
     private T[] numbers;
     
     Statistic(T[] numbers) {
       this.numbers = numbers;
     }
     
     double getAverage() {
       double sum = 0.0;
       for(T number : numbers) {
         sum += number.doubleValue();
       }
       
       return sum / numbers.length;
     }
    }


    Main:
    CODICE
    class TestNumbers {
     public static void main(String[] args) {
       Statistic<Integer> s = new Statistic<Integer>(new Integer[]{10,20,30,40,50});
       
       System.out.println("Average: "+s.getAverage());
     
     }
    }


    Output:
    CODICE
    Average: 30.0


    Gli aspetti interessanti da notare qui sono due: un codice che richiama doubleValue(), nel caso precedente di Stack, creerebbe problemi; la seconda cosa è che in questo caso se provassimo ad istanziare la classe Statistic utilizzando come tipo di parametro String, otterremmo un errore.
    Perchè si può richiamare doubleValue() in questo caso? Si può semplicemente perchè nel momento della compilazione il compilatore sa che l'oggetto su cui richiamiamo doubleValue() è un numero, e non può essere un altro tipo di dato (lo sa poichè T extends Number).

    Generics con argomenti wildcard

    Come classico esempio vedremo ora l'utilizzo delle wildcards, applicate all'esempio precedente. Ma prima di tutto: cosa sono le wildcards?
    Utilizzando la stessa classe di prima, potremmo aggiungere un metodo che verifichi se due medie (di due oggetti differenti quindi) sono identiche o no. Per implementare questo metodo, non potremo utilizzare il tipo T e definire così l'array da confrontare. Il motivo è presto spiegato:
    CODICE
    boolean compareAerage(Statistics<T> obj) {
     return getAverage() == obj.getAverage();
    }


    Visto che la classe è dichiarata utilizzando come tipo T, questo sarà identico anche in questo metodo: in pratica questo metodo va bene per confrontare due Integer, due Double o in generale due oggetti dello stesso tipo (numerici, ovviamente).

    Per ovviare a questo problema ed utilizzare qualsiasi tipo di parametro, e quindi confrontare Integer con Double o Float, è sufficiente utilizzare le wildcards. La sintassi è sempre la solita, con la differenza che non va specificato il parametro T, ma si utilizza il punto interrogativo, ?.

    Quindi il metodo diventerà:
    CODICE
    boolean compareAverage(Statistics<?> obj){
     return getAverage() == obj.getAverage();
    }


    Utilizzando questo codice che fa uso delle wildcars sarà possibile quindi confrontare qualsiasi tipo di oggetto, indipendentemente dal fatto che siano uguali o no.

    Conclusioni

    L'articolo è ora giunto al termine. Altri argomenti interessanti a proposito dei Generics sono: wildecards limitate, metodi generici e costruttori generici. Una trattazione più importante dovrebbe abbracciare anche l'override dei metodi.

    Per questo articolo, ci vedremo prossimamente su un nuovo articolo!

    Edited by eXander - 22/8/2015, 18:56
     
    .
  2.      
     
    .
    Avatar

    Where there's a user input, there's a vulnerability.

    Group
    Manager
    Posts
    11,133
    Reputazione
    +174

    Status
    Non ho ancora avuto modo di leggerlo tutto però si sentiva la mancanza di un tuo articolo Root! :P Ho modificato le intestazioni e messo i codici sotto spoiler, perché erano veramente troppo lunghi ahahah. :asd: Comunque mi aspetto che sia un buon articolo in quanto tuo!
     
    .
  3. Koskha
         
     
    .

    User deleted


    Come mai in Java Generics e in C++ Template? non riesco a capire queste cose :asd:
     
    .
  4.      
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Template suona più difficile. :asd:

    Grazie comunque! ;)
     
    .
  5. carbos
         
     
    .

    User deleted


    ma quindi perchè fare i tipi limitati? Non basta definire i metodi solo per quel tipo?
     
    .
  6.     +1    
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Perchè se non poni un limite al tipo puoi istanziare quella classe con qualsiasi tipo di oggetto, e la cosa non è corretta, ed a quel punto non potresti impedire la chiamata ad un metodo.
     
    .
5 replies since 22/8/2015, 16:00   845 views
  Share  
.
Top