Programmazione Generica in Java
I generics
La programmazione generica consiste nella possibilità di definire classi, interfacce, metodi e costruttori che possono essere utilizzati con tipi diversi di dati.
Con l’uso dei generics e quindi della programmazione generica:
- si può scrivere meno codice, consentendo al programmatore di scrivere algoritmi generici;
- si ottiene un codice più leggibile;
- e un codice facilmente manutenibile.
Classi generiche
Quando il concetto di programmazione generica viene applicato alle classi si potranno ottenere oggetti di quella classe che sono indipendenti rispetto ai tipi di dati che la classe manipola. Questo permette di definire classi che operano su tipi di dato diverso e implementano un comportamento comune e indipendente dal dato in questione.
La sintassi per definire una classe generica prevede l’introduzione, nella definizione della classe, dei parametri di tipo. I parametri di tipo sono degli identificatori la cui reale natura sarà fornita solamente nel momento dell’utilizzo della classe.
La sintassi per definire una classe di tipo generics sarà:
[opzmodificatoreVisibilità] class interface NomeClasse <TipoVariabile1, TipoVariabile2…..> {
Costruttori,
Istanze di variabili,
Metodi
}
Rispetto ad una classe normale, sono stati aggiunti tra parentesi angolari dei parametri di tipo (type parameters) che possono essere usati come tipi dichiarati per variabili, parametri e valori di ritorno. I parametri di tipo non sono tipi di dati realmente esistenti, ma saranno sostituiti con un tipo reale quando sarà istanziato un oggetto da una classe generica.
Un esempio di classe generica è la seguente classe Box che include tra le parentesi angolari un parametro <T>.
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
Il parametro T sta a marcare un futuro parametro, non definito , che potrà contenere valori non primitivi. Quindi potrà assumere valori di tipo Integer, Float, Double, Character, String e di altre classi.
Per convenzione, i nomi dei parametri di tipo sono lettere maiuscole singole. Questo è in netto contrasto con le convenzioni di denominazione delle variabili in genere, e con una buona ragione: senza questa convenzione, sarebbe difficile distinguere tra una variabile di tipo e una classe ordinaria o un nome di interfaccia.
I nomi dei parametri di tipo più comunemente usati sono:
Invocare e istanziare un tipo generico
Per fare riferimento alla classe Box generica si deve eseguire una chiamata di tipo generico, che sostituisce T con un valore concreto, come Integer :
Per creare un'istanza di questa classe si deve utilizzare come al solito la parola chiave new , ma in aggiunta è necessario inserire <Integer> tra il nome della classe e la parentesi:
Box<Integer> interoBox = new Box<Integer>();
Si può considerare una chiamata di tipo generico come qualcosa di simile a una normale chiamata a un metodo, ma invece di passare un argomento a un metodo si passa un argomento di tipo , in questo caso Intero , alla classe Box stessa. In questo modo è come se avessimo sostituito al parametro T la classe Integer in tutta la definizione della classe. In questo modo il metodo
public void set(T t) {
this.t = t;
}
accetta soltanto interi, passati come Integer. Qualora si provasse ad usare un tipo di dato diverso (esempio Double) si riceverebbe un errore di compilazione.
Da questa classe è possibile istanziare anche altri oggetti con un tipo di parametro diverso, senza dover ridefinire nulla all’interno della classe.
Ad esempio si potrebbe istanziare un oggetto passando al parametro un tipo String
Box<String> interoBox = new Box<String>();
senza dover ridefinire il tipo di dati con il quale il metodo set lavora.
Tutto ciò semplifica la scrittura del codice, evitando ad esempio il ricorso all’overload dei metodi (ossia prevedere più metodi con lo stesso nome e che si differenziano per una diversa tipologia di parametri.
C’e da tener presente che:
Box<String> interoBox = new Box<String>();
Consigliati da LinkedIn
è del tutto equivalente a:
Box<String> interoBox = new Box<>();
Ciò è reso possibile dalla funzionalità denominata inferenza di tipo.
Ad esempio:
public class Box<T, N> {......
Ciò obbliga a definire due tipi nella creazione dell’oggetto:
Box<String, Integer> interoBox = new Box<>();
Quando si crea un oggetto da una classe parametrica, non si può sostituire il parametro con un tipo di dato “primitivo” come: int, float, double, char.
Ad esempio crea un errore in compilazione la scrittura:
Box<int> interoBox = new Box<>();
Metodi generici
E’ possibile definire metodi generici, ossia con parametri di tipo non definito, sia all’interno di classi ordinarie, sia all’interno di classi generiche . In questo caso possono avere parametri che ridefiniscono i parametri della classe.
In pratica un metodo è generico quando accetta diversi tipi di dato sui quali esegue uno stesso algoritmo. Anche in questo caso, con il ricorso ai generics, si evita l’overload dei metodi.
Rispetto ad un metodo ordinario, un metodo di tipo generico prevede una lista dei parametri di tipo.
In pratica, rispetto ad un metodo ordinario, quale potrebbe essere ad esempio:
public void printArray (int dati []) {.........}
vengono introdotti tra parentesi angolari degli identificatori di tipo generico, eventualmente separati dalla virgola, che rappresentano una sorta di segnaposto per i tipi che effettivamente saranno usati dal parametro nel momento della chiamata del metodo.
Quindi il metodo ordinario, diventa generico quando scriviamo:
public <T1,T1> void printArray (int dati []) {.........}
la regola sintattica richiede che il tipo sia inserito obbligatoriamente prima del tipo di ritorno.
Per quanto riguarda le lettere da usare come parametro, il linguaggio non detta una regola formale, tuttavia per convenzione si assumono validi i seguenti valori:
Esempio di una classe che prevede un metodo generico per la stampa di dati contenuti in un array del quale non si conosce il tipo.
public class StampaGenerics {
public <T> void stampaArray(T[] dati) {
for (T x: dati)
System.out.println("Valore " + x);
}
}
public class Main {
public static void main(String[] args) {
Integer[] valori = {1, 9, 7, 8, 9,};
Double[] decimali = {1.5, 78.9, 4.7, 4.5};
StamaGenerics stamaGenerics = new StamaGenerics();
stampaGenerics.stampaArray(valori); // devo passare valori di tipo Strutturato
stamapGenerics.stampaArray(decimali);
}
}
Si è scritto un solo metodo con il quale si stampano array di qualsiasi tipo.
E’ da ricordare che le variabili parametriche non possono essere di tipo primitivo. Tuttavia java semplifica la gestione dei tipi tramite il ricorso all’autoboxing e auto-unboxing.